Skip to content

Commit 1a3119e

Browse files
jacob-kellergitster
authored andcommitted
blame: allow --contents to work with non-HEAD commit
The --contents option can be used with git blame to blame the file as if it had the contents from the specified file. This is akin to copying the contents into the working tree and then running git blame. This option has been supported since 1cfe773 ("git-blame: no rev means start from the working tree file.") The --contents option always blames the file as if it was based on the current HEAD commit. If you try to pass a revision while using --contents, you get the following error: fatal: cannot use --contents with final commit object name This is because the blame process generates a fake working tree commit which always uses the HEAD object as its sole parent. Enhance fake_working_tree_commit to take the object ID to use for the parent instead of always using the HEAD object. Then, always generate a fake commit when we have contents provided, even if we have a final object. Remove the check to disallow --contents and a final revision. Note that the behavior of generating a fake working commit is still skipped when a revision is provided but --contents is not provided. Generating such a commit in that case would combine the currently checked out file contents with the provided revision, which breaks normal blame behavior and produces unexpected results. This enables use of --contents with an arbitrary revision, rather than forcing the use of the local HEAD commit. This makes the --contents option significantly more flexible, as it is no longer required to check out the working tree to the desired commit before using --contents. Reword the documentation so that its clear that --contents can be used with <rev>. Add tests for the --contents option to the annotate-tests.sh test script. Signed-off-by: Jacob Keller <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 73876f4 commit 1a3119e

File tree

4 files changed

+46
-20
lines changed

4 files changed

+46
-20
lines changed

Documentation/blame-options.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ include::line-range-format.txt[]
6464
manual page.
6565

6666
--contents <file>::
67-
When <rev> is not specified, the command annotates the
68-
changes starting backwards from the working tree copy.
69-
This flag makes the command pretend as if the working
70-
tree copy has the contents of the named file (specify
71-
`-` to make the command read from the standard input).
67+
Pretend the file being annotated has a commit with the
68+
contents from the named file and a parent of <rev>,
69+
defaulting to HEAD when no <rev> is specified. You may
70+
specify '-' to make the command read from the standard
71+
input for the file contents.
7272

7373
--date <format>::
7474
Specifies the format used to output dates. If --date is not

Documentation/git-blame.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ SYNOPSIS
1212
[-L <range>] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>]
1313
[--ignore-rev <rev>] [--ignore-revs-file <file>]
1414
[--color-lines] [--color-by-age] [--progress] [--abbrev=<n>]
15-
[<rev> | --contents <file> | --reverse <rev>..<rev>] [--] <file>
15+
[ --contents <file> ] [<rev> | --reverse <rev>..<rev>] [--] <file>
1616

1717
DESCRIPTION
1818
-----------

blame.c

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,12 @@ static void set_commit_buffer_from_strbuf(struct repository *r,
176176
static struct commit *fake_working_tree_commit(struct repository *r,
177177
struct diff_options *opt,
178178
const char *path,
179-
const char *contents_from)
179+
const char *contents_from,
180+
struct object_id *oid)
180181
{
181182
struct commit *commit;
182183
struct blame_origin *origin;
183184
struct commit_list **parent_tail, *parent;
184-
struct object_id head_oid;
185185
struct strbuf buf = STRBUF_INIT;
186186
const char *ident;
187187
time_t now;
@@ -197,10 +197,7 @@ static struct commit *fake_working_tree_commit(struct repository *r,
197197
commit->date = now;
198198
parent_tail = &commit->parents;
199199

200-
if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL))
201-
die("no such ref: HEAD");
202-
203-
parent_tail = append_parent(r, parent_tail, &head_oid);
200+
parent_tail = append_parent(r, parent_tail, oid);
204201
append_merge_parents(r, parent_tail);
205202
verify_working_tree_path(r, commit, path);
206203

@@ -2771,22 +2768,37 @@ void setup_scoreboard(struct blame_scoreboard *sb,
27712768
sb->commits.compare = compare_commits_by_reverse_commit_date;
27722769
}
27732770

2774-
if (sb->final && sb->contents_from)
2775-
die(_("cannot use --contents with final commit object name"));
2776-
27772771
if (sb->reverse && sb->revs->first_parent_only)
27782772
sb->revs->children.name = NULL;
27792773

2780-
if (!sb->final) {
2774+
if (sb->contents_from || !sb->final) {
2775+
struct object_id head_oid, *parent_oid;
2776+
27812777
/*
2782-
* "--not A B -- path" without anything positive;
2783-
* do not default to HEAD, but use the working tree
2784-
* or "--contents".
2778+
* Build a fake commit at the top of the history, when
2779+
* (1) "git blame [^A] --path", i.e. with no positive end
2780+
* of the history range, in which case we build such
2781+
* a fake commit on top of the HEAD to blame in-tree
2782+
* modifications.
2783+
* (2) "git blame --contents=file [A] -- path", with or
2784+
* without positive end of the history range but with
2785+
* --contents, in which case we pretend that there is
2786+
* a fake commit on top of the positive end (defaulting to
2787+
* HEAD) that has the given contents in the path.
27852788
*/
2789+
if (sb->final) {
2790+
parent_oid = &sb->final->object.oid;
2791+
} else {
2792+
if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL))
2793+
die("no such ref: HEAD");
2794+
parent_oid = &head_oid;
2795+
}
2796+
27862797
setup_work_tree();
27872798
sb->final = fake_working_tree_commit(sb->repo,
27882799
&sb->revs->diffopt,
2789-
sb->path, sb->contents_from);
2800+
sb->path, sb->contents_from,
2801+
parent_oid);
27902802
add_pending_object(sb->revs, &(sb->final->object), ":");
27912803
}
27922804

t/annotate-tests.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ test_expect_success 'blame 1 author' '
7272
check_count A 2
7373
'
7474

75+
test_expect_success 'blame with --contents' '
76+
check_count --contents=file A 2
77+
'
78+
79+
test_expect_success 'blame with --contents changed' '
80+
echo "1A quick brown fox jumps over the" >contents &&
81+
echo "another lazy dog" >>contents &&
82+
check_count --contents=contents A 1 "Not Committed Yet" 1
83+
'
84+
7585
test_expect_success 'blame in a bare repo without starting commit' '
7686
git clone --bare . bare.git &&
7787
(
@@ -98,6 +108,10 @@ test_expect_success 'blame 2 authors' '
98108
check_count A 2 B 2
99109
'
100110

111+
test_expect_success 'blame with --contents and revision' '
112+
check_count -h testTag --contents=file A 2 "Not Committed Yet" 2
113+
'
114+
101115
test_expect_success 'setup B1 lines (branch1)' '
102116
git checkout -b branch1 main &&
103117
echo "3A slow green fox jumps into the" >>file &&

0 commit comments

Comments
 (0)