Git development
 help / color / mirror / Atom feed
From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
	"D. Ben Knoble" <ben.knoble@gmail.com>,
	Phillip Wood <phillip.wood123@gmail.com>,
	Harald Nordgren <haraldnordgren@gmail.com>,
	Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v3 2/2] pretty: add %gb to name the branch of each reflog entry
Date: Tue, 27 Oct 2026 08:38:47 +0000	[thread overview]
Message-ID: <dee548f9b1ca11feef2d74d0384c5c3c2e6c20d7.1793090327.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2448.v3.git.git.1793090327.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

The reflog of HEAD records the work done on every branch, so the
output of "git reflog" mixes them and does not say which entry was
made on which branch. Working that out by hand means looking back for
the branch switch before each entry, and a rebase can move HEAD to
another branch without writing one, as "git rebase <upstream>
<branch>" does.

Add 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
    9c8d7e6 HEAD@{1}: (topic) rebase (pick): add tests
    1a2b3c4 HEAD@{2}: (topic) rebase (start): checkout main
    1a2b3c4 HEAD@{3}: (main) commit: fix typo
    5d6e7f8 HEAD@{4}: (main) checkout: moving from topic to main

The branch is read from the entries that branch switches, branch
renames and rebases leave behind. The entries of a rebase are named
after the branch being rebased. When HEAD was detached, the entry
shows "detached at <commit>" with the commit HEAD was at when it
moved on. 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.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/pretty-formats.adoc |   3 +
 pretty.c                          |   4 +
 reflog-walk.c                     | 156 ++++++++++++++++++++++++++++--
 reflog-walk.h                     |   2 +
 t/t1414-reflog-walk.sh            | 122 +++++++++++++++++++++++
 5 files changed, 281 insertions(+), 6 deletions(-)

diff --git a/Documentation/pretty-formats.adoc b/Documentation/pretty-formats.adoc
index 2ae0eb11a9..a8d00397c4 100644
--- a/Documentation/pretty-formats.adoc
+++ b/Documentation/pretty-formats.adoc
@@ -334,6 +334,9 @@ endif::git-rev-list[]
 +%gE+:: reflog identity email (respecting .mailmap, see
 	linkgit:git-shortlog[1] or linkgit:git-blame[1])
 +%gs+:: reflog subject
++%gb+:: reflog branch, the branch `HEAD` was on when the entry was
+	recorded, e.g., `topic` or `detached at 1a2b3c4`. For the reflog
+	of any other ref, this is the shortened refname.
 ++%(trailers++`[:<option>,...]`++)++::
 display the trailers of the body as interpreted by
 linkgit:git-interpret-trailers[1]. The `trailers` string may be followed by
diff --git a/pretty.c b/pretty.c
index d8a9f370f6..1b3f7e2dc3 100644
--- a/pretty.c
+++ b/pretty.c
@@ -1637,6 +1637,10 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 			if (c->pretty_ctx->reflog_info)
 				get_reflog_message(sb, c->pretty_ctx->reflog_info);
 			return 2;
+		case 'b':	/* reflog branch */
+			if (c->pretty_ctx->reflog_info)
+				get_reflog_branch(sb, c->pretty_ctx->reflog_info);
+			return 2;
 		case 'n':
 		case 'N':
 		case 'e':
diff --git a/reflog-walk.c b/reflog-walk.c
index 4dbeaa93a7..45b5dc09b5 100644
--- a/reflog-walk.c
+++ b/reflog-walk.c
@@ -3,7 +3,10 @@
 #include "git-compat-util.h"
 #include "commit.h"
 #include "refs.h"
+#include "reflog.h"
 #include "diff.h"
+#include "hex.h"
+#include "object-name.h"
 #include "repository.h"
 #include "revision.h"
 #include "string-list.h"
@@ -18,8 +21,10 @@ struct complete_reflogs {
 		timestamp_t timestamp;
 		int tz;
 		char *message;
+		char *branch;
 	} *items;
 	int nr, alloc;
