From 82fd0f4a4b2c394ddba279eab7072fe87b750d2e Mon Sep 17 00:00:00 2001 From: Jeff King Date: Fri, 7 Jul 2017 05:07:16 -0400 Subject: revision: disallow reflog walking with revs->limited The reflog-walk code doesn't work with limit_list(). That function traverses down the real history graph, not the fake reflog history that get_revision() returns. So it's not going to actually examine all of the commits we're going to show, because we'd add them to the pending list only during the actual traversal. In practice this limitation doesn't really matter, because the options that require list-limiting generally need UNINTERESTING endpoints or symmetric ranges, which already are forbidden for reflog walks. Still, there are likely some corner cases that would behave oddly. We're better off to warn the user that we can't fulfill their request than to generate potentially wrong output. This will also make it easier to refactor the reflog-walking code, because it eliminates a whole area of corner cases we'd have to consider (that already don't work anyway). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- revision.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'revision.c') diff --git a/revision.c b/revision.c index e181ad1b70..6678de04d9 100644 --- a/revision.c +++ b/revision.c @@ -2366,6 +2366,8 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s if (revs->reverse && revs->reflog_info) die("cannot combine --reverse with --walk-reflogs"); + if (revs->reflog_info && revs->limited) + die("cannot combine --walk-reflogs with history-limiting options"); if (revs->rewrite_parents && revs->children.name) die("cannot combine --parents and --children"); -- cgit v1.2.1 From 7c2f08aa7a26e68475abe5c9fd7250aacbb6b7b2 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Fri, 7 Jul 2017 05:07:58 -0400 Subject: get_revision_1(): replace do-while with an early return The get_revision_1() function tries to avoid entering its main loop at all when there are no commits to look at. But it's perfectly safe to call pop_commit() on an empty list (in which case it will return NULL). Switching to an early return from the loop lets us skip repeating the loop condition before we enter the do-while. That will get more important when we start pulling reflog-walk commits from a source besides the revs->commits queue, as that condition will get much more complicated. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- revision.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'revision.c') diff --git a/revision.c b/revision.c index 6678de04d9..4019e8cf23 100644 --- a/revision.c +++ b/revision.c @@ -3111,12 +3111,12 @@ static void track_linear(struct rev_info *revs, struct commit *commit) static struct commit *get_revision_1(struct rev_info *revs) { - if (!revs->commits) - return NULL; - - do { + while (1) { struct commit *commit = pop_commit(&revs->commits); + if (!commit) + return NULL; + if (revs->reflog_info) { save_parents(revs, commit); fake_reflog_parent(revs->reflog_info, commit); @@ -3150,8 +3150,7 @@ static struct commit *get_revision_1(struct rev_info *revs) track_linear(revs, commit); return commit; } - } while (revs->commits); - return NULL; + } } /* -- cgit v1.2.1 From d08565bf2dd78aa34c68c94f1931539c1c8c5322 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Fri, 7 Jul 2017 05:14:07 -0400 Subject: reflog-walk: stop using fake parents The reflog-walk system works by putting a ref's tip into the pending queue, and then "traversing" the reflog by pretending that the parent of each commit is the previous reflog entry. This causes a number of user-visible oddities, as documented in t1414 (and the commit message which introduced it). We can fix all of them in one go by replacing the fake-reflog system with a much simpler one: just keeping a list of reflogs to show, and walking through them entry by entry. The implementation is fairly straight-forward, but there are a few items to note: 1. We obviously must skip calling add_parents_to_list() when we are traversing reflogs, since we do not want to walk the original parents at all. As a result, we must call try_to_simplify_commit() ourselves. There are other parts of add_parents_to_list() we skip, as well, but none of them should matter for a reflog traversal: - We do not allow UNINTERESTING commits, nor symmetric ranges (and we bail when these are used with "-g"). - Using --source makes no sense, since we aren't traversing. The reflog selector shows the same information with more detail. - Using --first-parent is still sensible, since you may want to see the first-parent diff for each entry. But since we're not traversing, we don't need to cull the parent list here. 2. Since we now just walk the reflog entries themselves, rather than starting with the ref tip, we now look at the "new" field of each entry rather than the "old" (i.e., we are showing entries, not faking parents). This removes all of the tricky logic around skipping past root commits. But note that we have no way to show an entry with the null sha1 in its "new" field (because such a commit obviously does not exist). Normally this would not happen, since we delete reflogs along with refs, but there is one special case. When we rename the currently checked out branch, we write two reflog entries into the HEAD log: one where the commit goes away, and another where it comes back. Prior to this commit, we show both entries with identical reflog messages. After this commit, we show only the "comes back" entry. See the update in t3200 which demonstrates this. Arguably either is fine, as the whole double-entry thing is a bit hacky in the first place. And until a recent fix, we truncated the traversal in such a case anyway, which was _definitely_ wrong. 3. We show individual reflogs in order, but choose which reflog to show at each stage based on which has the most recent timestamp. This interleaves the output from multiple reflogs based on date order, which is probably what you'd want with limiting like "-n 30". Note that the implementation aims for simplicity. It does a linear walk over the reflog queue for each commit it pulls, which may perform badly if you interleave an enormous number of reflogs. That seems like an unlikely use case; if we did want to handle it, we could probably keep a priority queue of reflogs, ordered by the timestamp of their current tip entry. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- revision.c | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) (limited to 'revision.c') diff --git a/revision.c b/revision.c index 4019e8cf23..587199739a 100644 --- a/revision.c +++ b/revision.c @@ -148,16 +148,14 @@ static void add_pending_object_with_path(struct rev_info *revs, if (revs->reflog_info && obj->type == OBJ_COMMIT) { struct strbuf buf = STRBUF_INIT; int len = interpret_branch_name(name, 0, &buf, 0); - int st; if (0 < len && name[len] && buf.len) strbuf_addstr(&buf, name + len); - st = add_reflog_for_walk(revs->reflog_info, - (struct commit *)obj, - buf.buf[0] ? buf.buf: name); + add_reflog_for_walk(revs->reflog_info, + (struct commit *)obj, + buf.buf[0] ? buf.buf: name); strbuf_release(&buf); - if (st) - return; + return; /* do not add the commit itself */ } add_object_array_with_path(obj, name, &revs->pending, mode, path); } @@ -3112,16 +3110,18 @@ static void track_linear(struct rev_info *revs, struct commit *commit) static struct commit *get_revision_1(struct rev_info *revs) { while (1) { - struct commit *commit = pop_commit(&revs->commits); + struct commit *commit; + + if (revs->reflog_info) + commit = next_reflog_entry(revs->reflog_info); + else + commit = pop_commit(&revs->commits); if (!commit) return NULL; - if (revs->reflog_info) { - save_parents(revs, commit); - fake_reflog_parent(revs->reflog_info, commit); + if (revs->reflog_info) commit->object.flags &= ~(ADDED | SEEN | SHOWN); - } /* * If we haven't done the list limiting, we need to look at @@ -3132,7 +3132,10 @@ static struct commit *get_revision_1(struct rev_info *revs) if (revs->max_age != -1 && (commit->date < revs->max_age)) continue; - if (add_parents_to_list(revs, commit, &revs->commits, NULL) < 0) { + + if (revs->reflog_info) + try_to_simplify_commit(revs, commit); + else if (add_parents_to_list(revs, commit, &revs->commits, NULL) < 0) { if (!revs->ignore_missing_links) die("Failed to traverse parents of commit %s", oid_to_hex(&commit->object.oid)); -- cgit v1.2.1 From de239446b69f3b453050af8091e07aa5433421cc Mon Sep 17 00:00:00 2001 From: Jeff King Date: Fri, 7 Jul 2017 05:16:21 -0400 Subject: reflog-walk: apply --since/--until to reflog dates When doing a reflog walk, we use the commit's date to do any date limiting. In earlier versions of Git, this could lead to nonsense results, since a skipped commit would truncate the traversal. So a sequence like: git commit ... git checkout week-old-branch git checkout - git log -g --since=1.day.ago would stop at the week-old-branch, even though the "git commit" entry further back is still interesting. As of the prior commit, which uses a parent-less traversal of the reflog, you get the whole reflog minus any commits whose dates do not match the specified options. This is arguably useful, as you could scan the reflogs for commits that originated in a certain range. But more likely a user doing a reflog walk wants to limit based on the reflog entries themselves. You can simulate --until with: git log -g @{1.day.ago} but there's no way to ask Git to traverse only back to a certain date. E.g.: # show me reflog entries from the past day git log -g --since=1.day.ago This patch teaches the revision machinery to prefer the reflog entry dates to the commit dates when doing a reflog walk. Technically this is a change in behavior that affects plumbing, but the previous behavior was so buggy that it's unlikely anyone was relying on it. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- revision.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'revision.c') diff --git a/revision.c b/revision.c index 587199739a..41b4375c3c 100644 --- a/revision.c +++ b/revision.c @@ -2965,6 +2965,18 @@ static inline int want_ancestry(const struct rev_info *revs) return (revs->rewrite_parents || revs->children.name); } +/* + * Return a timestamp to be used for --since/--until comparisons for this + * commit, based on the revision options. + */ +static timestamp_t comparison_date(const struct rev_info *revs, + struct commit *commit) +{ + return revs->reflog_info ? + get_reflog_timestamp(revs->reflog_info) : + commit->date; +} + enum commit_action get_commit_action(struct rev_info *revs, struct commit *commit) { if (commit->object.flags & SHOWN) @@ -2975,8 +2987,9 @@ enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi return commit_show; if (commit->object.flags & UNINTERESTING) return commit_ignore; - if (revs->min_age != -1 && (commit->date > revs->min_age)) - return commit_ignore; + if (revs->min_age != -1 && + comparison_date(revs, commit) > revs->min_age) + return commit_ignore; if (revs->min_parents || (revs->max_parents >= 0)) { int n = commit_list_count(commit->parents); if ((n < revs->min_parents) || @@ -3130,7 +3143,7 @@ static struct commit *get_revision_1(struct rev_info *revs) */ if (!revs->limited) { if (revs->max_age != -1 && - (commit->date < revs->max_age)) + comparison_date(revs, commit) < revs->max_age) continue; if (revs->reflog_info) -- cgit v1.2.1