Skip to content

reflog: add format %gb

Harald Nordgren via GitGitGadget git 5 versions – New

Get series with b4
b4 am [email protected]

Versions newest first · linked by author and title

  1. v5 Latest2 patches 0 repliesNew
  2. v42 patches 2 repliesSuperseded
  3. v32 patches 1 replySuperseded
Earlier versions · 2
VersionPostedPatchesRepliesStatus
v23 patches21 repliesSuperseded
v13 patches10 repliesSuperseded

Changes in v5 from the cover letter

* detached at <commit> now names the commit HEAD was detached at, or for a detached rebase, the commit it ended on.
* git rebase <upstream> <rev> counts <rev> as a branch only if refs/heads/<rev> exists, so origin/main and refs/heads/main show as detached.
* A commit whose subject starts with checkout is no longer taken for a rebase checkout.
* Added Phillip's test, and tests for a detached rebase that fast-forwards and one that is aborted.

Patches in v5 No applies recorded yet

#Subject and filesSizeRepliesApplied
1/2reflog: name the strings that mark switches, renames and rebase stepsbuiltin/branch.c builtin/checkout.c builtin/rebase.c +4 more+35 -200 replies
2/2pretty: add %gb to name the branch of each reflog entryDocumentation/pretty-formats.adoc pretty.c reflog-walk.c +2 more+449 -60 replies

Reviews trailers per patch

No Reviewed-by, Acked-by or Tested-by trailers for v5 yet.

Read the v5 thread

Cover letter [PATCH v5 0/2] reflog: add format %gb

Create a better reflog. Follows the discussion in
https://lore.kernel.org/git/[email protected]/.

The reflog of HEAD records the work done on every branch, so "git reflog" mixes them and does not say which entry was made on which branch. This adds a "%gb" placeholder that names the branch HEAD was on when each entry was recorded:

$ git log -g --format='%h %gd: (%gb) %gs'
9c8d7e6 HEAD@{0}: (topic) rebase (finish): returning to refs/heads/topic
1a2b3c4 HEAD@{2}: (topic) rebase (start): checkout main
1a2b3c4 HEAD@{3}: (main) commit: fix typo

It follows branch switches, branch renames and rebases, including "git rebase ", rebases of a detached HEAD and rebases of a branch that was already up to date. When HEAD was detached, the entry shows "detached at ".

Changes in v5:

* detached at <commit> now names the commit HEAD was detached at, or for a detached rebase, the commit it ended on.
* git rebase <upstream> <rev> counts <rev> as a branch only if refs/heads/<rev> exists, so origin/main and refs/heads/main show as detached.
* A commit whose subject starts with checkout is no longer taken for a rebase checkout.
* Added Phillip's test, and tests for a detached rebase that fast-forwards and one that is aborted.

Changes in v4:

* %gb now names the right branch when git rebase <upstream> <branch> runs with GIT_REFLOG_ACTION set.
* Reworded the %gb commit message.

Changes in v3:

* Dropped --operations.
* Replaced --historical-branch with a %gb placeholder for --format, e.g. git log -g --format='%h %gd: (%gb) %gs'.
* %gb names the right branch after git rebase <upstream> <branch>, a rebase of a detached HEAD and an up-to-date rebase.
* Named the returning to and checkout reflog strings in reflog.h too.

Changes in v2:

* Move the "historical branch" rightward in the display for cleaner alignment.
* Improve commit messages.

Harald Nordgren (2):
reflog: name the strings that mark switches, renames and rebase steps
pretty: add %gb to name the branch of each reflog entry

 Documentation/pretty-formats.adoc |   3 +
 builtin/branch.c                  |   3 +-
 builtin/checkout.c                |   3 +-
 builtin/rebase.c                  |  19 +--
 object-name.c                     |   3 +-
 pretty.c                          |   4 +
 reflog-walk.c                     | 217 +++++++++++++++++++++++++++-
 reflog-walk.h                     |   2 +
 reflog.h                          |   9 ++
 sequencer.c                       |  15 +-
 t/t1414-reflog-walk.sh            | 229 ++++++++++++++++++++++++++++++
 wt-status.c                       |   3 +-
 12 files changed, 484 insertions(+), 26 deletions(-)

