.. | ||
assets | ||
config | ||
src | ||
.gitignore | ||
LICENSE | ||
package.json | ||
README.md | ||
rollup.config.js |
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.
Libraries
- chalk: For more convenient coloring and styling of output.
- diff: For producing and applying diff patches.
- isomorphic-git: 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:
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.)
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.
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!