mirror of
https://github.com/HeyPuter/puter.git
synced 2025-02-02 14:18:43 +08:00
feat(git): Implement git diff
This commit is contained in:
parent
49c2f16351
commit
622b6a9b92
9
package-lock.json
generated
9
package-lock.json
generated
@ -12130,6 +12130,7 @@
|
||||
"dependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0",
|
||||
"buffer": "^6.0.3",
|
||||
"diff": "^5.2.0",
|
||||
"isomorphic-git": "^1.25.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -12164,6 +12165,14 @@
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"packages/git/node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
|
||||
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"packages/git/node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0",
|
||||
"buffer": "^6.0.3",
|
||||
"diff": "^5.2.0",
|
||||
"isomorphic-git": "^1.25.10"
|
||||
}
|
||||
}
|
||||
|
128
packages/git/src/diff.js
Normal file
128
packages/git/src/diff.js
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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 * as Diff from 'diff';
|
||||
import git from 'isomorphic-git';
|
||||
import path from 'path-browserify';
|
||||
|
||||
/**
|
||||
* Produce an array of diffs from two git tree.
|
||||
* @param fs
|
||||
* @param dir
|
||||
* @param gitdir
|
||||
* @param cache
|
||||
* @param env
|
||||
* @param a_tree A walker object for the left comparison, usually a TREE(), STAGE() or WORKDIR()
|
||||
* @param b_tree A walker object for the right comparison, usually a TREE(), STAGE() or WORKDIR()
|
||||
* @param read_a Callback run to extract the data from each file in a
|
||||
* @param read_b Callback run to extract the data from each file in b
|
||||
* @param context_lines Number of context lines to include in diff
|
||||
* @param path_filters Array of strings to filter which files to include
|
||||
* @returns {Promise<any>} An array of diff objects, suitable for passing to format_diffs()
|
||||
*/
|
||||
export const diff_git_trees = ({
|
||||
fs,
|
||||
dir,
|
||||
gitdir,
|
||||
cache,
|
||||
env,
|
||||
a_tree,
|
||||
b_tree,
|
||||
read_a,
|
||||
read_b,
|
||||
context_lines = 3,
|
||||
path_filters = [],
|
||||
}) => {
|
||||
return git.walk({
|
||||
fs,
|
||||
dir,
|
||||
gitdir,
|
||||
cache,
|
||||
trees: [ a_tree, b_tree ],
|
||||
map: async (filepath, [ a, b ]) => {
|
||||
|
||||
// Reject paths that don't match path_filters.
|
||||
// Or if path_filters is empty, match everything.
|
||||
const abs_filepath = path.resolve(env.PWD, filepath);
|
||||
if (path_filters.length > 0 && !path_filters.some(abs_path =>
|
||||
(filepath === '.') || (abs_filepath.startsWith(abs_path)) || (path.dirname(abs_filepath) === abs_path),
|
||||
)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (await git.isIgnored({ fs, dir, gitdir, filepath }))
|
||||
return null;
|
||||
|
||||
const [ a_type, b_type ] = await Promise.all([ a?.type(), b?.type() ]);
|
||||
|
||||
// Exclude directories from results
|
||||
if ((!a_type || a_type === 'tree') && (!b_type || b_type === 'tree'))
|
||||
return;
|
||||
|
||||
const [
|
||||
a_content,
|
||||
a_oid,
|
||||
a_mode,
|
||||
b_content,
|
||||
b_oid,
|
||||
b_mode,
|
||||
] = await Promise.all([
|
||||
read_a(a),
|
||||
a?.oid() ?? '00000000',
|
||||
a?.mode(),
|
||||
read_b(b),
|
||||
b?.oid() ?? '00000000',
|
||||
b?.mode(),
|
||||
]);
|
||||
|
||||
const diff = Diff.structuredPatch(
|
||||
a_content !== undefined ? filepath : '/dev/null',
|
||||
b_content !== undefined ? filepath : '/dev/null',
|
||||
a_content ?? '',
|
||||
b_content ?? '',
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
context: context_lines,
|
||||
newlineIsToken: true,
|
||||
});
|
||||
|
||||
// Diffs with no changes lines, but a changed mode, still need to show up.
|
||||
if (diff.hunks.length === 0 && a_mode === b_mode)
|
||||
return;
|
||||
|
||||
const mode_string = (mode) => {
|
||||
if (!mode)
|
||||
return '000000';
|
||||
return Number(mode).toString(8);
|
||||
};
|
||||
|
||||
return {
|
||||
a: {
|
||||
oid: a_oid,
|
||||
mode: mode_string(a_mode),
|
||||
},
|
||||
b: {
|
||||
oid: b_oid,
|
||||
mode: mode_string(b_mode),
|
||||
},
|
||||
diff,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
@ -212,3 +212,237 @@ export const format_tag = (tag, options = {}) => {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export const diff_formatting_options = {
|
||||
'patch': {
|
||||
description: 'Generate a patch.',
|
||||
type: 'boolean',
|
||||
short: 'p',
|
||||
},
|
||||
'no-patch': {
|
||||
description: 'Suppress patch output. Useful for commands that output a patch by default.',
|
||||
type: 'boolean',
|
||||
short: 's',
|
||||
},
|
||||
'raw': {
|
||||
description: 'Generate diff in raw format.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'patch-with-raw': {
|
||||
description: 'Alias for --patch --raw.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'numstat': {
|
||||
description: 'Generate a diffstat in a machine-friendly format.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'summary': {
|
||||
description: 'List newly added, deleted, or moved files.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'unified': {
|
||||
description: 'Generate patches with N lines of context. Implies --patch.',
|
||||
type: 'string',
|
||||
short: 'U',
|
||||
},
|
||||
'src-prefix': {
|
||||
description: 'Show the given source prefix instead of "a/".',
|
||||
type: 'string',
|
||||
},
|
||||
'dst-prefix': {
|
||||
description: 'Show the given destination prefix instead of "b/".',
|
||||
type: 'string',
|
||||
},
|
||||
'no-prefix': {
|
||||
description: 'Do not show source or destination prefixes.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'default-prefix': {
|
||||
description: 'Use default "a/" and "b/" source and destination prefixes.',
|
||||
type: 'boolean',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Process command-line options related to diff formatting, and return an options object to pass to format_diff().
|
||||
* @param options Parsed command-line options.
|
||||
* @returns {{raw: boolean, numstat: boolean, summary: boolean, patch: boolean, context_lines: number, no_patch: boolean, source_prefix: string, dest_prefix: string }}
|
||||
*/
|
||||
export const process_diff_formatting_options = (options) => {
|
||||
const result = {
|
||||
raw: false,
|
||||
numstat: false,
|
||||
summary: false,
|
||||
patch: false,
|
||||
context_lines: 3,
|
||||
no_patch: false,
|
||||
source_prefix: 'a/',
|
||||
dest_prefix: 'b/',
|
||||
};
|
||||
|
||||
if (options['raw'])
|
||||
result.raw = true;
|
||||
if (options['numstat'])
|
||||
result.numstat = true;
|
||||
if (options['summary'])
|
||||
result.summary = true;
|
||||
if (options['patch'])
|
||||
result.patch = true;
|
||||
if (options['patch-with-raw']) {
|
||||
result.patch = true;
|
||||
result.raw = true;
|
||||
}
|
||||
if (options['unified'] !== undefined) {
|
||||
result.patch = true;
|
||||
result.context_lines = options['unified'];
|
||||
}
|
||||
|
||||
// Prefixes
|
||||
if (options['src-prefix'])
|
||||
result.source_prefix = options['src-prefix'];
|
||||
if (options['dst-prefix'])
|
||||
result.dest_prefix = options['dst-prefix'];
|
||||
if (options['default-prefix']) {
|
||||
result.source_prefix = 'a/';
|
||||
result.dest_prefix = 'b/';
|
||||
}
|
||||
if (options['no-prefix']) {
|
||||
result.source_prefix = '';
|
||||
result.dest_prefix = '';
|
||||
}
|
||||
|
||||
// If nothing is specified, default to --patch
|
||||
if (!result.raw && !result.numstat && !result.summary && !result.patch)
|
||||
result.patch = true;
|
||||
|
||||
// --no-patch overrides the others
|
||||
if (options['no-patch'])
|
||||
result.no_patch = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a string representation of the given diffs.
|
||||
* @param diffs A single object, or array of them, in the format:
|
||||
* {
|
||||
* a: { mode, oid },
|
||||
* b: { mode, oid },
|
||||
* diff: object returned by Diff.structuredPatch() - see https://www.npmjs.com/package/diff
|
||||
* }
|
||||
* @param options Object returned by process_diff_formatting_options()
|
||||
* @returns {string}
|
||||
*/
|
||||
export const format_diffs = (diffs, options) => {
|
||||
if (!(diffs instanceof Array))
|
||||
diffs = [diffs];
|
||||
|
||||
let s = '';
|
||||
if (options.raw) {
|
||||
// https://git-scm.com/docs/diff-format#_raw_output_format
|
||||
for (const { a, b, diff } of diffs) {
|
||||
s += `:${a.mode} ${b.mode} ${shorten_hash(a.oid)} ${shorten_hash(b.oid)} `;
|
||||
// Status. For now, we just support A/D/M
|
||||
if (a.mode === '000000') {
|
||||
s += 'A'; // Added
|
||||
} else if (b.mode === '000000') {
|
||||
s += 'D'; // Deleted
|
||||
} else {
|
||||
s += 'M'; // Modified
|
||||
}
|
||||
// TODO: -z option
|
||||
s += `\t${diff.oldFileName}\n`;
|
||||
}
|
||||
s += '\n';
|
||||
}
|
||||
|
||||
if (options.numstat) {
|
||||
// https://git-scm.com/docs/diff-format#_other_diff_formats
|
||||
for (const { a, b, diff } of diffs) {
|
||||
const { added_lines, deleted_lines } = diff.hunks.reduce((acc, hunk) => {
|
||||
const first_char_counts = hunk.lines.reduce((acc, line) => {
|
||||
acc[line[0]] = (acc[line[0]] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
acc.added_lines += first_char_counts['+'] || 0;
|
||||
acc.deleted_lines += first_char_counts['-'] || 0;
|
||||
return acc;
|
||||
}, { added_lines: 0, deleted_lines: 0 });
|
||||
|
||||
// TODO: -z option
|
||||
s += `${added_lines}\t${deleted_lines}\t`;
|
||||
if (diff.oldFileName === diff.newFileName) {
|
||||
s += `${diff.oldFileName}\n`;
|
||||
} else {
|
||||
s += `${diff.oldFileName} => ${diff.newFileName}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: --stat / --compact-summary
|
||||
|
||||
if (options.summary) {
|
||||
// https://git-scm.com/docs/diff-format#_other_diff_formats
|
||||
for (const { a, b, diff } of diffs) {
|
||||
if (diff.oldFileName === diff.newFileName)
|
||||
continue;
|
||||
|
||||
if (diff.oldFileName === '/dev/null') {
|
||||
s += `create mode ${b.mode} ${diff.newFileName}\n`;
|
||||
} else if (diff.newFileName === '/dev/null') {
|
||||
s += `delete mode ${a.mode} ${diff.oldFileName}\n`;
|
||||
} else {
|
||||
// TODO: Abbreviate shared parts of path - see git manual link above.
|
||||
s += `rename ${diff.oldFileName} => ${diff.newFileName}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.patch) {
|
||||
for (const { a, b, diff } of diffs) {
|
||||
const a_path = diff.oldFileName.startsWith('/') ? diff.oldFileName : `${options.source_prefix}${diff.oldFileName}`;
|
||||
const b_path = diff.newFileName.startsWith('/') ? diff.newFileName : `${options.dest_prefix}${diff.newFileName}`;
|
||||
|
||||
// NOTE: This first line shows `a/$newFileName` for files that are new, not `/dev/null`.
|
||||
const first_line_a_path = a_path !== '/dev/null' ? a_path : `${options.source_prefix}${diff.newFileName}`;
|
||||
s += `diff --git ${first_line_a_path} ${b_path}\n`;
|
||||
if (a.mode === b.mode) {
|
||||
s += `index ${shorten_hash(a.oid)}..${shorten_hash(b.oid)} ${a.mode}`;
|
||||
} else {
|
||||
if (a.mode === '000000') {
|
||||
s += `new file mode ${b.mode}\n`;
|
||||
} else {
|
||||
s += `old mode ${a.mode}\n`;
|
||||
s += `new mode ${b.mode}\n`;
|
||||
}
|
||||
s += `index ${shorten_hash(a.oid)}..${shorten_hash(b.oid)}\n`;
|
||||
}
|
||||
if (!diff.hunks.length)
|
||||
continue;
|
||||
|
||||
s += `--- ${a_path}\n`;
|
||||
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`;
|
||||
|
||||
for (const line of hunk.lines) {
|
||||
switch (line[0]) {
|
||||
case '+':
|
||||
s += `\x1b[32;1m${line}\x1b[0m\n`;
|
||||
break;
|
||||
case '-':
|
||||
s += `\x1b[31;1m${line}\x1b[0m\n`;
|
||||
break;
|
||||
default:
|
||||
s += `${line}\n`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return s;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import module_checkout from './checkout.js'
|
||||
import module_clone from './clone.js'
|
||||
import module_commit from './commit.js'
|
||||
import module_config from './config.js'
|
||||
import module_diff from './diff.js'
|
||||
import module_fetch from './fetch.js'
|
||||
import module_help from './help.js'
|
||||
import module_init from './init.js'
|
||||
@ -40,6 +41,7 @@ export default {
|
||||
"clone": module_clone,
|
||||
"commit": module_commit,
|
||||
"config": module_config,
|
||||
"diff": module_diff,
|
||||
"fetch": module_fetch,
|
||||
"help": module_help,
|
||||
"init": module_init,
|
||||
|
196
packages/git/src/subcommands/diff.js
Normal file
196
packages/git/src/subcommands/diff.js
Normal file
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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, { STAGE, TREE, WORKDIR } from 'isomorphic-git';
|
||||
import { find_repo_root, group_positional_arguments } from '../git-helpers.js';
|
||||
import { SHOW_USAGE } from '../help.js';
|
||||
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';
|
||||
|
||||
export default {
|
||||
name: 'diff',
|
||||
usage: [
|
||||
'git diff [<options>] [--] [<path>...]',
|
||||
'git diff [<options>] --cached [--] [<path>...]',
|
||||
'git diff [<options>] <commit> [--] [<path>...]',
|
||||
'git diff [<options>] <commit> <commit> [--] [<path>...]',
|
||||
'git diff [<options>] --no-index [--] <path> <path>',
|
||||
],
|
||||
description: `Show changes between commits, the working tree, and elsewhere.`,
|
||||
args: {
|
||||
allowPositionals: true,
|
||||
tokens: true,
|
||||
options: {
|
||||
...diff_formatting_options,
|
||||
'cached': {
|
||||
description: 'Show changes staged for commit.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'exit-code': {
|
||||
description: 'Exit with 1 if there are differences, or 0 if there are no differences.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'staged': {
|
||||
description: 'Alias for --cached.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'no-index': {
|
||||
description: 'Compare files, ignoring git.',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
execute: async (ctx) => {
|
||||
const { io, fs, env, args } = ctx;
|
||||
const { stdout, stderr } = io;
|
||||
const { options, positionals, tokens } = args;
|
||||
const cache = {};
|
||||
|
||||
const diff_options = process_diff_formatting_options(options);
|
||||
if (diff_options.no_patch && !options['exit-code'])
|
||||
return;
|
||||
|
||||
if (options['staged']) {
|
||||
options['cached'] = true;
|
||||
delete options['staged'];
|
||||
}
|
||||
|
||||
if (options['cached'] && options['no-index']) {
|
||||
stderr('error: --cached and --no-index are mutually exclusive.');
|
||||
throw SHOW_USAGE;
|
||||
}
|
||||
|
||||
if (options['no-index']) {
|
||||
if (positionals.length !== 2) {
|
||||
stderr('error: git diff --no-index expects exactly 2 file paths to compare.');
|
||||
throw SHOW_USAGE;
|
||||
}
|
||||
|
||||
const [ a_rel_path, b_rel_path ] = positionals;
|
||||
const a_path = path.resolve(env.PWD, a_rel_path);
|
||||
const b_path = path.resolve(env.PWD, b_rel_path);
|
||||
const [ a_source, b_source, a_stat, b_stat ] = await Promise.all([
|
||||
fs.promises.readFile(a_path, { encoding: 'utf8' }),
|
||||
fs.promises.readFile(b_path, { encoding: 'utf8' }),
|
||||
fs.promises.stat(a_path),
|
||||
fs.promises.stat(b_path),
|
||||
]);
|
||||
|
||||
const diff = Diff.structuredPatch(a_rel_path, b_rel_path, a_source, b_source, undefined, undefined, {
|
||||
context: diff_options.context_lines,
|
||||
newlineIsToken: true,
|
||||
});
|
||||
|
||||
// Git mode format is, in octal:
|
||||
// 2 digits for the type of file
|
||||
// a 0
|
||||
// 3 digits for the permissions
|
||||
const mode_string = (stat) => {
|
||||
return (stat.isSymbolicLink() ? '12' : '10') + '0' + Number(a_stat.mode).toString(8);
|
||||
}
|
||||
|
||||
const a = { path: a_rel_path, oid: '00000000', mode: mode_string(a_stat) };
|
||||
const b = { path: b_rel_path, oid: '00000000', mode: mode_string(b_stat) };
|
||||
stdout(format_diffs({ a, b, diff }, diff_options));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { dir, gitdir } = await find_repo_root(fs, env.PWD);
|
||||
|
||||
// TODO: Canonical git is more permissive about requiring `--` before the file paths, when it's unambiguous.
|
||||
const { before: commit_args, after: path_args } = group_positional_arguments(tokens);
|
||||
|
||||
// Ensure all commit_args are commit references
|
||||
const resolved_commit_refs = await Promise.allSettled(commit_args.map(commit_arg => {
|
||||
return git.resolveRef({ fs, dir, gitdir, ref: commit_arg });
|
||||
}));
|
||||
for (const [i, ref] of resolved_commit_refs.entries()) {
|
||||
if (ref.status === 'rejected')
|
||||
throw new Error(`bad revision '${commit_args[i]}'`);
|
||||
}
|
||||
|
||||
const path_filters = path_args.map(it => path.resolve(env.PWD, it));
|
||||
const diff_ctx = {
|
||||
fs, dir, gitdir, cache, env,
|
||||
context_lines: diff_options.context_lines,
|
||||
path_filters,
|
||||
};
|
||||
|
||||
const read_tree = walker => walker?.content()?.then(it => new TextDecoder().decode(it));
|
||||
const read_staged = walker => walker?.oid()
|
||||
?.then(oid => git.readBlob({ fs, dir, gitdir, oid, cache }))
|
||||
.then(it => new TextDecoder().decode(it.blob));
|
||||
|
||||
let diffs = [];
|
||||
|
||||
if (options['cached']) {
|
||||
if (commit_args.length > 1) {
|
||||
stderr('error: Too many <commit>s passed to `git diff --cached`. Up to 1 is allowed.');
|
||||
throw SHOW_USAGE;
|
||||
}
|
||||
// Show staged changes
|
||||
diffs = await diff_git_trees({
|
||||
...diff_ctx,
|
||||
a_tree: TREE({ ref: commit_args[0] ?? 'HEAD' }),
|
||||
b_tree: STAGE(),
|
||||
read_a: read_tree,
|
||||
read_b: read_staged,
|
||||
});
|
||||
} else if (commit_args.length === 0) {
|
||||
// Show unstaged changes
|
||||
diffs = await diff_git_trees({
|
||||
...diff_ctx,
|
||||
a_tree: STAGE(),
|
||||
b_tree: WORKDIR(),
|
||||
read_a: read_staged,
|
||||
read_b: read_tree,
|
||||
});
|
||||
} else if (commit_args.length === 1) {
|
||||
// Changes from commit to workdir
|
||||
diffs = await diff_git_trees({
|
||||
...diff_ctx,
|
||||
a_tree: TREE({ ref: commit_args[0] }),
|
||||
b_tree: WORKDIR(),
|
||||
read_a: read_tree,
|
||||
read_b: read_tree,
|
||||
});
|
||||
} else if (commit_args.length === 2) {
|
||||
// Changes from one commit to another
|
||||
diffs = await diff_git_trees({
|
||||
...diff_ctx,
|
||||
a_tree: TREE({ ref: commit_args[0] }),
|
||||
b_tree: TREE({ ref: commit_args[1] }),
|
||||
read_a: read_tree,
|
||||
read_b: read_tree,
|
||||
});
|
||||
} else {
|
||||
// TODO: Canonical git supports >2 <commit>s for merge commits.
|
||||
stderr('error: Too many <commit>s passed to `git diff`. Up to 2 are supported.');
|
||||
throw SHOW_USAGE;
|
||||
}
|
||||
|
||||
if (!diff_options.no_patch)
|
||||
stdout(format_diffs(diffs, diff_options));
|
||||
|
||||
if (options['exit-code'])
|
||||
return diffs.length > 0 ? 1 : 0;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user