Explorar o código

Implement `git help`, `git version`, and subcommand infrastructure

Each subcommand is its own file, modeled after Phoenix's coreutils.
Sam Atkins hai 1 ano
pai
achega
85b7587c42

+ 151 - 12
package-lock.json

@@ -3357,6 +3357,14 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/@protobufjs/aspromise": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -4547,6 +4555,11 @@
       "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
       "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
     },
+    "node_modules/async-lock": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
+      "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
+    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -5148,6 +5161,11 @@
         "node": ">= 10.0"
       }
     },
+    "node_modules/clean-git-ref": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz",
+      "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="
+    },
     "node_modules/clean-stack": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -5499,6 +5517,17 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/cross-fetch": {
       "version": "3.1.8",
       "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
@@ -5768,6 +5797,11 @@
         "node": ">=0.3.1"
       }
     },
+    "node_modules/diff3": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz",
+      "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="
+    },
     "node_modules/dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -7226,7 +7260,6 @@
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
       "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
-      "dev": true,
       "engines": {
         "node": ">= 4"
       }
@@ -7615,6 +7648,43 @@
         "whatwg-fetch": "^3.4.1"
       }
     },
+    "node_modules/isomorphic-git": {
+      "version": "1.25.10",
+      "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.10.tgz",
+      "integrity": "sha512-IxGiaKBwAdcgBXwIcxJU6rHLk+NrzYaaPKXXQffcA0GW3IUrQXdUPDXDo+hkGVcYruuz/7JlGBiuaeTCgIgivQ==",
+      "dependencies": {
+        "async-lock": "^1.4.1",
+        "clean-git-ref": "^2.0.1",
+        "crc-32": "^1.2.0",
+        "diff3": "0.0.3",
+        "ignore": "^5.1.4",
+        "minimisted": "^2.0.0",
+        "pako": "^1.0.10",
+        "pify": "^4.0.1",
+        "readable-stream": "^3.4.0",
+        "sha.js": "^2.4.9",
+        "simple-get": "^4.0.1"
+      },
+      "bin": {
+        "isogit": "cli.cjs"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/isomorphic-git/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/istanbul-lib-coverage": {
       "version": "3.2.2",
       "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -8402,6 +8472,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/minimisted": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz",
+      "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==",
+      "dependencies": {
+        "minimist": "^1.2.5"
+      }
+    },
     "node_modules/minipass": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
@@ -9303,6 +9381,11 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -9423,6 +9506,14 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/pixelmatch": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
@@ -10280,6 +10371,18 @@
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
       "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
     },
+    "node_modules/sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "dependencies": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      },
+      "bin": {
+        "sha.js": "bin.js"
+      }
+    },
     "node_modules/shallow-clone": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
@@ -11947,6 +12050,11 @@
     "packages/git": {
       "version": "1.0.0",
       "license": "AGPL-3.0-only",
+      "dependencies": {
+        "@pkgjs/parseargs": "^0.11.0",
+        "buffer": "^6.0.3",
+        "isomorphic-git": "^1.25.10"
+      },
       "devDependencies": {
         "@rollup/plugin-commonjs": "^24.1.0",
         "@rollup/plugin-node-resolve": "^15.0.2",
@@ -11956,6 +12064,48 @@
         "rollup-plugin-copy": "^3.4.0"
       }
     },
+    "packages/git/node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
+    "packages/git/node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
     "packages/phoenix": {
       "name": "@heyputer/phoenix",
       "version": "0.0.0",
@@ -11988,13 +12138,6 @@
         "node-pty": "^1.0.0"
       }
     },
-    "packages/phoenix/node_modules/@pkgjs/parseargs": {
-      "version": "0.11.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "packages/phoenix/node_modules/@sinonjs/fake-timers": {
       "version": "11.2.2",
       "license": "BSD-3-Clause",
@@ -12079,10 +12222,6 @@
       "version": "3.0.9",
       "license": "MIT"
     },
