Merge branch 'sb/describe-blob'

"git describe" was taught to dig trees deeper to find a
<commit-ish>:<path> that refers to a given blob object.

* sb/describe-blob:
  builtin/describe.c: describe a blob
  builtin/describe.c: factor out describe_commit
  builtin/describe.c: print debug statements earlier
  builtin/describe.c: rename `oid` to avoid variable shadowing
  revision.h: introduce blob/tree walking in order of the commits
  list-objects.c: factor out traverse_trees_and_blobs
  t6120: fix typo in test name
This commit is contained in:
Junio C Hamano 2017-12-28 14:08:50 -08:00
commit 556de1a8e3
8 changed files with 277 additions and 52 deletions

View File

@ -3,14 +3,14 @@ git-describe(1)
NAME
----
git-describe - Describe a commit using the most recent tag reachable from it
git-describe - Give an object a human readable name based on an available ref
SYNOPSIS
--------
[verse]
'git describe' [--all] [--tags] [--contains] [--abbrev=<n>] [<commit-ish>...]
'git describe' [--all] [--tags] [--contains] [--abbrev=<n>] --dirty[=<mark>]
'git describe' <blob>
DESCRIPTION
-----------
@ -24,6 +24,12 @@ By default (without --all or --tags) `git describe` only shows
annotated tags. For more information about creating annotated tags
see the -a and -s options to linkgit:git-tag[1].
If the given object refers to a blob, it will be described
as `<commit-ish>:<path>`, such that the blob can be found
at `<path>` in the `<commit-ish>`, which itself describes the
first commit in which this blob occurs in a reverse revision walk
from HEAD.
OPTIONS
-------
<commit-ish>...::
@ -186,6 +192,14 @@ selected and output. Here fewest commits different is defined as
the number of commits which would be shown by `git log tag..input`
will be the smallest number of commits possible.
BUGS
----
Tree objects as well as tag objects not pointing at commits, cannot be described.
When describing blobs, the lightweight tags pointing at blobs are ignored,
but the blob is still described as <committ-ish>:<path> despite the lightweight
tag being favorable.
GIT
---
Part of the linkgit:git[1] suite

View File

@ -686,6 +686,11 @@ ifdef::git-rev-list[]
all object IDs which I need to download if I have the commit
object _bar_ but not _foo_''.
--in-commit-order::
Print tree and blob ids in order of the commits. The tree
and blob ids are printed after they are first referenced
by a commit.
--objects-edge::
Similar to `--objects`, but also print the IDs of excluded
commits prefixed with a ``-'' character. This is used by

View File

