Files
git/builtin/diff.c
Michael Montalbo 91c8989d4e diff: consult oid-only hunk providers via diff.<driver>.process
The provider chain so far holds the diff-hunks store in front of the
terminal builtin computation.  Open it to external processes: a pair on
a path whose driver configures diff.<driver>.process is answered by a
long-running process speaking a pkt-line protocol (following the filter
process protocol), registered at the head of the chain and consulted
before the store and before any blob is loaded.

The protocol starts with the smallest request that can carry an answer:
object names alone.  A request is the pathname and the pair's
old-oid/new-oid, with no content.  The process answers with hunk lines,
with a zero-hunk success that asserts the blobs equivalent (trailing
newlines included), or with status=need-content, on which the pair
falls through to the builtin answer.  This serves the two shapes that
need no content pushed to them: a cache keyed on the blob pair, and a
process that fetches the blobs itself (for example over "git cat-file
--batch").  A pair whose side is not a stored blob carries a NULL id;
the provider sends no request and passes it.  Because Git holds no
content for the exchange, the answer is used as sent: hunks are
validated for order, overlap, lockstep alignment, and magnitude, then
replayed without the normalization xdiff applies to diffs it computes
itself.  The magnitude bound is the blobs' sizes, read from the object
database without loading content: a blob of N bytes holds at most N
lines.

Because the process's answer is authoritative, it outranks the store,
and its head-of-chain position says so.  A pair the process answers
never reaches the store and is never recorded, so nothing it produces
enters the store, which holds the builtin answer only.  A request it
does not answer, whether need-content, a missing capability, or a
missing id, passes down the chain to the builtin answer, which is what
the store serves, so the store may serve such a pair and a warming run
may record it.  Entries recorded before a process was configured are
not purged; a pair the process answers ignores them, and "git diff-hunks
clear" discards them.

The provider gates itself per request.  The driver is looked up by the
old-side path, so a renamed file resolves to the same driver, and by
the repository-relative path, so a diff.relative run from a
subdirectory names the pair the same way.  Options the process is never
told about select no process: the whitespace-ignoring options, -I,
--anchored, and an algorithm forced by option or configuration (blame
routes its algorithm through xdl_opts, so --histogram is covered).  The
request gains its last field, the path; the consumers change only by
filling it, and neither names the process.

The provider's state is its repository's pool of running processes,
keyed by the configured command, so drivers sharing a command share a
process, a submodule speaks to its own, and releasing the provider
(from repo_clear()) stops them.  The pool owns a copy of each command
string, so an entry outlives a config re-read.  A command that fails
stays as an entry that is not retried: its request and every later one
pass, so the store may serve the path for the rest of the command.

A protocol error in a response never kills the command.  The response
is read through a packet reader gentle about framing, so an error takes
one path: a single warning, the process stopped and marked failed, and
the builtin diff for the rest of the command.  That covers garbage
bytes, a truncated response, an empty packet, a bare status, and an
unrecognized status.  Semantically invalid coordinates cost only their
pair: the response is drained, the pair is computed, and the process
stays alive.  A path the protocol cannot carry (an embedded newline, or
one too long for a packet) falls back per path rather than costing the
command its process.  The handshake keeps one fatal check: a process
that announces a capability Git did not request aborts the command, as
the long-running filter protocol does.

Consulting is allowed per command, following the allow_textconv
precedent.  "git diff", "git log" and "git show", and "git blame" set
allow_diff_process; the plumbing diff commands and the interactive-patch
machinery never set it, so scripted and staging output stays builtin.
The options adjust the flag:

- --no-ext-diff clears it and --ext-diff sets it;
- --diff-process and --no-diff-process set and clear it alone, leaving
  external diff drivers as they were;
- format-patch clears it unconditionally, so a generated patch applies
  for recipients without the process;
- range-diff passes --no-ext-diff to the "git log" it compares.

git blame and the summary formats consult the process.  For blame, a
pair reported equivalent emits no hunks, so the whole commit passes to
its parent.  In the stat formats such a pair sums to a zero-count entry,
which the "nothing changed" rule omits, as under -w.  The subprocess is
long-running: one startup cost across a traversal, one round-trip per
consulted pair.  Answers travel in struct xdl_hunk, new in
xdiff-interface.h, holding xdiff's 1-based coordinates; nothing feeds
them back to xdiff, since only coordinate consumers consult.

A content-carrying request is the natural extension: it would serve
sides that are not stored blobs and processes that want content pushed
to them, and bring patch output and log -L's range tracking to the same
answer.  As it stands, a process's answers show in blame and the summary
formats while patch output stays builtin.

t4080 exercises the protocol, the per-command gate, and the error paths:

- each adversarial response shape warns and falls back to builtin, the
  request log proving which failures disable the process and which keep
  it alive (a malformed hunk line, coordinates past the blob size, a
  count overflowing strtol(), overlapping or misaligned hunks, an
  unrecognized status, a bare status, an empty packet, a mid-response
  crash, and raw garbage);
- a capability-less process and status=abort degrade without noise, and
  a failed start warns once and returns the path to the store;
- a trailing token on a hunk line is ignored, pinning field
  appendability;
- positive consults for git diff, git show, and diff-tree under
  --ext-diff and --diff-process; textconv output and gitlink sides are
  never identified; a diff.relative run consults by the repo-relative
  path;
- the equivalence answer is pinned from both consumers, and a warming
  run past a deferring process records the pair for a later read.

Helped-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2026-08-01 15:09:52 -07:00

667 lines
18 KiB
C

/*
* Builtin "git diff"
*
* Copyright (c) 2006 Junio C Hamano
*/
#define USE_THE_REPOSITORY_VARIABLE
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "builtin.h"
#include "config.h"
#include "ewah/ewok.h"
#include "lockfile.h"
#include "color.h"
#include "commit.h"
#include "gettext.h"
#include "tag.h"
#include "diff.h"
#include "diff-merges.h"
#include "diffcore.h"
#include "preload-index.h"
#include "read-cache-ll.h"
#include "revision.h"
#include "log-tree.h"
#include "setup.h"
#include "oid-array.h"
#include "tree.h"
#define DIFF_NO_INDEX_EXPLICIT 1
#define DIFF_NO_INDEX_IMPLICIT 2
static const char builtin_diff_usage[] =
"git diff [<options>] [<commit>] [--] [<path>...]\n"
" or: git diff [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]\n"
" or: git diff [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]\n"
" or: git diff [<options>] <commit>...<commit> [--] [<path>...]\n"
" or: git diff [<options>] <blob> <blob>\n"
" or: git diff [<options>] --no-index [--] <path> <path> [<pathspec>...]"
"\n"
COMMON_DIFF_OPTIONS_HELP;
static const char *blob_path(struct object_array_entry *entry)
{
return entry->path ? entry->path : entry->name;
}
static void stuff_change(struct diff_options *opt,
unsigned old_mode, unsigned new_mode,
const struct object_id *old_oid,
const struct object_id *new_oid,
int old_oid_valid,
int new_oid_valid,
const char *old_path,
const char *new_path)
{
struct diff_filespec *one, *two;
if (!is_null_oid(old_oid) && !is_null_oid(new_oid) &&
oideq(old_oid, new_oid) && (old_mode == new_mode))
return;
if (opt->flags.reverse_diff) {
SWAP(old_mode, new_mode);
SWAP(old_oid, new_oid);
SWAP(old_path, new_path);
}
if (opt->prefix &&
(strncmp(old_path, opt->prefix, opt->prefix_length) ||
strncmp(new_path, opt->prefix, opt->prefix_length)))
return;
one = alloc_filespec(old_path);
two = alloc_filespec(new_path);
fill_filespec(one, old_oid, old_oid_valid, old_mode);
fill_filespec(two, new_oid, new_oid_valid, new_mode);
diff_queue(&diff_queued_diff, one, two);
}
static void builtin_diff_b_f(struct rev_info *revs,
int argc, const char **argv UNUSED,
struct object_array_entry **blob)
{
/* Blob vs file in the working tree*/
struct stat st;
const char *path;
if (argc > 1)
usage(builtin_diff_usage);
GUARD_PATHSPEC(&revs->prune_data, PATHSPEC_FROMTOP | PATHSPEC_LITERAL);
path = revs->prune_data.items[0].match;
if (lstat(path, &st))
die_errno(_("failed to stat '%s'"), path);
if (!(S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)))
die(_("'%s': not a regular file or symlink"), path);
diff_set_mnemonic_prefix(&revs->diffopt, "o/", "w/");
if (blob[0]->mode == S_IFINVALID)
blob[0]->mode = canon_mode(st.st_mode);
stuff_change(&revs->diffopt,
blob[0]->mode, canon_mode(st.st_mode),
&blob[0]->item->oid, null_oid(the_hash_algo),
1, 0,
blob[0]->path ? blob[0]->path : path,
path);
diffcore_std(&revs->diffopt);
diff_flush(&revs->diffopt);
}
static void builtin_diff_blobs(struct rev_info *revs,
int argc, const char **argv UNUSED,
struct object_array_entry **blob)
{
const unsigned mode = canon_mode(S_IFREG | 0644);
if (argc > 1)
usage(builtin_diff_usage);
if (blob[0]->mode == S_IFINVALID)
blob[0]->mode = mode;
if (blob[1]->mode == S_IFINVALID)
blob[1]->mode = mode;
stuff_change(&revs->diffopt,
blob[0]->mode, blob[1]->mode,
&blob[0]->item->oid, &blob[1]->item->oid,
1, 1,
blob_path(blob[0]), blob_path(blob[1]));
diffcore_std(&revs->diffopt);
diff_flush(&revs->diffopt);
}
static void builtin_diff_index(struct rev_info *revs,
int argc, const char **argv)
{
unsigned int option = 0;
while (1 < argc) {
const char *arg = argv[1];
if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged"))
option |= DIFF_INDEX_CACHED;
else if (!strcmp(arg, "--merge-base"))
option |= DIFF_INDEX_MERGE_BASE;
else
usage(builtin_diff_usage);
argv++; argc--;
}
/*
* Make sure there is one revision (i.e. pending object),
* and there is no revision filtering parameters.
*/
if (revs->pending.nr != 1 ||
revs->max_count != -1 || revs->min_age != -1 ||
revs->max_age != -1)
usage(builtin_diff_usage);
if (!(option & DIFF_INDEX_CACHED)) {
setup_work_tree(the_repository);
if (repo_read_index_preload(the_repository,
&revs->diffopt.pathspec, 0) < 0) {
die_errno("repo_read_index_preload");
}
} else if (repo_read_index(the_repository) < 0) {
die_errno("repo_read_cache");
}
run_diff_index(revs, option);
}
static void builtin_diff_tree(struct rev_info *revs,
int argc, const char **argv,
struct object_array_entry *ent0,
struct object_array_entry *ent1)
{
const struct object_id *(oid[2]);
struct object_id mb_oid;
int merge_base = 0;
while (1 < argc) {
const char *arg = argv[1];
if (!strcmp(arg, "--merge-base"))
merge_base = 1;
else
usage(builtin_diff_usage);
argv++; argc--;
}
if (merge_base) {
diff_get_merge_base(revs, &mb_oid);
oid[0] = &mb_oid;
oid[1] = &revs->pending.objects[1].item->oid;
} else {
int swap = 0;
/*
* We saw two trees, ent0 and ent1. If ent1 is uninteresting,
* swap them.
*/
if (ent1->item->flags & UNINTERESTING)
swap = 1;
oid[swap] = &ent0->item->oid;
oid[1 - swap] = &ent1->item->oid;
}
diff_tree_oid(oid[0], oid[1], "", &revs->diffopt);
log_tree_diff_flush(revs);
}
static void builtin_diff_combined(struct rev_info *revs,
int argc, const char **argv UNUSED,
struct object_array_entry *ent,
int ents, int first_non_parent)
{
struct oid_array parents = OID_ARRAY_INIT;
int i;
if (argc > 1)
usage(builtin_diff_usage);
if (first_non_parent < 0)
die(_("no merge given, only parents."));
if (first_non_parent >= ents)
BUG("first_non_parent out of range: %d", first_non_parent);
diff_merges_set_dense_combined_if_unset(revs);
for (i = 0; i < ents; i++) {
if (i != first_non_parent)
oid_array_append(&parents, &ent[i].item->oid);
}
diff_tree_combined(&ent[first_non_parent].item->oid, &parents, revs);
oid_array_clear(&parents);
}
static void refresh_index_quietly(void)
{
struct lock_file lock_file = LOCK_INIT;
int fd;
fd = repo_hold_locked_index(the_repository, &lock_file, 0);
if (fd < 0)
return;
discard_index(the_repository->index);
repo_read_index(the_repository);
refresh_index(the_repository->index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL,
NULL);
repo_update_index_if_able(the_repository, &lock_file);
}
static void builtin_diff_files(struct rev_info *revs, int argc, const char **argv)
{
unsigned int options = 0;
while (1 < argc && argv[1][0] == '-') {
if (!strcmp(argv[1], "--base"))
revs->max_count = 1;
else if (!strcmp(argv[1], "--ours"))
revs->max_count = 2;
else if (!strcmp(argv[1], "--theirs"))
revs->max_count = 3;
else if (!strcmp(argv[1], "-q"))
options |= DIFF_SILENT_ON_REMOVED;
else if (!strcmp(argv[1], "-h"))
usage(builtin_diff_usage);
else {
error(_("invalid option: %s"), argv[1]);
usage(builtin_diff_usage);
}
argv++; argc--;
}
/*
* "diff --base" should not combine merges because it was not
* asked to. "diff -c" should not densify (if the user wants
* dense one, --cc can be explicitly asked for, or just rely
* on the default).
*/
if (revs->max_count == -1 &&
(revs->diffopt.output_format & DIFF_FORMAT_PATCH))
diff_merges_set_dense_combined_if_unset(revs);
setup_work_tree(the_repository);
if (repo_read_index_preload(the_repository, &revs->diffopt.pathspec,
0) < 0) {
die_errno("repo_read_index_preload");
}
run_diff_files(revs, options);
}
struct symdiff {
struct bitmap *skip;
int warn;
const char *base, *left, *right;
};
/*
* Check for symmetric-difference arguments, and if present, arrange
* everything we need to know to handle them correctly. As a bonus,
* weed out all bogus range-based revision specifications, e.g.,
* "git diff A..B C..D" or "git diff A..B C" get rejected.
*
* For an actual symmetric diff, *symdiff is set this way:
*
* - its skip is non-NULL and marks *all* rev->pending.objects[i]
* indices that the caller should ignore (extra merge bases, of
* which there might be many, and A in A...B). Note that the
* chosen merge base and right side are NOT marked.
* - warn is set if there are multiple merge bases.
* - base, left, and right point to the names to use in a
* warning about multiple merge bases.
*
* If there is no symmetric diff argument, sym->skip is NULL and
* sym->warn is cleared. The remaining fields are not set.
*/
static void symdiff_prepare(struct rev_info *rev, struct symdiff *sym)
{
int i, is_symdiff = 0, basecount = 0, othercount = 0;
int lpos = -1, rpos = -1, basepos = -1;
struct bitmap *map = NULL;
/*
* Use the whence fields to find merge bases and left and
* right parts of symmetric difference, so that we do not
* depend on the order that revisions are parsed. If there
* are any revs that aren't from these sources, we have a
* "git diff C A...B" or "git diff A...B C" case. Or we
* could even get "git diff A...B C...E", for instance.
*
* If we don't have just one merge base, we pick one
* at random.
*
* NB: REV_CMD_LEFT, REV_CMD_RIGHT are also used for A..B,
* so we must check for SYMMETRIC_LEFT too. The two arrays
* rev->pending.objects and rev->cmdline.rev are parallel.
*/
for (i = 0; i < rev->cmdline.nr; i++) {
struct object *obj = rev->pending.objects[i].item;
switch (rev->cmdline.rev[i].whence) {
case REV_CMD_MERGE_BASE:
if (basepos < 0)
basepos = i;
basecount++;
break; /* do mark all bases */
case REV_CMD_LEFT:
if (lpos >= 0)
usage(builtin_diff_usage);
lpos = i;
if (obj->flags & SYMMETRIC_LEFT) {
is_symdiff = 1;
break; /* do mark A */
}
continue;
case REV_CMD_RIGHT:
if (rpos >= 0)
usage(builtin_diff_usage);
rpos = i;
continue; /* don't mark B */
case REV_CMD_PARENTS_ONLY:
case REV_CMD_REF:
case REV_CMD_REV:
othercount++;
continue;
}
if (!map)
map = bitmap_new();
bitmap_set(map, i);
}
/*
* Forbid any additional revs for both A...B and A..B.
*/
if (lpos >= 0 && othercount > 0)
usage(builtin_diff_usage);
if (!is_symdiff) {
bitmap_free(map);
sym->warn = 0;
sym->skip = NULL;
return;
}
sym->left = rev->pending.objects[lpos].name;
sym->right = rev->pending.objects[rpos].name;
if (basecount == 0)
die(_("%s...%s: no merge base"), sym->left, sym->right);
sym->base = rev->pending.objects[basepos].name;
bitmap_unset(map, basepos); /* unmark the base we want */
sym->warn = basecount > 1;
sym->skip = map;
}
static void symdiff_release(struct symdiff *sdiff)
{
bitmap_free(sdiff->skip);
}
int cmd_diff(int argc,
const char **argv,
const char *prefix,
struct repository *repo UNUSED)
{
int i;
struct rev_info rev;
struct object_array ent = OBJECT_ARRAY_INIT;
int first_non_parent = -1;
int blobs = 0, paths = 0;
struct object_array_entry *blob[2];
int nongit = 0, no_index = 0;
int result;
struct symdiff sdiff;
/*
* We could get N tree-ish in the rev.pending_objects list.
* Also there could be M blobs there, and P pathspecs. --cached may
* also be present.
*
* N=0, M=0:
* cache vs files (diff-files)
*
* N=0, M=0, --cached:
* HEAD vs cache (diff-index --cached)
*
* N=0, M=2:
* compare two random blobs. P must be zero.
*
* N=0, M=1, P=1:
* compare a blob with a working tree file.
*
* N=1, M=0:
* tree vs files (diff-index)
*
* N=1, M=0, --cached:
* tree vs cache (diff-index --cached)
*
* N=2, M=0:
* tree vs tree (diff-tree)
*
* N=0, M=0, P=2:
* compare two filesystem entities (aka --no-index).
*
* Other cases are errors.
*/
/* Were we asked to do --no-index explicitly? */
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--")) {
i++;
break;
}
if (!strcmp(argv[i], "--no-index"))
no_index = DIFF_NO_INDEX_EXPLICIT;
if (argv[i][0] != '-')
break;
}
prefix = setup_git_directory_gently(the_repository, &nongit);
if (!nongit) {
prepare_repo_settings(the_repository);
the_repository->settings.command_requires_full_index = 0;
}
if (!no_index) {
/*
* Treat git diff with at least one path outside of the
* repo the same as if the command would have been executed
* outside of a git repository. In this case it behaves
* the same way as "git diff --no-index <a> <b>", which acts
* as a colourful "diff" replacement.
*/
if (nongit || ((argc == i + 2) &&
(!path_inside_repo(the_repository, prefix, argv[i]) ||
!path_inside_repo(the_repository, prefix, argv[i + 1]))))
no_index = DIFF_NO_INDEX_IMPLICIT;
}
/*
* When operating outside of a Git repository we need to have a hash
* algorithm at hand so that we can generate the blob hashes. We
* default to SHA1 here, but may eventually want to change this to be
* configurable via a command line option.
*/
if (nongit)
repo_set_hash_algo(the_repository, GIT_HASH_DEFAULT);
init_diff_ui_defaults();
repo_config(the_repository, git_diff_ui_config, NULL);
/*
* If we are ignoring the fact that our current directory may
* be part of a working tree controlled by a Git repository to
* pretend to be a "better GNU diff", we should undo the
* effect of the setup code that did a chdir() to the top of
* the working tree. Where we came from is recorded in the
* prefix.
*/
if (no_index && prefix) {
if (chdir(prefix))
die(_("cannot come back to cwd"));
prefix = NULL;
}
prefix = precompose_argv_prefix(argc, argv, prefix);
repo_init_revisions(the_repository, &rev, prefix);
/* Set up defaults that will apply to both no-index and regular diffs. */
init_diffstat_widths(&rev.diffopt);
rev.diffopt.flags.allow_external = 1;
rev.diffopt.flags.allow_textconv = 1;
rev.diffopt.flags.allow_diff_process = 1;
/* If this is a no-index diff, just run it and exit there. */
if (no_index)
exit(diff_no_index(&rev, the_repository->hash_algo,
no_index == DIFF_NO_INDEX_IMPLICIT,
argc, argv));
/*
* Otherwise, we are doing the usual "git" diff; set up any
* further defaults that apply to regular diffs.
*/
rev.diffopt.skip_stat_unmatch = !!diff_auto_refresh_index;
/*
* Default to intent-to-add entries invisible in the
* index. This makes them show up as new files in diff-files
* and not at all in diff-cached.
*/
rev.diffopt.ita_invisible_in_index = 1;
if (nongit)
die(_("Not a git repository"));
argc = setup_revisions(argc, argv, &rev, NULL);
if (!rev.diffopt.output_format) {
rev.diffopt.output_format = DIFF_FORMAT_PATCH;
diff_setup_done(&rev.diffopt);
}
rev.diffopt.flags.recursive = 1;
rev.diffopt.rotate_to_strict = 1;
setup_diff_pager(&rev.diffopt);
/*
* Do we have --cached and not have a pending object, then
* default to HEAD by hand. Eek.
*/
if (!rev.pending.nr) {
int i;
for (i = 1; i < argc; i++) {
const char *arg = argv[i];
if (!strcmp(arg, "--"))
break;
else if (!strcmp(arg, "--cached") ||
!strcmp(arg, "--staged")) {
add_head_to_pending(&rev);
if (!rev.pending.nr) {
struct tree *tree;
tree = lookup_tree(the_repository,
the_repository->hash_algo->empty_tree);
add_pending_object(&rev, &tree->object, "HEAD");
}
break;
}
}
}
/*
* The hunk store is keyed by blob pair, so any diff whose
* file pairs carry known blob object IDs (tree-to-tree,
* index-to-tree) can consult the same entries that
* "git log --stat" and "git blame" use; pairs without known
* blobs bypass it at lookup time.
*/
diff_hunks_attach(&rev.diffopt);
symdiff_prepare(&rev, &sdiff);
for (i = 0; i < rev.pending.nr; i++) {
struct object_array_entry *entry = &rev.pending.objects[i];
struct object *obj = entry->item;
const char *name = entry->name;
int flags = (obj->flags & UNINTERESTING);
if (!obj->parsed)
obj = parse_object(the_repository, &obj->oid);
obj = deref_tag(the_repository, obj, NULL, 0);
if (!obj)
die(_("invalid object '%s' given."), name);
if (obj->type == OBJ_COMMIT) {
struct tree *tree = repo_get_commit_tree(
the_repository, (struct commit *)obj);
if (!tree)
die(_("unable to read tree object for commit '%s'"), name);
obj = &tree->object;
}
if (obj->type == OBJ_TREE) {
if (sdiff.skip && bitmap_get(sdiff.skip, i))
continue;
obj->flags |= flags;
add_object_array(obj, name, &ent);
if (first_non_parent < 0 &&
(i >= rev.cmdline.nr || /* HEAD by hand. */
rev.cmdline.rev[i].whence != REV_CMD_PARENTS_ONLY))
first_non_parent = ent.nr - 1;
} else if (obj->type == OBJ_BLOB) {
if (2 <= blobs)
die(_("more than two blobs given: '%s'"), name);
blob[blobs] = entry;
blobs++;
} else {
die(_("unhandled object '%s' given."), name);
}
}
if (rev.prune_data.nr)
paths += rev.prune_data.nr;
/*
* Now, do the arguments look reasonable?
*/
if (!ent.nr) {
switch (blobs) {
case 0:
builtin_diff_files(&rev, argc, argv);
break;
case 1:
if (paths != 1)
usage(builtin_diff_usage);
builtin_diff_b_f(&rev, argc, argv, blob);
break;
case 2:
if (paths)
usage(builtin_diff_usage);
builtin_diff_blobs(&rev, argc, argv, blob);
break;
default:
usage(builtin_diff_usage);
}
}
else if (blobs)
usage(builtin_diff_usage);
else if (ent.nr == 1)
builtin_diff_index(&rev, argc, argv);
else if (ent.nr == 2) {
if (sdiff.warn)
warning(_("%s...%s: multiple merge bases, using %s"),
sdiff.left, sdiff.right, sdiff.base);
builtin_diff_tree(&rev, argc, argv,
&ent.objects[0], &ent.objects[1]);
} else
builtin_diff_combined(&rev, argc, argv,
ent.objects, ent.nr,
first_non_parent);
result = diff_result_code(&rev);
if (1 < rev.diffopt.skip_stat_unmatch)
refresh_index_quietly();
diff_hunks_detach(&rev.diffopt);
release_revisions(&rev);
object_array_clear(&ent);
symdiff_release(&sdiff);
return result;
}