-    "packages/phoenix/node_modules/path-browserify": {
-      "version": "1.0.1",
-      "license": "MIT"
-    },
     "packages/phoenix/node_modules/randomstring": {
       "version": "1.3.0",
       "license": "MIT",

+ 5 - 0
packages/git/package.json

@@ -16,5 +16,10 @@
     "mocha": "^10.2.0",
     "rollup": "^3.21.4",
     "rollup-plugin-copy": "^3.4.0"
+  },
+  "dependencies": {
+    "@pkgjs/parseargs": "^0.11.0",
+    "buffer": "^6.0.3",
+    "isomorphic-git": "^1.25.10"
   }
 }

+ 39 - 0
packages/git/src/git-command-definition.js

@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024  Puter Technologies Inc.
+ *
+ * This file is part of Puter's Git client.
+ *
+ * Puter's Git client is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * The command definition for `git` itself, in the same format as subcommands.
+ */
+export default {
+    name: 'git',
+    usage: 'git [--version] [--help] [command] [command-args...]',
+    description: 'Git version-control client for Puter.',
+    args: {
+        options: {
+            help: {
+                description: 'Display help information for git itself, or a subcommand.',
+                type: 'boolean',
+            },
+            version: {
+                description: 'Display version information about git.',
+                type: 'boolean',
+            },
+        },
+    },
+};

+ 143 - 0
packages/git/src/help.js

@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024  Puter Technologies Inc.
+ *
+ * This file is part of Puter's Git client.
+ *
+ * Puter's Git client is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Throw this from a subcommand's execute() in order to print its usage text to stderr.
+ * @type {symbol}
+ */
+export const SHOW_USAGE = Symbol('SHOW_USAGE');
+
+/**
+ * Full manual page for the command.
+ * @param command
+ * @returns {string}
+ */
+export const produce_help_string = (command) => {
+    const { name, usage, description, args } = command;
+    const options = args?.options;
+
+    let s = '';
+    const indent = '    ';
+
+    const heading = (text) => {
+        s += `\n\x1B[34;1m${text}:\x1B[0m\n`
+    };
+
+    heading('SYNOPSIS');
+    if (!usage) {
+        s += `${indent}git ${name}\n`;
+    } else if (typeof usage === 'string') {
+        s += `${indent}${usage}\n`;
+    } else {
+        let first = true;
+        for (const usage_line of usage) {
+            if (first) {
+                first = false;
+                s += `${indent}${usage_line}\n`;
+            } else {
+                s += `${indent}${usage_line}\n`;
+            }
+        }
+    }
+
+    if (description) {
+        heading('DESCRIPTION');
+        s += `${indent}${description}\n`;
+    }
+
+    if (typeof options === 'object' && Object.keys(options).length > 0) {
+        heading('OPTIONS');
+        // Figure out how long each invocation is, so we can align the descriptions
+        for (const [name, option] of Object.entries(options)) {
+            // Invocation
+            s += indent;
+            if (option.short)
+                s += `-${option.short}, `;
+            s += `--${name}`;
+            if (option.type !== 'boolean')
+                s += ` <${option.type}>`;
+            s += '\n';
+
+            // Description
+            s += `${indent}${indent}${option.description}\n\n`;
+        }
+    }
+
+    if (!s.endsWith('\n\n'))
+        s += '\n';
+
+    return s;
+}
+
+/**
+ * Usage for the command, which is a short summary.
+ * @param command
+ * @returns {string}
+ */
+export const produce_usage_string = (command) => {
+    const { name, usage, args } = command;
+    const options = args?.options;
+
+    let s = '';
+
+    // Usage
+    if (!usage) {
+        s += `usage: git ${name}\n`;
+    } else if (typeof usage === 'string') {
+        s += `usage: ${usage}\n`;
+    } else {
+        let first = true;
+        for (const usage_line of usage) {
+            if (first) {
+                first = false;
+                s += `usage: ${usage_line}\n`;
+            } else {
+                s += `   or: ${usage_line}\n`;
+            }
+        }
+    }
+
+    // List of options
+    if (typeof options === 'object' && Object.keys(options).length > 0) {
+        // Figure out how long each invocation is, so we can align the descriptions
+        const option_strings = Object.entries(options).map(([name, option]) => {
+            let invocation = '';
+            if (option.short)
+                invocation += `-${option.short}, `;
+            invocation += `--${name}`;
+            if (option.type !== 'boolean')
+                invocation += ` <${option.type}>`;
+
+            return [invocation, option.description];
+        });
+
+        const indent_size = 2 + option_strings.reduce(
+            (max_length, option) => Math.max(max_length, option[0].length), 0);
+
+        s += '\n';
+        for (const [invocation, description] of option_strings) {
+            s += `    ${invocation}`;
+            if (indent_size - invocation.length > 0)
+                s += ' '.repeat(indent_size - invocation.length);
+            s += `${description}\n`;
+        }
+    }
+
+    return s;
+}

