puter/packages/git
Sam Atkins 955154468f feat(git): Make git add work for deleted files
Previously, this:

```sh
> rm deleted.txt
> git add deleted.txt
```

...would unhelpfully complain that deleted.txt cannot be found. Now, it
records the deleted file to the index, just as canonical git does.
2024-06-28 14:19:23 -04:00
..
assets Add boilerplate for empty git app 2024-05-22 08:56:50 +01:00
config tweak(git): Move proxy url definition into config files 2024-06-28 14:19:23 -04:00
src feat(git): Make git add work for deleted files 2024-06-28 14:19:23 -04:00
.gitignore Add boilerplate for empty git app 2024-05-22 08:56:50 +01:00
LICENSE Add boilerplate for empty git app 2024-05-22 08:56:50 +01:00
package.json feat(git): Add --color and --no-color options 2024-06-28 14:19:23 -04:00
README.md docs(git): Document git client 2024-06-28 14:19:23 -04:00
rollup.config.js refactor: flatten the monorepo 2024-06-08 01:07:42 -04:00

Puter Git Client

This is a JavaScript in-browser git client running on the Puter filesystem, using isomorphic-git.

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.

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:

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 <command>',
    ],
    // 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:

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.)

Isomorphic-git

The library we use for most interaction with git's files is isomorphic-git.

It handles most of the basics, but because we want to do everything that git 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!