@ -3,6 +3,7 @@
#include "lockfile.h"
#include "commit.h"
#include "tag.h"
#include "blob.h"
#include "refs.h"
#include "builtin.h"
#include "exec_cmd.h"
@ -12,6 +13,8 @@
#include "hashmap.h"
#include "argv-array.h"
#include "run-command.h"
#include "revision.h"
#include "list-objects.h"
#define MAX_TAGS (FLAG_BITS - 1)
@ -256,7 +259,7 @@ static unsigned long finish_depth_computation(
return seen_commits;
}
static void display_name(struct commit_name *n)
static void append_name(struct commit_name *n, struct strbuf *dst)
{
if (n->prio == 2 && !n->tag) {
n->tag = lookup_tag(&n->oid);
@ -272,19 +275,18 @@ static void display_name(struct commit_name *n)
}
if (n->tag)
printf("%s", n->tag->tag);
strbuf_addstr(dst, n->tag->tag);
else
printf("%s", n->path);
strbuf_addstr(dst, n->path);
}
static void show_suffix(int depth, const struct object_id *oid)
static void append_suffix(int depth, const struct object_id *oid, struct strbuf *dst)
{
printf("-%d-g%s", depth, find_unique_abbrev(oid->hash, abbrev));
strbuf_addf(dst, "-%d-g%s", depth, find_unique_abbrev(oid->hash, abbrev));
}
static void describe(const char *arg, int last_one)
static void describe_commit(struct object_id *oid, struct strbuf *dst)
{
struct object_id oid;
struct commit *cmit, *gave_up_on = NULL;
struct commit_list *list;
struct commit_name *n;
@ -293,30 +295,25 @@ static void describe(const char *arg, int last_one)
unsigned long seen_commits = 0;
unsigned int unannotated_cnt = 0;
if (get_oid(arg, &oid))
die(_("Not a valid object name %s"), arg);
cmit = lookup_commit_reference(&oid);
if (!cmit)
die(_("%s is not a valid '%s' object"), arg, commit_type);
cmit = lookup_commit_reference(oid);
n = find_commit_name(&cmit->object.oid);
if (n && (tags || all || n->prio == 2)) {
/*
* Exact match to an existing ref.
*/
display_name(n);
append_name(n, dst);
if (longformat)
show_suffix(0, n->tag ? &n->tag->tagged->oid : &oid);
append_suffix(0, n->tag ? &n->tag->tagged->oid : oid, dst);
if (suffix)
printf("%s", suffix);
printf("\n");
strbuf_addstr(dst, suffix);
return;
}
if (!max_candidates)
die(_("no tag exactly matches '%s'"), oid_to_hex(&cmit->object.oid));
if (debug)
fprintf(stderr, _("searching to describe %s\n"), arg);
fprintf(stderr, _("No exact match on refs or tags, searching to describe\n"));
if (!have_util) {
struct hashmap_iter iter;
@ -381,22 +378,21 @@ static void describe(const char *arg, int last_one)
}
if (!match_cnt) {
struct object_id *oid = &cmit->object.oid;
struct object_id *cmit_oid = &cmit->object.oid;
if (always) {
printf("%s", find_unique_abbrev(oid->hash, abbrev));
strbuf_addstr(dst, find_unique_abbrev(cmit_oid->hash, abbrev));
if (suffix)
printf("%s", suffix);
printf("\n");
strbuf_addstr(dst, suffix);
return;
}
if (unannotated_cnt)
die(_("No annotated tags can describe '%s'.\n"
"However, there were unannotated tags: try --tags."),
oid_to_hex(oid));
oid_to_hex(cmit_oid));
else
die(_("No tags can describe '%s'.\n"
"Try --always, or create some tags."),
oid_to_hex(oid));
oid_to_hex(cmit_oid));
}
QSORT(all_matches, match_cnt, compare_pt);
@ -434,15 +430,86 @@ static void describe(const char *arg, int last_one)
}
}
display_name(all_matches[0].name);
append_name(all_matches[0].name, dst);
if (abbrev)
show_suffix(all_matches[0].depth, &cmit->object.oid);
append_suffix(all_matches[0].depth, &cmit->object.oid, dst);
if (suffix)
printf("%s", suffix);
printf("\n");
strbuf_addstr(dst, suffix);
}
struct process_commit_data {
struct object_id current_commit;
struct object_id looking_for;
struct strbuf *dst;
struct rev_info *revs;
};
static void process_commit(struct commit *commit, void *data)
{
struct process_commit_data *pcd = data;
pcd->current_commit = commit->object.oid;
}
static void process_object(struct object *obj, const char *path, void *data)
{
struct process_commit_data *pcd = data;
if (!oidcmp(&pcd->looking_for, &obj->oid) && !pcd->dst->len) {
reset_revision_walk();
describe_commit(&pcd->current_commit, pcd->dst);
strbuf_addf(pcd->dst, ":%s", path);
free_commit_list(pcd->revs->commits);
pcd->revs->commits = NULL;
}
}
static void describe_blob(struct object_id oid, struct strbuf *dst)
{
struct rev_info revs;
struct argv_array args = ARGV_ARRAY_INIT;
struct process_commit_data pcd = { null_oid, oid, dst, &revs};
argv_array_pushl(&args, "internal: The first arg is not parsed",
"--objects", "--in-commit-order", "--reverse", "HEAD",
NULL);
init_revisions(&revs, NULL);
if (setup_revisions(args.argc, args.argv, &revs, NULL) > 1)
BUG("setup_revisions could not handle all args?");
if (prepare_revision_walk(&revs))
die("revision walk setup failed");
traverse_commit_list(&revs, process_commit, process_object, &pcd);
reset_revision_walk();
}
static void describe(const char *arg, int last_one)
{
struct object_id oid;
struct commit *cmit;
struct strbuf sb = STRBUF_INIT;
if (debug)
fprintf(stderr, _("describe %s\n"), arg);
if (get_oid(arg, &oid))
die(_("Not a valid object name %s"), arg);
cmit = lookup_commit_reference_gently(&oid, 1);
if (cmit)
describe_commit(&oid, &sb);
else if (lookup_blob(&oid))
describe_blob(oid, &sb);
else
die(_("%s is neither a commit nor blob"), arg);
puts(sb.buf);
if (!last_one)
clear_commit_marks(cmit, -1);
strbuf_release(&sb);
}
int cmd_describe(int argc, const char **argv, const char *prefix)