+ 144 - 3
packages/git/src/main.js

@@ -16,6 +16,147 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-window.main = () => {
-    console.log('Well hello friends!');
-}
+import { parseArgs } from '@pkgjs/parseargs';
+import subcommands from './subcommands/__exports__.js';
+import git_command from './git-command-definition.js';
+import fs from './filesystem.js';
+import git from 'isomorphic-git';
+import { Buffer } from 'buffer';
+import { produce_usage_string, SHOW_USAGE } from './help.js';
+
+const encoder = new TextEncoder();
+
+window.Buffer = Buffer;
+
+window.main = async () => {
+    const shell = puter.ui.parentApp();
+    if (!shell) {
+        await puter.ui.alert('Git must be run from a terminal. Try `git --help`');
+        puter.exit();
+        return;
+    }
+
+    shell.on('close', () => {
+        console.log('Shell closed; exiting git...');
+        puter.exit();
+    });
+
+    const stdout = (message) => {
+        shell.postMessage({
+            $: 'stdout',
+            data: encoder.encode(message + '\n'),
+        });
+    };
+    // TODO: Separate stderr message?
+    const stderr = stdout;
+
+    const url_params = new URL(document.location).searchParams;
+    const puter_args = JSON.parse(url_params.get('puter.args')) ?? {};
+    const { command_line, env } = puter_args;
+
+    // isomorphic-git assumes the Node.js process object exists,
+    // so fill-in the parts it uses.
+    window.process = {
+        cwd: () => env.PWD,
+        platform: 'puter',
+    }
+
+    // Git's command structure is a little unusual:
+    // > git [options-for-git] [subcommand [options-and-args-for-subcommand]]
+    // Also, a couple of options (--help and --version) are syntactic sugar for `help` and `version` subcommands.
+    // The approach here is to first try and parse these top-level options, and then based on that, run a subcommand.
+
+    // If no raw args, just print help and exit
+    const raw_args = command_line?.args ?? [];
+    if (raw_args.length === 0) {
+        stdout(produce_usage_string(git_command));
+        puter.exit();
+        return;
+    }
+
+    const { values: global_options, positionals: global_positionals } = parseArgs({
+        options: git_command.args.options,
+        allowPositionals: true,
+        args: raw_args,
+        strict: false,
+    });
+
+    let subcommand_name = null;
+    let first_positional_is_subcommand = false;
+    if (global_options.help) {
+        subcommand_name = 'help';
+    } else if (global_options.version) {
+        subcommand_name = 'version';
+    }
+
+    if (!subcommand_name) {
+        subcommand_name = global_positionals[0];
+        first_positional_is_subcommand = true;
+    }
+
+    // See if we're running a subcommand we recognize
+    let exit_code = 0;
+    const subcommand = subcommands[subcommand_name];
+    if (!subcommand) {
+        stderr(`git: '${subcommand_name}' is not a recognized git command. See 'git --help'`);
+        puter.exit(1);
+        return;
+    }
+
+    // Try and remove the subcommand positional arg, and any global options, from args.
+    const subcommand_args = raw_args;
+    const remove_arg = (arg) => {
+        const index = subcommand_args.indexOf(arg);
+        if (index >= 0)
+            subcommand_args.splice(index, 1);
+
+    }
+    remove_arg('--help');
+    remove_arg('--version');
+    if (first_positional_is_subcommand) {
+        // TODO: This is not a 100% reliable way to do this, as it may also match the value of `--option-with-value value`
+        //       But that's not a problem until we add some global options that take a value.
+        remove_arg(subcommand_name);
+    }
+
+    // Parse the remaining args scoped to this subcommand, and run it.
+    let parsed_args;
+    try {
+        parsed_args = parseArgs({
+            ...subcommand.args,
+            args: subcommand_args,
+        });
+    } catch (e) {
+        stderr(produce_usage_string(subcommand));
+        puter.exit(1);
+        return;
+    }
+
+    const ctx = {
+        io: {
+            stdout,
+            stderr,
+        },
+        fs,
+        args: {
+            options: parsed_args.values,
+            positionals: parsed_args.positionals,
+        },
+        env,
+    };
+
+    try {
+        exit_code = await subcommand.execute(ctx) ?? 0;
+    } catch (e) {
+        if (e === SHOW_USAGE) {
+            stderr(produce_usage_string(subcommand));
+        } else {
+            stderr(`fatal: ${e.message}`);
+            console.error(e);
+        }
+        exit_code = 1;
+    }
+
+    // TODO: Support passing an exit code to puter.exit();
+    puter.exit(exit_code);
+}