base-commit: fb255012acbcbe81189cb8014da0280b09c66ecc
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2448%2FHaraldNordgren%2Freflog-operations-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2448/HaraldNordgren/reflog-operations-v5
Pull-Request: https://github.com/git/git/pull/2448

Range-diff vs v4:

1: f960a24b8b ! 1: 115b5a81b7 reflog: name the strings that mark switches, renames and rebase steps
@@ Commit message
messages are written or read: the "checkout: moving from " prefix
written by "git checkout", the "Branch: renamed " prefix written by
"git branch -m", and the "start", "finish" and "abort" steps, the
- "returning to " before the branch a rebase goes back to and the
- "checkout " before what it checks out, all written by "git rebase".
+ "returning to " before the branch a rebase goes back to, the
+ "checkout " before what it checks out and the "fast-forward" it
+ records when a commit can be reused, all written by "git rebase".

No change in behavior.

@@ reflog.h
+#define REFLOG_STEP_ABORT "abort"
+#define REFLOG_RETURNING_TO "returning to "
+#define REFLOG_CHECKOUT "checkout "
++#define REFLOG_FAST_FORWARD "fast-forward"
+
#define REFLOG_EXPIRE_TOTAL (1 << 0)
#define REFLOG_EXPIRE_UNREACH (1 << 1)
@@ sequencer.c
#include "sparse-index.h"
#include "strvec.h"
#include "quote.h"
+@@ sequencer.c: static int fast_forward_to(struct repository *r,
+ if (checkout_fast_forward(r, from, to, 1))
+ return -1; /* the callee should have complained already */
+
+- strbuf_addf(&sb, "%s: fast-forward", action_name(opts));
++ strbuf_addf(&sb, "%s: " REFLOG_FAST_FORWARD, action_name(opts));
+
+ transaction = ref_store_transaction_begin(get_main_ref_store(the_repository),
+ 0, &err);
@@ sequencer.c: static int checkout_onto(struct repository *r, struct replay_opts *opts,
RESET_WORKING_TREE_UPDATE_HEAD | RESET_WORKING_TREE_UPDATE_ORIG_HEAD |
2: 99a1047702 ! 2: 34bb930908 pretty: add %gb to name the branch of each reflog entry

     @@ Commit message
          The branch is read from the entries created by branch switches,
          branch renames and rebases. The entries of a rebase are labeled with
          the branch being rebased. When HEAD was detached, the entry shows
     -    "detached at <commit>" with the commit HEAD was at when it moved on.
     +    "detached at <commit>" with the commit HEAD was detached at, or for a
     +    rebase of a detached HEAD the commit the rebase ended on. These
     +    entries only record what was typed, so when no newer entry says
     +    otherwise, a name counts as a branch if a branch of that name exists.
          When nothing older in the reflog says which branch a rebase started
          on, the rebase is taken to have started on the branch it rebased. For
          the reflog of any other ref, "%gb" is the name of that ref.
     @@ reflog-walk.c: static void complete_reflogs_clear(void *util, const char *str UN
       	free_complete_reflog(array);
       }
       
     -+static char *branch_label(const char *name, size_t len,
     -+			  const struct object_id *oid)
     -+{
     -+	if (name) {
     -+		struct object_id hex_oid;
     -+		char *branch;
     -+
     -+		if (skip_prefix(name, "refs/heads/", &name))
     -+			len -= strlen("refs/heads/");
     -+		branch = xmemdupz(name, len);
     -+		if (strcmp(branch, "HEAD") && get_oid_hex(branch, &hex_oid) &&
     -+		    !check_refname_format(branch, REFNAME_ALLOW_ONELEVEL))
     -+			return branch;
     -+		free(branch);
     -+	}
     -+	return xstrfmt("detached at %s",
     -+		       repo_find_unique_abbrev(the_repository, oid,
     -+					       DEFAULT_ABBREV));
     -+}
     -+
      +static const char *skip_rebase_step(const char *message, const char *step)
      +{
      +	const char *p = strstr(message, " (");
     @@ reflog-walk.c: static void complete_reflogs_clear(void *util, const char *str UN
      +	return p;
      +}
      +
     -+static const char *skip_rebase_checkout(const char *message)
     ++static bool is_commit_subject(const char *text, const struct object_id *oid)
      +{
     -+	const char *p = strstr(message, " (");
     -+	const char *colon = strstr(message, ": ");
     ++	struct commit *commit = lookup_commit_reference_gently(the_repository,
     ++							       oid, 1);
     ++	const char *buffer, *subject;
     ++	size_t len;
     ++	bool ret;
     ++
     ++	if (!commit)
     ++		return false;
     ++	buffer = repo_get_commit_buffer(the_repository, commit, NULL);
     ++	len = find_commit_subject(buffer, &subject);
     ++	ret = strcspn(text, "\n") == len && !memcmp(text, subject, len);
     ++	repo_unuse_commit_buffer(the_repository, commit, buffer);
     ++	return ret;
     ++}
      +
     -+	if (!colon || (p && p < colon) ||
     -+	    !skip_prefix(colon + 2, REFLOG_CHECKOUT, &p))
     ++static const char *skip_rebase_checkout(const struct reflog_info *item)
     ++{
     ++	const char *colon = strstr(item->message, ": ");
     ++	const char *rev;
     ++
     ++	if (!colon || !skip_prefix(colon + 2, REFLOG_CHECKOUT, &rev) ||
     ++	    is_commit_subject(colon + 2, &item->noid))
      +		return NULL;
     -+	return p;
     ++	return rev;
     ++}
     ++
     ++static const char *existing_branch(const char *name)
     ++{
     ++	char *ref = xstrfmt("refs/heads/%.*s", (int)strcspn(name, "\n"), name);
     ++	bool exists = refs_ref_exists(get_main_ref_store(the_repository), ref);
     ++
     ++	free(ref);
     ++	return exists ? name : NULL;
      +}
      +
      +struct branch_move {
      +	const char *from, *to;
      +	size_t from_len, to_len;
     ++	bool from_detached;
      +};
      +
     -+static bool parse_branch_move(const char *message, struct branch_move *move)
     ++static bool parse_branch_move(const struct reflog_info *item,
     ++			      struct branch_move *move)
      +{
     -+	const char *to;
     ++	const char *message = item->message;
     ++	const char *from, *to, *end;
     ++	struct object_id oid;
      +
     -+	move->from = move->to = NULL;
     -+	move->from_len = move->to_len = 0;
     -+	if (skip_prefix(message, REFLOG_SWITCH_PREFIX, &move->from) ||
     -+	    skip_prefix(message, REFLOG_RENAME_PREFIX, &move->from)) {
     -+		to = strstr(move->from, " to ");
     ++	memset(move, 0, sizeof(*move));
     ++	if (skip_prefix(message, REFLOG_SWITCH_PREFIX, &from)) {
     ++		to = strstr(from, " to ");
      +		if (!to)
      +			return false;
     -+		move->from_len = to - move->from;
     -+		move->to = to + strlen(" to ");
     ++		if (!parse_oid_hex(from, &oid, &end) && end == to) {
     ++			move->from_detached = true;
     ++		} else {
     ++			move->from = from;
     ++			move->from_len = to - from;
     ++		}
     ++		move->to = existing_branch(to + strlen(" to "));
     ++	} else if (skip_prefix(message, REFLOG_RENAME_PREFIX "refs/heads/",
     ++			       &from)) {
     ++		to = strstr(from, " to refs/heads/");
     ++		if (!to)
     ++			return false;
     ++		move->from = from;
     ++		move->from_len = to - from;
     ++		move->to = to + strlen(" to refs/heads/");
      +	} else if (((to = skip_rebase_step(message, REFLOG_STEP_FINISH)) ||
      +		    (to = skip_rebase_step(message, REFLOG_STEP_ABORT))) &&
     -+		   skip_prefix(to, REFLOG_RETURNING_TO, &to)) {
     ++		   skip_prefix(to, REFLOG_RETURNING_TO "refs/heads/", &to)) {
      +		move->from = move->to = to;
      +		move->from_len = strcspn(to, "\n");
     -+	} else if ((to = skip_rebase_checkout(message))) {
     -+		move->to = to;
     -+	} else if (!skip_rebase_step(message, REFLOG_STEP_START)) {
     ++	} else if (skip_rebase_step(message, REFLOG_STEP_START)) {
     ++		return true;
     ++	} else if ((to = skip_rebase_checkout(item))) {
     ++		move->to = existing_branch(to);
     ++	} else {
      +		return false;
      +	}
      +	if (move->to)
     @@ reflog-walk.c: static void complete_reflogs_clear(void *util, const char *str UN
      +	return true;
      +}
      +
     ++static char *detached_label(const struct complete_reflogs *array,
     ++			    int i, int end)
     ++{
     ++	const struct reflog_info *item = &array->items[i];
     ++
     ++	if (skip_rebase_step(item->message, REFLOG_STEP_START)) {
     ++		size_t len = strstr(item->message, " (") + 2 - item->message;
     ++
     ++		for (int j = end - 1; j > i; j--) {
     ++			const char *message = array->items[j].message;
     ++
     ++			if (!strncmp(message, item->message, len) ||
     ++			    starts_with(message,
     ++					"rebase: " REFLOG_FAST_FORWARD)) {
     ++				item = &array->items[j];
     ++				break;
     ++			}
     ++		}
     ++	}
     ++	return xstrfmt("detached at %s",
     ++		       repo_find_unique_abbrev(the_repository, &item->noid,
     ++					       DEFAULT_ABBREV));
     ++}
     ++
      +static void mark_branches(struct complete_reflogs *array)
      +{
      +	struct ref_store *refs = get_main_ref_store(the_repository);
     -+	struct object_id oid;
     -+	const char *head = refs_resolve_ref_unsafe(refs, "HEAD", 0, &oid, NULL);
     ++	const char *head = refs_resolve_ref_unsafe(refs, "HEAD", 0, NULL, NULL);
      +	char *branch = NULL;
     -+	int unknown = array->nr;
     ++	bool detached = false;
     ++	int unlabeled = array->nr;
      +
     -+	if (head)
     -+		branch = branch_label(head, strlen(head), &oid);
     ++	if (head && skip_prefix(head, "refs/heads/", &head))
     ++		branch = xstrdup(head);
     ++	else if (head)
     ++		detached = true;
      +
      +	for (int i = array->nr - 1; i >= 0; i--) {
      +		struct reflog_info *item = &array->items[i];
      +		struct branch_move move;
      +
     -+		if (!parse_branch_move(item->message, &move)) {
     ++		if (!parse_branch_move(item, &move)) {
      +			item->branch = xstrdup_or_null(branch);
      +			continue;
      +		}
      +
      +		if (!branch) {
     -+			branch = branch_label(move.to, move.to_len,
     -+					      &array->items[unknown - 1].noid);
     -+			for (int j = i + 1; j < unknown; j++)
     ++			branch = move.to && !detached ?
     ++				xmemdupz(move.to, move.to_len) :
     ++				detached_label(array, i, unlabeled);
     ++			for (int j = i + 1; j < unlabeled; j++)
      +				array->items[j].branch = xstrdup(branch);
      +		}
      +		item->branch = branch;
     -+		if (move.from) {
     -+			branch = branch_label(move.from, move.from_len,
     -+					      &item->ooid);
     -+		} else {
     -+			branch = NULL;
     -+			unknown = i;
     -+		}
     ++		branch = move.from ? xmemdupz(move.from, move.from_len) : NULL;
     ++		detached = move.from_detached;
     ++		unlabeled = i;
      +	}
      +
     -+	if (!branch && unknown < array->nr)
     -+		for (int i = 0; i < unknown; i++)
     -+			array->items[i].branch =
     -+				xstrdup(array->items[unknown].branch);
     ++	if (!branch) {
     ++		if (detached)
     ++			branch = detached_label(array, 0, unlabeled);
     ++		else if (unlabeled < array->nr)
     ++			branch = xstrdup(array->items[unlabeled].branch);
     ++		for (int i = 0; branch && i < unlabeled; i++)
     ++			array->items[i].branch = xstrdup(branch);
     ++	}
      +	free(branch);
      +}
      +
     @@ t/t1414-reflog-walk.sh: test_expect_success 'walk prefers reflog to ref tip' '
      +	)
      +'
      +
     -+test_expect_success '%gb names a detached HEAD by the commit it was left at' '
     ++test_expect_success '%gb names a detached HEAD by the commit it was detached at' '
      +	(
      +		cd branches &&
      +		git switch --detach topic~1 &&
     ++		detached_at=$(git rev-parse --short HEAD) &&
      +		test_commit detached &&
      +		git rebase topic &&
      +		rebased=$(git rev-parse HEAD) &&
      +		git switch topic &&
      +
     -+		left_before=$(git rev-parse --short detached) &&
     -+		left_after=$(git rev-parse --short $rebased) &&
     ++		rebased_at=$(git rev-parse --short $rebased) &&
      +		cat >expect <<-EOF &&
      +		(topic) checkout: moving from $rebased to topic
     -+		(detached at $left_after) rebase (pick): detached
     -+		(detached at $left_after) rebase (start): checkout topic
     -+		(detached at $left_before) commit: detached
     -+		(detached at $left_before) checkout: moving from topic to topic~1
     ++		(detached at $rebased_at) rebase (pick): detached
     ++		(detached at $rebased_at) rebase (start): checkout topic
     ++		(detached at $detached_at) commit: detached
     ++		(detached at $detached_at) checkout: moving from topic to topic~1
      +		EOF
      +		git log -g -5 --format="(%gb) %gs" >actual &&
      +		test_cmp expect actual
      +	)
      +'
      +
     -+test_expect_success '%gb names the branch of a "git pull --rebase" in a fresh clone' '
     -+	git clone branches downstream &&
     ++test_expect_success '%gb names a detached rebase by the commit it ended on' '
     ++	write_script add-exec <<-\EOF &&
     ++	{ echo "exec true" && cat "$1"; } >"$1.new" &&
     ++	mv "$1.new" "$1"
     ++	EOF
      +	(
      +		cd branches &&
     -+		test_commit upstream-change
     -+	) &&
     ++		git switch --detach topic^0 &&
     ++		test_set_sequence_editor "$TRASH_DIRECTORY/add-exec" &&
     ++		git rebase -i @^ &&
     ++		ended_at=$(git rev-parse --short HEAD) &&
     ++		test_commit after-fast-forward &&
     ++
     ++		cat >expect <<-EOF &&
     ++		(detached at $ended_at) commit: after-fast-forward
     ++		(detached at $ended_at) rebase: fast-forward
     ++		(detached at $ended_at) rebase (start): checkout @^
     ++		EOF
     ++		git log -g -3 --format="(%gb) %gs" >actual &&
     ++		test_cmp expect actual
     ++	)
     ++'
     ++
     ++test_expect_success '%gb names an aborted rebase of a detached HEAD' '
      +	(
     -+		cd downstream &&
     -+		test_commit local-change &&
     -+		git pull --rebase &&
     ++		cd branches &&
     ++		git switch --detach topic^0 &&
     ++		test_must_fail git rebase -f -x false @^ &&
     ++		git rebase --abort &&
     ++		returned_to=$(git rev-parse HEAD) &&
     ++		short=$(git rev-parse --short HEAD) &&
      +
     -+		echo "(topic)" >expect &&
     -+		git log -g --format="(%gb)" >out &&
     -+		sort -u out >actual &&
     ++		cat >expect <<-EOF &&
     ++		(detached at $short) rebase (abort): returning to $returned_to
     ++		(detached at $short) rebase (pick): B
     ++		(detached at $short) rebase (start): checkout @^
     ++		EOF
     ++		git log -g -3 --format="(%gb) %gs" >actual &&
      +		test_cmp expect actual
      +	)
      +'
     @@ t/t1414-reflog-walk.sh: test_expect_success 'walk prefers reflog to ref tip' '
      +test_expect_success '%gb follows a renamed branch' '
      +	(
      +		cd branches &&
     ++		git switch topic &&
      +		test_commit before-rename &&
      +		git branch -m topic renamed &&
      +		test_commit after-rename &&
     @@ t/t1414-reflog-walk.sh: test_expect_success 'walk prefers reflog to ref tip' '
      +		test_cmp expect actual
      +	)
      +'
     ++
     ++test_expect_success '%gb labels branches and detached HEADs' '
     ++	test_create_repo origin &&
     ++	(
     ++		cd origin &&
     ++		test_commit O1 &&
     ++		test_commit O2
     ++	) &&
     ++	git clone origin downstream &&
     ++	(
     ++		cd origin &&
     ++		test_commit O3
     ++	) &&
     ++	(
     ++		cd downstream &&
     ++		test_commit L1 &&
     ++		git branch side &&
     ++		test_commit L2 &&
     ++		git pull -r "file://$TRASH_DIRECTORY/origin" &&
     ++		test_commit L3 &&
     ++		GIT_REFLOG_ACTION="sync switch" git rebase -i FETCH_HEAD side^0 &&
     ++		H1=$(git rev-parse --short HEAD) &&
     ++		test_commit "checkout main" L4.t L4 L4 &&
     ++		git rebase FETCH_HEAD side &&
     ++		test_commit L5 &&
     ++		git rebase origin/main origin/main &&
     ++		H2=$(git rev-parse --short HEAD) &&
     ++		test_commit L6 &&
     ++		git rebase main refs/heads/main &&
     ++		H3=$(git rev-parse --short HEAD) &&
     ++		test_commit L7 &&
     ++		git switch side &&
     ++		test_commit L8 &&
     ++		git checkout origin/main &&
     ++		H4=$(git rev-parse --short HEAD) &&
     ++		test_commit L9 &&
     ++
     ++		cat >expect <<-EOF &&
     ++		commit: L9 (detached at $H4)
     ++		checkout: moving from side to origin/main (detached at $H4)
     ++		commit: L8 (side)
     ++		checkout: moving from $(git rev-parse L7) to side (side)
     ++		commit: L7 (detached at $H3)
     ++		rebase: checkout refs/heads/main (detached at $H3)
     ++		commit: L6 (detached at $H2)
     ++		rebase: checkout origin/main (detached at $H2)
     ++		commit: L5 (side)
     ++		rebase (finish): returning to refs/heads/side (side)
     ++		rebase (pick): L1 (side)
     ++		rebase (start): checkout FETCH_HEAD (side)
     ++		commit: checkout main (detached at $H1)
     ++		sync switch (pick): L1 (detached at $H1)
     ++		sync switch (start): checkout FETCH_HEAD (detached at $H1)
     ++		commit: L3 (main)
     ++		pull -r file://$TRASH_DIRECTORY/origin (finish): returning to refs/heads/main (main)
     ++		pull -r file://$TRASH_DIRECTORY/origin (pick): L2 (main)
     ++		pull -r file://$TRASH_DIRECTORY/origin (pick): L1 (main)
     ++		pull -r file://$TRASH_DIRECTORY/origin (start): checkout $(git rev-parse FETCH_HEAD) (main)
     ++		commit: L2 (main)
     ++		commit: L1 (main)
     ++		clone: from $(cd ../origin && pwd) (main)
     ++		EOF
     ++
     ++		git log -g --format="%gs (%gb)" >actual &&
     ++		test_cmp expect actual
     ++	)
     ++'
      +
       test_expect_success 'rev-list -g complains when there are no reflogs' '
       	test_must_fail git rev-list -g
Signature · 1 line
gitgitgadget