+	bool branches_marked;
 };
 
 static int read_one_reflog(const char *refname UNUSED,
@@ -38,6 +43,7 @@ static int read_one_reflog(const char *refname UNUSED,
 	item->timestamp = timestamp;
 	item->tz = tz;
 	item->message = xstrdup(message);
+	item->branch = NULL;
 	array->nr++;
 	return 0;
 }
@@ -52,6 +58,7 @@ static void free_complete_reflog(struct complete_reflogs *array)
 	for (i = 0; i < array->nr; i++) {
 		free(array->items[i].email);
 		free(array->items[i].message);
+		free(array->items[i].branch);
 	}
 	free(array->items);
 	free(array->ref);
@@ -65,6 +72,113 @@ static void complete_reflogs_clear(void *util, const char *str UNUSED)
 	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, " (");
+	const char *colon = strstr(message, ": ");
+
+	if (!p || (colon && colon < p) ||
+	    !skip_prefix(p + 2, step, &p) || !skip_prefix(p, "): ", &p))
+		return NULL;
+	return p;
+}
+
+struct branch_move {
+	const char *from, *to;
+	size_t from_len, to_len;
+};
+
+static bool parse_branch_move(const char *message, struct branch_move *move)
+{
+	const char *to;
+
+	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 ");
+		if (!to)
+			return false;
+		move->from_len = to - move->from;
+		move->to = to + strlen(" to ");
+	} 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)) {
+		move->from = move->to = to;
+		move->from_len = strcspn(to, "\n");
+	} else if (skip_prefix(message, "rebase: " REFLOG_CHECKOUT, &to)) {
+		move->to = to;
+	} else if (!skip_rebase_step(message, REFLOG_STEP_START)) {
+		return false;
+	}
+	if (move->to)
+		move->to_len = strcspn(move->to, "\n");
+	return true;
+}
+
+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);
+	char *branch = NULL;
+	int unknown = array->nr;
+
+	if (head)
+		branch = branch_label(head, strlen(head), &oid);
+
+	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)) {
+			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++)
+				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;
+		}
+	}
+
+	if (!branch && unknown < array->nr)
+		for (int i = 0; i < unknown; i++)
+			array->items[i].branch =
+				xstrdup(array->items[unknown].branch);
+	free(branch);
+}
+
 static struct complete_reflogs *read_complete_reflog(const char *ref)
 {
 	struct complete_reflogs *reflogs =
@@ -234,6 +348,16 @@ int add_reflog_for_walk(struct reflog_walk_info *info,
 	return 0;
 }
 
+static const char *short_ref(struct complete_reflogs *reflogs)
+{
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	if (!reflogs->short_ref)
+		reflogs->short_ref =
+			refs_shorten_unambiguous_ref(refs, reflogs->ref, 0);
+	return reflogs->short_ref;
+}
+
 void get_reflog_selector(struct strbuf *sb,
 			 struct reflog_walk_info *reflog_info,
 			 struct date_mode dmode, int force_date,
@@ -247,12 +371,7 @@ void get_reflog_selector(struct strbuf *sb,
 		return;
 
 	if (shorten) {
-		if (!commit_reflog->reflogs->short_ref)
-			commit_reflog->reflogs->short_ref
-				= refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-							       commit_reflog->reflogs->ref,
-							       0);
-		printed_ref = commit_reflog->reflogs->short_ref;
+		printed_ref = short_ref(commit_reflog->reflogs);
 	} else {
 		printed_ref = commit_reflog->reflogs->ref;
 	}
@@ -287,6 +406,31 @@ void get_reflog_message(struct strbuf *sb,
 	strbuf_add(sb, info->message, len);
 }
 
+void get_reflog_branch(struct strbuf *sb,
+		       struct reflog_walk_info *reflog_info)
+{
+	struct commit_reflog *commit_reflog = reflog_info->last_commit_reflog;
+	struct complete_reflogs *reflogs;
+	const char *branch;
+
+	if (!commit_reflog)
+		return;
+
+	reflogs = commit_reflog->reflogs;
+	if (strcmp(reflogs->ref, "HEAD")) {
+		strbuf_addstr(sb, short_ref(reflogs));
+		return;
+	}
+
+	if (!reflogs->branches_marked) {
+		mark_branches(reflogs);
+		reflogs->branches_marked = true;
+	}
+	branch = reflogs->items[commit_reflog->recno+1].branch;
+	if (branch)
+		strbuf_addstr(sb, branch);
+}
+
 const char *get_reflog_ident(struct reflog_walk_info *reflog_info)
 {
 	struct commit_reflog *commit_reflog = reflog_info->last_commit_reflog;
diff --git a/reflog-walk.h b/reflog-walk.h
index 989583dc55..fc1f344ce6 100644
--- a/reflog-walk.h
+++ b/reflog-walk.h
@@ -13,6 +13,8 @@ void show_reflog_message(struct reflog_walk_info *info, int,
 			 struct date_mode, int force_date);
 void get_reflog_message(struct strbuf *sb,
 			struct reflog_walk_info *reflog_info);
+void get_reflog_branch(struct strbuf *sb,
+		       struct reflog_walk_info *reflog_info);
 const char *get_reflog_ident(struct reflog_walk_info *reflog_info);
 timestamp_t get_reflog_timestamp(struct reflog_walk_info *reflog_info);
 void get_reflog_selector(struct strbuf *sb,
diff --git a/t/t1414-reflog-walk.sh b/t/t1414-reflog-walk.sh
index be6c3f472c..0e821922ca 100755
--- a/t/t1414-reflog-walk.sh
+++ b/t/t1414-reflog-walk.sh
@@ -131,6 +131,128 @@ test_expect_success 'walk prefers reflog to ref tip' '
 	test_cmp expect actual
 '
 
+test_expect_success '%gb names the branch HEAD was on' '
+	git init branches &&
+	(
+		cd branches &&
+		test_commit base &&
+		git switch -c topic &&
+		test_commit A &&
+		test_commit B &&
+		git switch main &&
+		test_commit upstream &&
+		git switch topic &&
+		git rebase main &&
+
+		cat >expect <<-\EOF &&
+		(topic) rebase (finish): returning to refs/heads/topic
+		(topic) rebase (pick): B
+		(topic) rebase (pick): A
+		(topic) rebase (start): checkout main
+		(topic) checkout: moving from main to topic
+		(main) commit: upstream
+		(main) checkout: moving from topic to main
+		(topic) commit: B
+		(topic) commit: A
+		(topic) checkout: moving from main to topic
+		(main) commit (initial): base
+		EOF
+		git log -g --format="(%gb) %gs" >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '%gb names the ref of other reflogs' '
+	(
+		cd branches &&
+		echo main >expect &&
+		git log -g -1 --format=%gb main >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '%gb follows "git rebase <upstream> <branch>"' '
+	(
+		cd branches &&
+		git switch main &&
+		test_commit main-change &&
+		git rebase main topic &&
+		git switch main &&
+		git rebase main topic &&
+
+		cat >expect <<-\EOF &&
+		(topic) rebase: checkout topic
+		(main) checkout: moving from topic to main
+		(topic) rebase (finish): returning to refs/heads/topic
+		(topic) rebase (pick): B
+		(topic) rebase (pick): A
+		(topic) rebase (start): checkout main
+		(main) commit: main-change
+		(main) checkout: moving from topic to main
+		EOF
+		git log -g -8 --format="(%gb) %gs" >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '%gb names a detached HEAD by the commit it was left at' '
+	(
+		cd branches &&
+		git switch --detach topic~1 &&
+		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) &&
+		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
+		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 &&
+	(
+		cd branches &&
+		test_commit upstream-change
+	) &&
+	(
+		cd downstream &&
+		test_commit local-change &&
+		git pull --rebase &&
+
+		echo "(topic)" >expect &&
+		git log -g --format="(%gb)" >out &&
+		sort -u out >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '%gb follows a renamed branch' '
+	(
+		cd branches &&
+		test_commit before-rename &&
+		git branch -m topic renamed &&
+		test_commit after-rename &&
+
+		cat >expect <<-\EOF &&
+		(renamed) commit: after-rename
+		(renamed) Branch: renamed refs/heads/topic to refs/heads/renamed
+		(topic) commit: before-rename
+		EOF
+		git log -g -3 --format="(%gb) %gs" >actual &&
+		test_cmp expect actual
+	)
+'
+
 test_expect_success 'rev-list -g complains when there are no reflogs' '
 	test_must_fail git rev-list -g
 '
-- 
gitgitgadget

      parent reply	other threads:[~2026-10-27  8:38 UTC|newest]

Thread overview: 38+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-15 13:13 [PATCH 0/3] reflog: add --operations and --historical-branch Harald Nordgren via GitGitGadget
2026-10-15 13:13 ` [PATCH 1/3] reflog: name the strings that mark switches, renames and rebase steps Harald Nordgren via GitGitGadget
2026-10-15 14:35   ` Kristoffer Haugsbakk
2026-10-15 20:52     ` Harald Nordgren
2026-10-15 13:13 ` [PATCH 2/3] reflog: add --operations to show one entry per command Harald Nordgren via GitGitGadget
2026-10-15 13:13 ` [PATCH 3/3] reflog: add --historical-branch to name the branch of each entry Harald Nordgren via GitGitGadget
2026-10-15 14:36 ` [PATCH 0/3] reflog: add --operations and --historical-branch Kristoffer Haugsbakk
2026-10-15 15:24   ` Harald Nordgren
2026-10-15 15:35     ` Kristoffer Haugsbakk
2026-10-15 20:47       ` Harald Nordgren
2026-10-16 18:26     ` D. Ben Knoble
2026-10-23 15:31   ` Harald Nordgren
2026-10-16 18:36 ` D. Ben Knoble
2026-10-17 10:03   ` Harald Nordgren
2026-10-18 18:27 ` [PATCH v2 " Harald Nordgren via GitGitGadget
2026-10-18 18:27   ` [PATCH v2 1/3] reflog: name the strings that mark switches, renames and rebase steps Harald Nordgren via GitGitGadget
2026-10-23 21:19     ` Junio C Hamano
2026-10-23 22:47       ` Harald Nordgren
2026-10-23 22:55         ` Junio C Hamano
2026-10-18 18:27   ` [PATCH v2 2/3] reflog: add --operations to show one entry per command Harald Nordgren via GitGitGadget
2026-10-18 18:27   ` [PATCH v2 3/3] reflog: add --historical-branch to name the branch of each entry Harald Nordgren via GitGitGadget
2026-10-25 19:18     ` Kristoffer Haugsbakk
2026-10-25 20:22       ` Harald Nordgren
2026-10-26  8:39         ` Kristoffer Haugsbakk
2026-10-26  8:42           ` Harald Nordgren
2026-10-23 15:32   ` [PATCH v2 0/3] reflog: add --operations and --historical-branch Harald Nordgren
2026-10-24 13:51   ` Phillip Wood
2026-10-24 15:22     ` D. Ben Knoble
2026-10-24 16:40     ` Junio C Hamano
2026-10-24 18:46       ` Harald Nordgren
2026-10-25 18:52         ` Kristoffer Haugsbakk
2026-10-26 10:52       ` Phillip Wood
2026-10-26 11:00         ` Harald Nordgren
2026-10-26 15:07           ` Junio C Hamano
2026-10-24 19:12     ` Harald Nordgren
2026-10-27  8:38 ` [PATCH v3 0/2] " Harald Nordgren via GitGitGadget
2026-10-27  8:38   ` [PATCH v3 1/2] reflog: name the strings that mark switches, renames and rebase steps Harald Nordgren via GitGitGadget
2026-10-27  8:38   ` Harald Nordgren via GitGitGadget [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=dee548f9b1ca11feef2d74d0384c5c3c2e6c20d7.1793090327.git.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=ben.knoble@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=haraldnordgren@gmail.com \
    --cc=kristofferhaugsbakk@fastmail.com \
    --cc=phillip.wood123@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox