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 1cfe77333f ("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 <jacob.keller@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Jacob Keller 2023-03-24 10:08:00 -07:00 committed by Junio C Hamano
parent 73876f4861
commit 1a3119ed06
4 changed files with 46 additions and 20 deletions

View File

@ -64,11 +64,11 @@ include::line-range-format.txt[]
manual page.
--contents <file>::
When <rev> is not specified, the command annotates the
changes starting backwards from the working tree copy.
This flag makes the command pretend as if the working
tree copy has the contents of the named file (specify
`-` to make the command read from the standard input).
Pretend the file being annotated has a commit with the
contents from the named file and a parent of <rev>,
defaulting to HEAD when no <rev> is specified. You may
specify '-' to make the command read from the standard
input for the file contents.
--date <format>::
Specifies the format used to output dates. If --date is not

View File

@ -12,7 +12,7 @@ SYNOPSIS
[-L <range>] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>]
[--ignore-rev <rev>] [--ignore-revs-file <file>]
[--color-lines] [--color-by-age] [--progress] [--abbrev=<n>]
[<rev> | --contents <file> | --reverse <rev>..<rev>] [--] <file>
[ --contents <file> ] [<rev> | --reverse <rev>..<rev>] [--] <file>
DESCRIPTION
-----------

40
blame.c
View File

@ -176,12 +176,12 @@ static void set_commit_buffer_from_strbuf(struct repository *r,
static struct commit *fake_working_tree_commit(struct repository *r,
struct diff_options *opt,
const char *path,
const char *contents_from)
const char *contents_from,
struct object_id *oid)
{
struct commit *commit;
struct blame_origin *origin;
struct commit_list **parent_tail, *parent;
struct object_id head_oid;
struct strbuf buf = STRBUF_INIT;
const char *ident;
time_t now;
@ -197,10 +197,7 @@ static struct commit *fake_working_tree_commit(struct repository *r,
commit->date = now;
parent_tail = &commit->parents;
if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL))
die("no such ref: HEAD");
parent_tail = append_parent(r, parent_tail, &head_oid);
parent_tail = append_parent(r, parent_tail, oid);
append_merge_parents(r, parent_tail);
verify_working_tree_path(r, commit, path);
@ -2771,22 +2768,37 @@ void setup_scoreboard(struct blame_scoreboard *sb,
sb->commits.compare = compare_commits_by_reverse_commit_date;
}
if (sb->final && sb->contents_from)
die(_("cannot use --contents with final commit object name"));
if (sb->reverse && sb->revs->first_parent_only)
sb->revs->children.name = NULL;
if (!sb->final) {
if (sb->contents_from || !sb->final) {
struct object_id head_oid, *parent_oid;
/*
* "--not A B -- path" without anything positive;
* do not default to HEAD, but use the working tree
* or "--contents".
* Build a fake commit at the top of the history, when
* (1) "git blame [^A] --path", i.e. with no positive end
* of the history range, in which case we build such
* a fake commit on top of the HEAD to blame in-tree
* modifications.
* (2) "git blame --contents=file [A] -- path", with or
* without positive end of the history range but with
* --contents, in which case we pretend that there is
* a fake commit on top of the positive end (defaulting to
* HEAD) that has the given contents in the path.
*/
if (sb->final) {
parent_oid = &sb->final->object.oid;
} else {
if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL))
die("no such ref: HEAD");
parent_oid = &head_oid;
}
setup_work_tree();
sb->final = fake_working_tree_commit(sb->repo,
&sb->revs->diffopt,
sb->path, sb->contents_from);
sb->path, sb->contents_from,
parent_oid);
add_pending_object(sb->revs, &(sb->final->object), ":");
}

View File

@ -72,6 +72,16 @@ test_expect_success 'blame 1 author' '
check_count A 2
'
test_expect_success 'blame with --contents' '
check_count --contents=file A 2
'
test_expect_success 'blame with --contents changed' '
echo "1A quick brown fox jumps over the" >contents &&
echo "another lazy dog" >>contents &&
check_count --contents=contents A 1 "Not Committed Yet" 1
'
test_expect_success 'blame in a bare repo without starting commit' '
git clone --bare . bare.git &&
(
@ -98,6 +108,10 @@ test_expect_success 'blame 2 authors' '
check_count A 2 B 2
'
test_expect_success 'blame with --contents and revision' '
check_count -h testTag --contents=file A 2 "Not Committed Yet" 2
'
test_expect_success 'setup B1 lines (branch1)' '
git checkout -b branch1 main &&
echo "3A slow green fox jumps into the" >>file &&