# Puter Git Client This is a JavaScript in-browser git client running on the Puter filesystem, using [isomorphic-git](https://isomorphic-git.org). It aims to match `git`'s interface, so that running a command behaves the same way as it would there. Of course, `git` has a large variety of subcommands and options, some of which are obscure, so many will be missing. Consider adding them if you are interested! :^) While this is built for Puter, the only Puter-specific parts are: - `src/filesystem.js` which integrates with Puter's filesystem. - `src/main.js` which uses the Puter SDK to connect to the parent Phoenix shell. By modifying these, and the values in `config/`, you should be able to port it to other environments. ## Libraries - [**chalk**](https://www.npmjs.com/package/chalk): For more convenient coloring and styling of output. - [**diff**](https://www.npmjs.com/package/diff): For producing and applying diff patches. - [**isomorphic-git**](https://isomorphic-git.org): For interacting with the git repository. See section below. ## Subcommand structure The `git` CLI is structured as a series of sub-commands - `git branch` has nothing in common with `git version`, for example. Each of these is modelled as its own file in `src/subcommands`, and should export a default object with the following structure: ```js export default { // The subcommand's name name: 'help', // A string or array of strings, for how the subcommand is run. Shown in the help. usage: [ 'git help [-a|--all]', 'git help ', ], // A description, shown in the help. description: `Display help information for git itself, or a subcommand.`, // Arguments object, passed to `parseArgs()` args: { allowPositionals: true, options: { all: { // The one deviation from parseArgs() is that each option gets a description, // which is shown in the help output. description: 'List all available subcommands.', type: 'boolean', } }, }, // Function to actually run the subcommand. // The return value can be a posix-style exit code, or a plain `return` as a shorthand for 0. // Throwing errors is allowed. // Throwing the special `SHOW_USAGE` value defined in `src/help.js` can be used to print the command usage text. execute: async (ctx) => { // ctx has the following structure: ctx = { io: { stdout, // Function that takes a string, and prints it to stdout stderr, // Function that takes a string, and prints it to stderr }, fs, // A filesystem implementation, for isomorphic-git args, // An object returned from `parseArgs()`. env, // Object containing environment variables, such as PWD for the current working directory. } } } ``` These are them listed in `src/subcommands/__exports__.js`, which can be automatically generated by running `packages/phoenix/tools/gen.js packages/git/src/subcommands` from the Puter repo root. But it's not hard to modify manually. ## Common patterns ### Shared options It's common in a few places that options are shared between commands, for example options related to formatting commits are shared by `git log` and `git show`. In these cases, those options are defined in an object in `src/format.js` with an accompanying function to process their results into a more convenient format. For example, `diff_formatting_options` and `process_diff_formatting_options(options)`: Some of the options are shorthands for others; some "imply" another; and some are set by default under certain conditions. All this is handled in one place instead of in each subcommand that needs them. ### Repo root Since the `git` command may be run from a repository, a subdirectory, or a non-repository, you can locate the git directory like so: ```js const { dir, gitdir } = await find_repo_root(fs, env.PWD); ``` The `dir` and `gitdir` variables can then be passed to isomorphic-git methods that expect them. If no repository is found, this will throw an error, which is then printed to stderr. (isomorphic-git's `git.findRoot()` does not implement checks for a `.git` text file that points to an alternative directory that git's metadata is stored in. We should maybe upstream this.) ### Parallel processing Filesystem access going over the network has a performance cost, so to try and counteract this, we try to do things in parallel. There's a lot of use of `await Promise.all(...)` and `await Promise.allSettled()`. Because isomorphic-git has its own caching, (using the `cache` object,) it's possible that this doesn't actually help. Once performance becomes an issue, it'd be worth experimenting to see if running the same commands in sequence is faster, especially where they access the same files. ## Isomorphic-git The library we use for most interaction with git's files is [isomorphic-git](https://isomorphic-git.org). It handles most of the basics, but because we want to do everything that [git](https://git-scm.com) does, a lot has to be implemented manually. For example, it has a `git.add()` method for adding files, but whereas `git add deleted_file` will stage that deletion, `git.add({ filepath: 'deleted_file', ... })` will complain that the file does not exist. So our implementation of `git add` manually iterates the changed files and either calls `git.add()` or `git.remove()` as appropriate. ### Troubleshooting tips: - File paths given to isomorphic-git need to be relative to the repo root. Absolute paths often silently do nothing!