View File

@ -214,27 +214,17 @@ static void add_pending_tree(struct rev_info *revs, struct tree *tree)
add_pending_object(revs, &tree->object, "");
}
static void do_traverse(struct rev_info *revs,
show_commit_fn show_commit,
show_object_fn show_object,
void *show_data,
filter_object_fn filter_fn,
void *filter_data)
static void traverse_trees_and_blobs(struct rev_info *revs,
struct strbuf *base,
show_object_fn show_object,
void *show_data,
filter_object_fn filter_fn,
void *filter_data)
{
int i;
struct commit *commit;
struct strbuf base;
strbuf_init(&base, PATH_MAX);
while ((commit = get_revision(revs)) != NULL) {
/*
* an uninteresting boundary commit may not have its tree
* parsed yet, but we are not going to show them anyway
*/
if (commit->tree)
add_pending_tree(revs, commit->tree);
show_commit(commit, show_data);
}
assert(base->len == 0);
for (i = 0; i < revs->pending.nr; i++) {
struct object_array_entry *pending = revs->pending.objects + i;
struct object *obj = pending->item;
@ -251,13 +241,13 @@ static void do_traverse(struct rev_info *revs,
path = "";
if (obj->type == OBJ_TREE) {
process_tree(revs, (struct tree *)obj, show_object,
&base, path, show_data,
base, path, show_data,
filter_fn, filter_data);
continue;
}
if (obj->type == OBJ_BLOB) {
process_blob(revs, (struct blob *)obj, show_object,
&base, path, show_data,
base, path, show_data,
filter_fn, filter_data);
continue;
}
@ -265,7 +255,42 @@ static void do_traverse(struct rev_info *revs,
oid_to_hex(&obj->oid), name);
}
object_array_clear(&revs->pending);
strbuf_release(&base);
}
static void do_traverse(struct rev_info *revs,
show_commit_fn show_commit,
show_object_fn show_object,
void *show_data,
filter_object_fn filter_fn,
void *filter_data)
{
struct commit *commit;
struct strbuf csp; /* callee's scratch pad */
strbuf_init(&csp, PATH_MAX);
while ((commit = get_revision(revs)) != NULL) {
/*
* an uninteresting boundary commit may not have its tree
* parsed yet, but we are not going to show them anyway
*/
if (commit->tree)
add_pending_tree(revs, commit->tree);
show_commit(commit, show_data);
if (revs->tree_blobs_in_commit_order)
/*
* NEEDSWORK: Adding the tree and then flushing it here
* needs a reallocation for each commit. Can we pass the
* tree directory without allocation churn?
*/
traverse_trees_and_blobs(revs, &csp,
show_object, show_data,
filter_fn, filter_data);
}
traverse_trees_and_blobs(revs, &csp,
show_object, show_data,
filter_fn, filter_data);
strbuf_release(&csp);
}
void traverse_commit_list(struct rev_info *revs,

View File

@ -1855,6 +1855,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
revs->dense = 0;
} else if (!strcmp(arg, "--show-all")) {
revs->show_all = 1;
} else if (!strcmp(arg, "--in-commit-order")) {
revs->tree_blobs_in_commit_order = 1;
} else if (!strcmp(arg, "--remove-empty")) {
revs->remove_empty_trees = 1;
} else if (!strcmp(arg, "--merges")) {

View File

@ -121,7 +121,8 @@ struct rev_info {
bisect:1,
ancestry_path:1,
first_parent_only:1,
line_level_traverse:1;
line_level_traverse:1,
tree_blobs_in_commit_order:1;
/* Diff flags */
unsigned int diff:1,

77
t/t6100-rev-list-in-order.sh Executable file
View File

@ -0,0 +1,77 @@
#!/bin/sh
test_description='rev-list testing in-commit-order'
. ./test-lib.sh
test_expect_success 'setup a commit history with trees, blobs' '
for x in one two three four
do
echo $x >$x &&
git add $x &&
git commit -m "add file $x" ||
return 1
done &&
for x in four three
do
git rm $x &&
git commit -m "remove $x" ||
return 1
done
'
test_expect_success 'rev-list --in-commit-order' '
git rev-list --in-commit-order --objects HEAD >actual.raw &&
cut -c 1-40 >actual <actual.raw &&
git cat-file --batch-check="%(objectname)" >expect.raw <<-\EOF &&
HEAD^{commit}
HEAD^{tree}
HEAD^{tree}:one
HEAD^{tree}:two
HEAD~1^{commit}
HEAD~1^{tree}
HEAD~1^{tree}:three
HEAD~2^{commit}
HEAD~2^{tree}
HEAD~2^{tree}:four
HEAD~3^{commit}
# HEAD~3^{tree} skipped, same as HEAD~1^{tree}
HEAD~4^{commit}
# HEAD~4^{tree} skipped, same as HEAD^{tree}
HEAD~5^{commit}
HEAD~5^{tree}
EOF
grep -v "#" >expect <expect.raw &&
test_cmp expect actual
'
test_expect_success 'rev-list lists blobs and trees after commits' '
git rev-list --objects HEAD >actual.raw &&
cut -c 1-40 >actual <actual.raw &&
git cat-file --batch-check="%(objectname)" >expect.raw <<-\EOF &&
HEAD^{commit}
HEAD~1^{commit}
HEAD~2^{commit}
HEAD~3^{commit}
HEAD~4^{commit}
HEAD~5^{commit}
HEAD^{tree}
HEAD^{tree}:one
HEAD^{tree}:two
HEAD~1^{tree}
HEAD~1^{tree}:three
HEAD~2^{tree}
HEAD~2^{tree}:four
# HEAD~3^{tree} skipped, same as HEAD~1^{tree}
# HEAD~4^{tree} skipped, same as HEAD^{tree}
HEAD~5^{tree}
EOF
grep -v "#" >expect <expect.raw &&
test_cmp expect actual
'
test_done

View File

@ -304,12 +304,46 @@ test_expect_success 'describe chokes on severely broken submodules' '
mv .git/modules/sub1/ .git/modules/sub_moved &&
test_must_fail git describe --dirty
'
test_expect_success 'describe ignoring a borken submodule' '
test_expect_success 'describe ignoring a broken submodule' '
git describe --broken >out &&
test_when_finished "mv .git/modules/sub_moved .git/modules/sub1" &&
grep broken out
'
test_expect_success 'describe a blob at a directly tagged commit' '
echo "make it a unique blob" >file &&
git add file && git commit -m "content in file" &&
git tag -a -m "latest annotated tag" unique-file &&
git describe HEAD:file >actual &&
echo "unique-file:file" >expect &&
test_cmp expect actual
'
test_expect_success 'describe a blob with its first introduction' '
git commit --allow-empty -m "empty commit" &&
git rm file &&
git commit -m "delete blob" &&
git revert HEAD &&
git commit --allow-empty -m "empty commit" &&
git describe HEAD:file >actual &&
echo "unique-file:file" >expect &&
test_cmp expect actual
'
test_expect_success 'describe directly tagged blob' '
git tag test-blob unique-file:file &&
git describe test-blob >actual &&
echo "unique-file:file" >expect &&
# suboptimal: we rather want to see "test-blob"
test_cmp expect actual
'
test_expect_success 'describe tag object' '
git tag test-blob-1 -a -m msg unique-file:file &&
test_must_fail git describe test-blob-1 2>actual &&
test_i18ngrep "fatal: test-blob-1 is neither a commit nor blob" actual
'
test_expect_failure ULIMIT_STACK_SIZE 'name-rev works in a deep repo' '
i=1 &&
while test $i -lt 8000