Browse Source

feat(git): Add --color and --no-color options

These allow the user to force color on or off. The chalk library is used
for the output, because it's already used elsewhere in Puter and seems
like a good choice.

Ideally, the default will be based on whether stdout is a tty, but Puter
doesn't yet have that concept, so we just default to color.
Sam Atkins 11 months ago
parent
commit
d6dd1a5bb0

+ 13 - 0
package-lock.json

@@ -12130,6 +12130,7 @@
       "dependencies": {
         "@pkgjs/parseargs": "^0.11.0",
         "buffer": "^6.0.3",
+        "chalk": "^5.3.0",
         "diff": "^5.2.0",
         "isomorphic-git": "^1.25.10"
       },
@@ -12165,6 +12166,18 @@
         "ieee754": "^1.2.1"
       }
     },
+    "packages/git/node_modules/chalk": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+      "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
     "packages/git/node_modules/diff": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",

+ 1 - 0
packages/git/package.json

@@ -20,6 +20,7 @@
   "dependencies": {
     "@pkgjs/parseargs": "^0.11.0",
     "buffer": "^6.0.3",
+    "chalk": "^5.3.0",
     "diff": "^5.2.0",
     "isomorphic-git": "^1.25.10"
   }

+ 51 - 0
packages/git/src/color.js

@@ -0,0 +1,51 @@
+/*
+ * 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 chalk from 'chalk';
+
+export const color_options = {
+    'color': {
+        // TODO: '--color[=<when>]' syntax, once we have an args parser that supports optional string option-arguments.
+        description: 'Force colored output.',
+        type: 'boolean',
+    },
+    'no-color': {
+        description: 'Disable colored output.',
+        type: 'boolean',
+    },
+}
+
+/**
+ * Process command-line options related to color, and modify them in place.
+ * Sets the chalk color level based on whether color is enabled or disabled.
+ * @param options Parsed command-line options, which will be modified in place.
+ */
+export const process_color_options = (options) => {
+
+    if (!options['color'] && !options['no-color']) {
+        // TODO: Default to whether we're running in a TTY, once we have that concept.
+        options['color'] = true;
+    }
+
+    if (options['no-color']) {
+        options['color'] = false;
+        delete options['no-color'];
+    }
+
+    chalk.level = options.color ? 3 : 0;
+}

+ 4 - 3
packages/git/src/format.js

