mirror of
https://github.com/git/git.git
synced 2024-11-08 18:23:01 +01:00
e54501004a
The diff code represents paths using the diff_filespec struct. This struct has a sha1 to represent the sha1 of the content at that path, as well as a sha1_valid member which indicates whether its sha1 field is actually useful. If sha1_valid is not true, then the filespec represents a working tree file (e.g., for the no-index case, or for when the index is not up-to-date). The diff_filespec is only used internally, though. At the interfaces to the diff subsystem, callers feed the sha1 directly, and we create a diff_filespec from it. It's at that point that we look at the sha1 and decide whether it is valid or not; callers may pass the null sha1 as a sentinel value to indicate that it is not. We should not typically see the null sha1 coming from any other source (e.g., in the index itself, or from a tree). However, a corrupt tree might have a null sha1, which would cause "diff --patch" to accidentally diff the working tree version of a file instead of treating it as a blob. This patch extends the edges of the diff interface to accept a "sha1_valid" flag whenever we accept a sha1, and to use that flag when creating a filespec. In some cases, this means passing the flag through several layers, making the code change larger than would be desirable. One alternative would be to simply die() upon seeing corrupted trees with null sha1s. However, this fix more directly addresses the problem (while bogus sha1s in a tree are probably a bad thing, it is really the sentinel confusion sending us down the wrong code path that is what makes it devastating). And it means that git is more capable of examining and debugging these corrupted trees. For example, you can still "diff --raw" such a tree to find out when the bogus entry was introduced; you just cannot do a "--patch" diff (just as you could not with any other corrupted tree, as we do not have any content to diff). Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
423 lines
11 KiB
C
423 lines
11 KiB
C
/*
|
|
* Builtin "git diff"
|
|
*
|
|
* Copyright (c) 2006 Junio C Hamano
|
|
*/
|
|
#include "cache.h"
|
|
#include "color.h"
|
|
#include "commit.h"
|
|
#include "blob.h"
|
|
#include "tag.h"
|
|
#include "diff.h"
|
|
#include "diffcore.h"
|
|
#include "revision.h"
|
|
#include "log-tree.h"
|
|
#include "builtin.h"
|
|
#include "submodule.h"
|
|
#include "sha1-array.h"
|
|
|
|
struct blobinfo {
|
|
unsigned char sha1[20];
|
|
const char *name;
|
|
unsigned mode;
|
|
};
|
|
|
|
static const char builtin_diff_usage[] =
|
|
"git diff [<options>] [<commit> [<commit>]] [--] [<path>...]";
|
|
|
|
static void stuff_change(struct diff_options *opt,
|
|
unsigned old_mode, unsigned new_mode,
|
|
const unsigned char *old_sha1,
|
|
const unsigned char *new_sha1,
|
|
int old_sha1_valid,
|
|
int new_sha1_valid,
|
|
const char *old_name,
|
|
const char *new_name)
|
|
{
|
|
struct diff_filespec *one, *two;
|
|
|
|
if (!is_null_sha1(old_sha1) && !is_null_sha1(new_sha1) &&
|
|
!hashcmp(old_sha1, new_sha1) && (old_mode == new_mode))
|
|
return;
|
|
|
|
if (DIFF_OPT_TST(opt, REVERSE_DIFF)) {
|
|
unsigned tmp;
|
|
const unsigned char *tmp_u;
|
|
const char *tmp_c;
|
|
tmp = old_mode; old_mode = new_mode; new_mode = tmp;
|
|
tmp_u = old_sha1; old_sha1 = new_sha1; new_sha1 = tmp_u;
|
|
tmp_c = old_name; old_name = new_name; new_name = tmp_c;
|
|
}
|
|
|
|
if (opt->prefix &&
|
|
(strncmp(old_name, opt->prefix, opt->prefix_length) ||
|
|
strncmp(new_name, opt->prefix, opt->prefix_length)))
|
|
return;
|
|
|
|
one = alloc_filespec(old_name);
|
|
two = alloc_filespec(new_name);
|
|
fill_filespec(one, old_sha1, old_sha1_valid, old_mode);
|
|
fill_filespec(two, new_sha1, new_sha1_valid, new_mode);
|
|
|
|
diff_queue(&diff_queued_diff, one, two);
|
|
}
|
|
|
|
static int builtin_diff_b_f(struct rev_info *revs,
|
|
int argc, const char **argv,
|
|
struct blobinfo *blob,
|
|
const char *path)
|
|
{
|
|
/* Blob vs file in the working tree*/
|
|
struct stat st;
|
|
|
|
if (argc > 1)
|
|
usage(builtin_diff_usage);
|
|
|
|
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].sha1, null_sha1,
|
|
1, 0,
|
|
path, path);
|
|
diffcore_std(&revs->diffopt);
|
|
diff_flush(&revs->diffopt);
|
|
return 0;
|
|
}
|
|
|
|
static int builtin_diff_blobs(struct rev_info *revs,
|
|
int argc, const char **argv,
|
|
struct blobinfo *blob)
|
|
{
|
|
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].sha1, blob[1].sha1,
|
|
1, 1,
|
|
blob[0].name, blob[1].name);
|
|
diffcore_std(&revs->diffopt);
|
|
diff_flush(&revs->diffopt);
|
|
return 0;
|
|
}
|
|
|
|
static int builtin_diff_index(struct rev_info *revs,
|
|
int argc, const char **argv)
|
|
{
|
|
int cached = 0;
|
|
while (1 < argc) {
|
|
const char *arg = argv[1];
|
|
if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged"))
|
|
cached = 1;
|
|
else
|
|
usage(builtin_diff_usage);
|
|
argv++; argc--;
|
|
}
|
|
if (!cached)
|
|
setup_work_tree();
|
|
/*
|
|
* 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 (read_cache_preload(revs->diffopt.pathspec.raw) < 0) {
|
|
perror("read_cache_preload");
|
|
return -1;
|
|
}
|
|
return run_diff_index(revs, cached);
|
|
}
|
|
|
|
static int builtin_diff_tree(struct rev_info *revs,
|
|
int argc, const char **argv,
|
|
struct object_array_entry *ent)
|
|
{
|
|
const unsigned char *(sha1[2]);
|
|
int swap = 0;
|
|
|
|
if (argc > 1)
|
|
usage(builtin_diff_usage);
|
|
|
|
/* We saw two trees, ent[0] and ent[1].
|
|
* if ent[1] is uninteresting, they are swapped
|
|
*/
|
|
if (ent[1].item->flags & UNINTERESTING)
|
|
swap = 1;
|
|
sha1[swap] = ent[0].item->sha1;
|
|
sha1[1-swap] = ent[1].item->sha1;
|
|
diff_tree_sha1(sha1[0], sha1[1], "", &revs->diffopt);
|
|
log_tree_diff_flush(revs);
|
|
return 0;
|
|
}
|
|
|
|
static int builtin_diff_combined(struct rev_info *revs,
|
|
int argc, const char **argv,
|
|
struct object_array_entry *ent,
|
|
int ents)
|
|
{
|
|
struct sha1_array parents = SHA1_ARRAY_INIT;
|
|
int i;
|
|
|
|
if (argc > 1)
|
|
usage(builtin_diff_usage);
|
|
|
|
if (!revs->dense_combined_merges && !revs->combine_merges)
|
|
revs->dense_combined_merges = revs->combine_merges = 1;
|
|
for (i = 1; i < ents; i++)
|
|
sha1_array_append(&parents, ent[i].item->sha1);
|
|
diff_tree_combined(ent[0].item->sha1, &parents,
|
|
revs->dense_combined_merges, revs);
|
|
sha1_array_clear(&parents);
|
|
return 0;
|
|
}
|
|
|
|
static void refresh_index_quietly(void)
|
|
{
|
|
struct lock_file *lock_file;
|
|
int fd;
|
|
|
|
lock_file = xcalloc(1, sizeof(struct lock_file));
|
|
fd = hold_locked_index(lock_file, 0);
|
|
if (fd < 0)
|
|
return;
|
|
discard_cache();
|
|
read_cache();
|
|
refresh_cache(REFRESH_QUIET|REFRESH_UNMERGED);
|
|
update_index_if_able(&the_index, lock_file);
|
|
}
|
|
|
|
static int 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
|
|
return error(_("invalid option: %s"), argv[1]);
|
|
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->combine_merges &&
|
|
(revs->diffopt.output_format & DIFF_FORMAT_PATCH))
|
|
revs->combine_merges = revs->dense_combined_merges = 1;
|
|
|
|
setup_work_tree();
|
|
if (read_cache_preload(revs->diffopt.pathspec.raw) < 0) {
|
|
perror("read_cache_preload");
|
|
return -1;
|
|
}
|
|
return run_diff_files(revs, options);
|
|
}
|
|
|
|
int cmd_diff(int argc, const char **argv, const char *prefix)
|
|
{
|
|
int i;
|
|
struct rev_info rev;
|
|
struct object_array_entry ent[100];
|
|
int ents = 0, blobs = 0, paths = 0;
|
|
const char *path = NULL;
|
|
struct blobinfo blob[2];
|
|
int nongit;
|
|
int result = 0;
|
|
|
|
/*
|
|
* We could get N tree-ish in the rev.pending_objects list.
|
|
* Also there could be M blobs there, and P pathspecs.
|
|
*
|
|
* N=0, M=0:
|
|
* cache vs files (diff-files)
|
|
* 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 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.
|
|
*/
|
|
|
|
prefix = setup_git_directory_gently(&nongit);
|
|
gitmodules_config();
|
|
git_config(git_diff_ui_config, NULL);
|
|
|
|
init_revisions(&rev, prefix);
|
|
|
|
/* If this is a no-index diff, just run it and exit there. */
|
|
diff_no_index(&rev, argc, argv, nongit, prefix);
|
|
|
|
/* Otherwise, we are doing the usual "git" diff */
|
|
rev.diffopt.skip_stat_unmatch = !!diff_auto_refresh_index;
|
|
|
|
/* Default to let external and textconv be used */
|
|
DIFF_OPT_SET(&rev.diffopt, ALLOW_EXTERNAL);
|
|
DIFF_OPT_SET(&rev.diffopt, ALLOW_TEXTCONV);
|
|
|
|
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;
|
|
if (diff_setup_done(&rev.diffopt) < 0)
|
|
die(_("diff_setup_done failed"));
|
|
}
|
|
|
|
DIFF_OPT_SET(&rev.diffopt, RECURSIVE);
|
|
|
|
/*
|
|
* If the user asked for our exit code then don't start a
|
|
* pager or we would end up reporting its exit code instead.
|
|
*/
|
|
if (!DIFF_OPT_TST(&rev.diffopt, EXIT_WITH_STATUS) &&
|
|
check_pager_config("diff") != 0)
|
|
setup_pager();
|
|
|
|
/*
|
|
* 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((const unsigned char*)EMPTY_TREE_SHA1_BIN);
|
|
add_pending_object(&rev, &tree->object, "HEAD");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < rev.pending.nr; i++) {
|
|
struct object_array_entry *list = rev.pending.objects+i;
|
|
struct object *obj = list->item;
|
|
const char *name = list->name;
|
|
int flags = (obj->flags & UNINTERESTING);
|
|
if (!obj->parsed)
|
|
obj = parse_object(obj->sha1);
|
|
obj = deref_tag(obj, NULL, 0);
|
|
if (!obj)
|
|
die(_("invalid object '%s' given."), name);
|
|
if (obj->type == OBJ_COMMIT)
|
|
obj = &((struct commit *)obj)->tree->object;
|
|
if (obj->type == OBJ_TREE) {
|
|
if (ARRAY_SIZE(ent) <= ents)
|
|
die(_("more than %d trees given: '%s'"),
|
|
(int) ARRAY_SIZE(ent), name);
|
|
obj->flags |= flags;
|
|
ent[ents].item = obj;
|
|
ent[ents].name = name;
|
|
ents++;
|
|
continue;
|
|
}
|
|
if (obj->type == OBJ_BLOB) {
|
|
if (2 <= blobs)
|
|
die(_("more than two blobs given: '%s'"), name);
|
|
hashcpy(blob[blobs].sha1, obj->sha1);
|
|
blob[blobs].name = name;
|
|
blob[blobs].mode = list->mode;
|
|
blobs++;
|
|
continue;
|
|
|
|
}
|
|
die(_("unhandled object '%s' given."), name);
|
|
}
|
|
if (rev.prune_data.nr) {
|
|
if (!path)
|
|
path = rev.prune_data.items[0].match;
|
|
paths += rev.prune_data.nr;
|
|
}
|
|
|
|
/*
|
|
* Now, do the arguments look reasonable?
|
|
*/
|
|
if (!ents) {
|
|
switch (blobs) {
|
|
case 0:
|
|
result = builtin_diff_files(&rev, argc, argv);
|
|
break;
|
|
case 1:
|
|
if (paths != 1)
|
|
usage(builtin_diff_usage);
|
|
result = builtin_diff_b_f(&rev, argc, argv, blob, path);
|
|
break;
|
|
case 2:
|
|
if (paths)
|
|
usage(builtin_diff_usage);
|
|
result = builtin_diff_blobs(&rev, argc, argv, blob);
|
|
break;
|
|
default:
|
|
usage(builtin_diff_usage);
|
|
}
|
|
}
|
|
else if (blobs)
|
|
usage(builtin_diff_usage);
|
|
else if (ents == 1)
|
|
result = builtin_diff_index(&rev, argc, argv);
|
|
else if (ents == 2)
|
|
result = builtin_diff_tree(&rev, argc, argv, ent);
|
|
else if (ent[0].item->flags & UNINTERESTING) {
|
|
/*
|
|
* diff A...B where there is at least one merge base
|
|
* between A and B. We have ent[0] == merge-base,
|
|
* ent[ents-2] == A, and ent[ents-1] == B. Show diff
|
|
* between the base and B. Note that we pick one
|
|
* merge base at random if there are more than one.
|
|
*/
|
|
ent[1] = ent[ents-1];
|
|
result = builtin_diff_tree(&rev, argc, argv, ent);
|
|
} else
|
|
result = builtin_diff_combined(&rev, argc, argv,
|
|
ent, ents);
|
|
result = diff_result_code(&rev.diffopt, result);
|
|
if (1 < rev.diffopt.skip_stat_unmatch)
|
|
refresh_index_quietly();
|
|
return result;
|
|
}
|