+ 26 - 0
packages/git/src/subcommands/__exports__.js

@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024  Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+// Generated by /tools/gen.js
+import module_help from './help.js'
+import module_version from './version.js'
+
+export default {
+    "help": module_help,
+    "version": module_version,
+};

+ 67 - 0
packages/git/src/subcommands/help.js

@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024  Puter Technologies Inc.
+ *
+ * This file is part of Puter's Git client.
+ *
+ * Puter's Git client is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+import git from 'isomorphic-git';
+import { ErrorCodes } from '@heyputer/puter-js-common/src/PosixError.js';
+import subcommands from './__exports__.js';
+import git_command from '../git-command-definition.js';
+import { produce_help_string } from '../help.js';
+
+export default {
+    name: 'help',
+    usage: ['git help [-a|--all]', 'git help <command>'],
+    description: `Display help information for git itself, or a subcommand.`,
+    args: {
+        allowPositionals: true,
+        options: {
+            all: {
+                description: 'List all available subcommands.',
+                type: 'boolean',
+            }
+        },
+    },
+    execute: async (ctx) => {
+        const { io, fs, env, args } = ctx;
+        const { stdout, stderr } = io;
+        const { options, positionals } = args;
+
+        if (options.all) {
+            stdout(`See 'git help <command>' for more information.\n`);
+            const max_name_length = Object.keys(subcommands).reduce((max, name) => Math.max(max, name.length), 0);
+            for (const [name, command] of Object.entries(subcommands)) {
+                stdout(`    ${name} ${' '.repeat(Math.max(max_name_length - name.length, 0))} ${command.description || ''}`);
+            }
+            return;
+        }
+
+        if (positionals.length > 0) {
+            // Try and display help page for the subcommand
+            const subcommand_name = positionals[0];
+            const subcommand = subcommands[subcommand_name];
+            if (!subcommand)
+                throw new Error(`No manual entry for ${subcommand_name}`);
+
+            stdout(produce_help_string(subcommand));
+
+            return;
+        }
+
+        // No subcommand name, so show general help
+        stdout(produce_help_string(git_command));
+    }
+}

+ 40 - 0
packages/git/src/subcommands/version.js

@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024  Puter Technologies Inc.
+ *
+ * This file is part of Puter's Git client.
+ *
+ * Puter's Git client is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+import git from 'isomorphic-git';
+import path from 'path-browserify';
+import { ErrorCodes } from '@heyputer/puter-js-common/src/PosixError.js';
+
+const VERSION = '1.0.0';
+
+export default {
+    name: 'version',
+    usage: 'git version',
+    description: `Display version information about git.`,
+    args: {
+        allowPositionals: false,
+        options: {},
+    },
+    execute: async (ctx) => {
+        const { io, fs, env, args } = ctx;
+        const { stdout, stderr } = io;
+        const { options, positionals } = args;
+
+        stdout(`Puter git version ${VERSION} (isomorphic git version ${git.version()})`);
+    }
+}