@@ -17,6 +17,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 import { shorten_hash } from './git-helpers.js';
+import chalk from 'chalk';
 
 export const commit_formatting_options = {
     'abbrev-commit': {
@@ -428,15 +429,15 @@ export const format_diffs = (diffs, options) => {
             s += `+++ ${b_path}\n`;
 
             for (const hunk of diff.hunks) {
-                s += `\x1b[36;1m@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\x1b[0m\n`;
+                s += chalk.blueBright(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\n`);
 
                 for (const line of hunk.lines) {
                     switch (line[0]) {
                         case '+':
-                            s += `\x1b[32;1m${line}\x1b[0m\n`;
+                            s += chalk.greenBright(`${line}\n`);
                             break;
                         case '-':
-                            s += `\x1b[31;1m${line}\x1b[0m\n`;
+                            s += chalk.redBright(`${line}\n`);
                             break;
                         default:
                             s += `${line}\n`;

+ 7 - 2
packages/git/src/subcommands/branch.js

@@ -19,6 +19,8 @@
 import git from 'isomorphic-git';
 import { find_repo_root, shorten_hash } from '../git-helpers.js';
 import { SHOW_USAGE } from '../help.js';
+import { color_options, process_color_options } from '../color.js';
+import chalk from 'chalk';
 
 const BRANCH = {
     name: 'branch',
@@ -64,7 +66,8 @@ const BRANCH = {
                 description: 'Perform the action forcefully. For --delete, ignores whether the branches are fully merged. For --move, --copy, and creating new branches, ignores whether a branch already exists with that name.',
                 type: 'boolean',
                 short: 'f',
-            }
+            },
+            ...color_options,
         },
     },
     execute: async (ctx) => {
@@ -108,6 +111,8 @@ const BRANCH = {
             }
         }
 
+        process_color_options(options);
+
         const { dir, gitdir } = await find_repo_root(fs, env.PWD);
 
         const get_current_branch = async () => git.currentBranch({
@@ -266,7 +271,7 @@ const BRANCH = {
 
             for (const branch of branches) {
                 if (branch === current_branch) {
-                    stdout(`\x1b[32;1m* ${branch}\x1b[0m`);
+                    stdout(chalk.greenBright(`* ${branch}`));
                 } else {
                     stdout(`  ${branch}`);
                 }

+ 4 - 0
packages/git/src/subcommands/diff.js

@@ -23,6 +23,7 @@ import * as Diff from 'diff';
 import path from 'path-browserify';
 import { diff_formatting_options, format_diffs, process_diff_formatting_options } from '../format.js';
 import { diff_git_trees } from '../diff.js';
+import { color_options, process_color_options } from '../color.js';
 
 export default {
     name: 'diff',
@@ -55,6 +56,7 @@ export default {
                 description: 'Compare files, ignoring git.',
                 type: 'boolean',
             },
+            ...color_options,
         },
     },
     execute: async (ctx) => {
@@ -63,6 +65,8 @@ export default {
         const { options, positionals, tokens } = args;
         const cache = {};
 
+        process_color_options(options);
+
         const diff_options = process_diff_formatting_options(options);
         if (diff_options.no_patch && !options['exit-code'])
             return;

+ 3 - 0
packages/git/src/subcommands/log.js

@@ -28,6 +28,7 @@ import {
 import path from 'path-browserify';
 import { SHOW_USAGE } from '../help.js';
 import { diff_git_trees } from '../diff.js';
+import { color_options, process_color_options } from '../color.js';
 
 export default {
     name: 'log',
@@ -39,6 +40,7 @@ export default {
         options: {
             ...commit_formatting_options,
             ...diff_formatting_options,
+            ...color_options,
             'max-count': {
                 description: 'Maximum number of commits to output.',
                 type: 'string',
@@ -54,6 +56,7 @@ export default {
 
         process_commit_formatting_options(options);
         const diff_options = process_diff_formatting_options(options, { show_patch_by_default: false });
+        process_color_options(options);
 
         const depth = Number(options['max-count']) || undefined;
 

+ 8 - 2
packages/git/src/subcommands/push.js

@@ -21,6 +21,8 @@ import http from 'isomorphic-git/http/web';
 import { determine_fetch_remote, find_repo_root, shorten_hash } from '../git-helpers.js';
 import { SHOW_USAGE } from '../help.js';
 import { authentication_options, Authenticator } from '../auth.js';
+import { color_options, process_color_options } from '../color.js';
+import chalk from 'chalk';
 
 export default {
     name: 'push',
@@ -37,6 +39,7 @@ export default {
                 short: 'f',
             },
             ...authentication_options,
+            ...color_options,
         },
     },
     execute: async (ctx) => {
@@ -45,6 +48,8 @@ export default {
         const { options, positionals } = args;
         const cache = {};
 
+        process_color_options(options);
+
         const { dir, gitdir } = await find_repo_root(fs, env.PWD);
 
         const remotes = await git.listRemotes({
@@ -265,12 +270,13 @@ export default {
         stdout(`To ${remote_url}`);
         let any_failed = false;
         for (const { flag, summary, source, dest, reason } of results) {
-            stdout(`${flag === '!' ? '\x1b[31;1m' : ''} ${flag} ${summary.padEnd(19, ' ')}\x1b[0m ${source} -> ${dest}${reason ? ` (${reason})` : ''}`);
+            const flag_and_summary = `${flag} ${summary.padEnd(19, ' ')}`;
+            stdout(` ${ (flag === '!') ? chalk.redBright(flag_and_summary) : flag_and_summary } ${source} -> ${dest}${reason ? ` (${reason})` : ''}`);
             if (reason)
                 any_failed = true;
         }
         if (any_failed) {
-            stderr(`\x1b[31;1merror: Failed to push some refs to '${remote_url}'\x1b[0m`);
+            stderr(chalk.redBright(`error: Failed to push some refs to '${remote_url}'`));
         }
     },
 };

+ 3 - 0
packages/git/src/subcommands/show.js

@@ -28,6 +28,7 @@ import {
     process_diff_formatting_options,
 } from '../format.js';
 import { diff_git_trees } from '../diff.js';
+import { color_options, process_color_options } from '../color.js';
 
 export default {
     name: 'show',
@@ -38,6 +39,7 @@ export default {
         options: {
             ...commit_formatting_options,
             ...diff_formatting_options,
+            ...color_options,
         },
     },
     execute: async (ctx) => {
@@ -47,6 +49,7 @@ export default {
 
         process_commit_formatting_options(options);
         const diff_options = process_diff_formatting_options(options);
+        process_color_options(options);
 
         const { dir, gitdir } = await find_repo_root(fs, env.PWD);