diff --git a/Documentation/git.adoc b/Documentation/git.adoc index 8a5cdd3b3d22c5..5775d5c06a728d 100644 --- a/Documentation/git.adoc +++ b/Documentation/git.adoc @@ -669,6 +669,10 @@ parameter, . + For each path `GIT_EXTERNAL_DIFF` is called, two environment variables, `GIT_DIFF_PATH_COUNTER` and `GIT_DIFF_PATH_TOTAL` are set. ++ +When Git diff is called to compare two commits, four additional +environment variables, `GIT_DIFF_ENDPOINT_A`, `GIT_DIFF_ENDPOINT_B`, +`GIT_DIFF_PATH_A` and `GIT_DIFF_PATH_B` are set. `GIT_EXTERNAL_DIFF_TRUST_EXIT_CODE`:: If this Boolean environment variable is set to true then the @@ -679,13 +683,24 @@ For each path `GIT_EXTERNAL_DIFF` is called, two environment variables, is expected to return exit code 0 regardless of equality. Any other exit code causes Git to report a fatal error. - `GIT_DIFF_PATH_COUNTER`:: A 1-based counter incremented by one for every path. `GIT_DIFF_PATH_TOTAL`:: The total number of paths. +`GIT_DIFF_ENDPOINT_A`:: + The full SHA-1 object name of the source commit. + +`GIT_DIFF_ENDPOINT_B`:: + The full SHA-1 object name of the destination commit. + +`GIT_DIFF_PATH_A`:: + The repository path in the source endpoint, `/dev/null` if added. + +`GIT_DIFF_PATH_B`:: + The repository path in the destination endpoint, `/dev/null` if deleted. + other ~~~~~ `GIT_MERGE_VERBOSITY`:: diff --git a/builtin/diff.c b/builtin/diff.c index 0b23c41456837f..b0189b48c57714 100644 --- a/builtin/diff.c +++ b/builtin/diff.c @@ -25,6 +25,7 @@ #include "setup.h" #include "oid-array.h" #include "tree.h" +#include "hex.h" #define DIFF_NO_INDEX_EXPLICIT 1 #define DIFF_NO_INDEX_IMPLICIT 2 @@ -170,14 +171,27 @@ static void builtin_diff_index(struct rev_info *revs, run_diff_index(revs, option); } +struct symdiff { + struct bitmap *skip; + int warn; + const char *base, *left, *right; + struct object_array_entry *end0, *end1; +}; + static void builtin_diff_tree(struct rev_info *revs, int argc, const char **argv, struct object_array_entry *ent0, - struct object_array_entry *ent1) + struct object_array_entry *ent1, + struct symdiff *sdiff) { const struct object_id *(oid[2]); struct object_id mb_oid; int merge_base = 0; + const struct object_id *(endpoint[2]); + int reverse = 0; + + if (revs->diffopt.flags.reverse_diff) + reverse = 1; while (1 < argc) { const char *arg = argv[1]; @@ -192,6 +206,8 @@ static void builtin_diff_tree(struct rev_info *revs, diff_get_merge_base(revs, &mb_oid); oid[0] = &mb_oid; oid[1] = &revs->pending.objects[1].item->oid; + endpoint[0] = oid[0]; + endpoint[1] = oid[1]; } else { int swap = 0; @@ -203,7 +219,26 @@ static void builtin_diff_tree(struct rev_info *revs, swap = 1; oid[swap] = &ent0->item->oid; oid[1 - swap] = &ent1->item->oid; + + /* + * If this is a symmetric diff, obtain the endpoints from previous + * argument filtering. Otherwise there are only two revs, which are + * the endpoints. + */ + if (sdiff->skip) { + endpoint[swap] = &sdiff->end0->item->oid; + endpoint[1 - swap] = &sdiff->end1->item->oid; + } else { + if (revs->pending.nr != 2) + BUG("unexpected revs->pending.nr: %d", revs->pending.nr); + endpoint[swap] = &revs->pending.objects[0].item->oid; + endpoint[1 - swap] = &revs->pending.objects[1].item->oid; + } } + + revs->diffopt.endpoint.oid[0] = endpoint[reverse]; + revs->diffopt.endpoint.oid[1] = endpoint[1 - reverse]; + diff_tree_oid(oid[0], oid[1], "", &revs->diffopt); log_tree_diff_flush(revs); } @@ -289,12 +324,6 @@ static void builtin_diff_files(struct rev_info *revs, int argc, const char **arg 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, @@ -389,6 +418,8 @@ static void symdiff_prepare(struct rev_info *rev, struct symdiff *sym) bitmap_unset(map, basepos); /* unmark the base we want */ sym->warn = basecount > 1; sym->skip = map; + sym->end0 = &rev->pending.objects[basepos]; + sym->end1 = &rev->pending.objects[rpos]; } static void symdiff_release(struct symdiff *sdiff) @@ -636,7 +667,7 @@ int cmd_diff(int argc, 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]); + &ent.objects[0], &ent.objects[1], &sdiff); } else builtin_diff_combined(&rev, argc, argv, ent.objects, ent.nr, diff --git a/diff.c b/diff.c index 397e38b41cc6fa..a2fed288aa0a93 100644 --- a/diff.c +++ b/diff.c @@ -46,6 +46,7 @@ #include "setup.h" #include "strmap.h" #include "ws.h" +#include "hash.h" #ifdef NO_FAST_WORKING_DIRECTORY #define FAST_WORKING_DIRECTORY 0 @@ -4758,6 +4759,8 @@ static void run_external_diff(const struct external_diff *pgm, struct child_process cmd = CHILD_PROCESS_INIT; struct diff_queue_struct *q = &diff_queued_diff; int rc; + const char *path_one; + const char *path_two; /* * Trivial equality is handled by diff_unmodified_pair() before @@ -4787,6 +4790,19 @@ static void run_external_diff(const struct external_diff *pgm, ++o->diff_path_counter); strvec_pushf(&cmd.env, "GIT_DIFF_PATH_TOTAL=%d", q->nr); + if (o->endpoint.oid[0] && o->endpoint.oid[1]) { + strvec_pushf(&cmd.env, "GIT_DIFF_ENDPOINT_A=%s", + oid_to_hex(o->endpoint.oid[0])); + strvec_pushf(&cmd.env, "GIT_DIFF_ENDPOINT_B=%s", + oid_to_hex(o->endpoint.oid[1])); + + path_one = DIFF_FILE_VALID(one) ? name : "/dev/null"; + path_two = DIFF_FILE_VALID(two) ? (other ? other : name) : "/dev/null"; + + strvec_pushf(&cmd.env, "GIT_DIFF_PATH_A=%s", path_one); + strvec_pushf(&cmd.env, "GIT_DIFF_PATH_B=%s", path_two); + } + diff_free_filespec_data(one); diff_free_filespec_data(two); cmd.use_shell = 1; diff --git a/diff.h b/diff.h index 7eb84aadf40c41..bf35df7adf6ac8 100644 --- a/diff.h +++ b/diff.h @@ -420,6 +420,11 @@ struct diff_options { */ int max_depth; int max_depth_valid; + + /* The end-points of the diff */ + struct { + const struct object_id *oid[2]; + } endpoint; }; unsigned diff_filter_bit(char status); diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh index 7ec5854f74d651..253b1ce010d259 100755 --- a/t/t4020-diff-external.sh +++ b/t/t4020-diff-external.sh @@ -2,21 +2,18 @@ test_description='external diff interface test' +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + . ./test-lib.sh test_expect_success setup ' - test_tick && - echo initial >file && - git add file && - git commit -m initial && + test_commit initial file initial A && - test_tick && - echo second >file && + test_commit second file second B && before=$(git hash-object file) && before=$(git rev-parse --short $before) && - git add file && - git commit -m second && test_tick && echo third >file @@ -260,9 +257,7 @@ test_expect_success 'force diff with "diff"' ' ' test_expect_success 'GIT_EXTERNAL_DIFF with more than one changed files' ' - echo anotherfile > file2 && - git add file2 && - git commit -m "added 2nd file" && + test_commit "added 2nd file" file2 anotherfile C && echo modified >file2 && GIT_EXTERNAL_DIFF=echo git diff ' @@ -346,4 +341,95 @@ test_expect_success 'submodule diff' ' test_cmp expected actual ' +test_expect_success 'setup script for export endpoints' ' + write_script ext-diff-endpoints.sh <<-\EOF + printf "END_A=$GIT_DIFF_ENDPOINT_A " >>revs_and_paths.txt && + printf "END_B=$GIT_DIFF_ENDPOINT_B " >>revs_and_paths.txt && + printf "PATH_A=$GIT_DIFF_PATH_A " >>revs_and_paths.txt && + printf "PATH_B=$GIT_DIFF_PATH_B\n" >>revs_and_paths.txt + EOF +' + +test_expect_success 'setup renamed files' ' + git reset --hard && + + test_seq -f "Line %d" 15 > path0 && + test_commit --append path0 path0 "" P0 && + mv path0 path1 && + git add path0 path1 && + git commit -m "rename path0 to path1" && + git tag P1 && + + mkdir dir && + sed "s/Line 11/line 11/" dir/path2 && + rm -f path1 && + git add path1 dir/path2 && + git commit -m "rename path1 to dir/path2, change contents" && + git tag P2 && + + git checkout -b topic P0 && + sed "s/Line 12/line 12/" path3 && + rm -f path0 && + git add path0 path3 && + git commit -m "rename path0 to path3, change contents" && + git tag T +' + +check_export_endpoints () { + local args= + if test $# -gt 5 + then + args="$1" + shift + else + args="$1 $2" + fi + + local endpoint_a="$1" + local endpoint_B="$2" + local path_a="$3" + local path_b="$4" + local desc="$5" + + test_expect_success "GIT_EXTERNAL_DIFF endpoints are commits, $desc" " + >revs_and_paths.txt && + e1=\$(git rev-parse $endpoint_a) && + e2=\$(git rev-parse $endpoint_B) && + cat >expect <<-EOF && + END_A=\$e1 END_B=\$e2 PATH_A=$path_a PATH_B=$path_b + EOF + + GIT_EXTERNAL_DIFF=./ext-diff-endpoints.sh git diff $args && + test_cmp expect revs_and_paths.txt + " +} + +# NB: inputs are tags or branches, output is always in terms of commits +check_export_endpoints A B file file "file changed" +check_export_endpoints B C /dev/null file2 "file added" +check_export_endpoints C B file2 /dev/null "file deleted" +check_export_endpoints "-R B C" C B file2 /dev/null "-R reverses diff" +check_export_endpoints P0 P1 path0 path1 "path renamed, contents unchanged" +check_export_endpoints P1 P2 path1 dir/path2 "path renamed and contents changed" +check_export_endpoints "P2^^ P2^" P0 P1 path0 path1 "expression resolves to commit" +check_export_endpoints A..B A B file file "range A..B" +check_export_endpoints P0...P2 P0 P2 path0 dir/path2 "merge base range (base is same as left) P0...P2" +check_export_endpoints "-R P0...P2" P2 P0 dir/path2 path0 "merge base reverse -R P0...P2" +check_export_endpoints P2...T P0 T path0 path3 "merge-base range P2...T" +check_export_endpoints "--merge-base P2 T" P0 T path0 path3 "--merge-base P2 T" +check_export_endpoints main...topic P0 T path0 path3 "merge-base range on branches main...topic" +check_export_endpoints "P0 P1 -- \"path1\"" P0 P1 /dev/null path1 "add instead of rename as a result of pathspec scope" +check_export_endpoints "--relative=dir P1 P2" P1 P2 /dev/null path2 "--relative=dir" + +test_expect_success 'GIT_EXTERNAL_DIFF endpoints are trees' ' + >revs_and_paths.txt && + end_a=$(git rev-parse A^{tree}) && + end_b=$(git rev-parse B^{tree}) && + cat >expect <<-EOF && + END_A=$end_a END_B=$end_b PATH_A=file PATH_B=file + EOF + GIT_EXTERNAL_DIFF=./ext-diff-endpoints.sh git diff $end_a $end_b && + test_cmp expect revs_and_paths.txt +' + test_done