Git development
 help / color / mirror / Atom feed
From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Harald Nordgren <haraldnordgren@gmail.com>,
	Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH 3/3] reflog: add --historical-branch to name the branch of each entry
Date: Thu, 15 Oct 2026 13:13:58 +0000	[thread overview]
Message-ID: <4efe74b04e3c69b78caa4acd24c95a8cb64d0c57.1792070038.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2448.git.git.1792070038.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

"git reflog" does not say which branch HEAD was on when an entry was
recorded. After switching between a few branches, telling which
commits and rebases belong to which branch means reading back to the
last "checkout: moving from" entry for each of them. Commit
decorations do not help either, since they only name branches that
still point at that commit, and a rebased or amended commit has none.

Teach "git reflog" and "git log -g" a "--historical-branch" option
that decorates each entry with the branch it was made on, in place of
the refs that point at the commit now. The name is colored like a
branch decoration:

  $ git reflog --historical-branch
  4e5f6a7 (topic) HEAD@{0}: commit: add tests
  1a2b3c4 (topic) HEAD@{1}: checkout: moving from main to topic
  8c9d0e1 (main) HEAD@{2}: commit: fix typo

Every entry gets a name, including ones whose commits are no longer on
any branch. Entries from before a branch was renamed show its old name,
and entries made on a detached HEAD show "detached at <commit>".
Combined with "--operations", which leaves out the branch switches
themselves, it lists what was done to each branch.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/git-reflog.adoc       |   3 +-
 Documentation/rev-list-options.adoc |   8 ++
 log-tree.c                          |  21 ++++++
 reflog-walk.c                       | 113 ++++++++++++++++++++++++++--
 reflog-walk.h                       |   4 +-
 revision.c                          |   8 +-
 revision.h                          |   1 +
 t/t1414-reflog-walk.sh              |  95 ++++++++++++++++++++++-
 8 files changed, 242 insertions(+), 11 deletions(-)

diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc
index 672b9ea72e..cd988f438b 100644
--- a/Documentation/git-reflog.adoc
+++ b/Documentation/git-reflog.adoc
@@ -43,7 +43,8 @@ actions, and in addition the `HEAD` reflog records branch switching.
 --pretty=oneline`; see linkgit:git-log[1] for more information.
 With `--operations`, branch switches and other entries that change no
 branch are left out, and a rebase is shown as a single entry instead
-of one entry for each commit it replayed.
+of one entry for each commit it replayed. With `--historical-branch`,
+each entry is decorated with the branch it was made on.
 
 The "list" subcommand lists all refs which have a corresponding reflog.
 
diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc
index a40ba2641b..d2a0a9b3bc 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -359,6 +359,14 @@ Under `--pretty=reference`, this information will not be shown at all.
 	rebase started. It is an error to use this option unless
 	`--walk-reflogs` is in use.
 
+`--historical-branch`::
+	Decorate each reflog entry with the branch `HEAD` was on when
+	the entry was recorded, such as `(topic)`, in place of the refs
+	that point at the commit now. When `HEAD` was detached, the
+	entry shows `(detached at <commit>)`. For the reflog of any
+	other ref, the name of that ref is shown. It is an error to use
+	this option unless `--walk-reflogs` is in use.
+
 `--merge`::
 	Show commits touching conflicted paths in the range `HEAD...<other>`,
 	where `<other>` is the first existing pseudoref in `MERGE_HEAD`,
diff --git a/log-tree.c b/log-tree.c
index 83a3c4bf9b..d457f0451a 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -421,6 +421,23 @@ void format_decorations(struct strbuf *sb,
 	}
 }
 
+static void show_historical_branch(struct rev_info *opt)
+{
+	const char *branch = get_reflog_branch(opt->reflog_info);
+	enum git_colorbool use_color = opt->diffopt.use_color;
+	const char *color_commit, *color_reset;
+
+	if (!branch)
+		return;
+
+	color_commit = diff_get_color(use_color, DIFF_COMMIT);
+	color_reset = decorate_get_color(use_color, DECORATION_NONE);
+	fprintf(opt->diffopt.file, "%s (%s%s%s%s%s)%s",
+		color_commit, color_reset,
+		decorate_get_color(use_color, DECORATION_REF_LOCAL),
+		branch, color_reset, color_commit, color_reset);
+}
+
 void show_decorations(struct rev_info *opt, struct commit *commit)
 {
 	struct strbuf sb = STRBUF_INIT;
@@ -431,6 +448,10 @@ void show_decorations(struct rev_info *opt, struct commit *commit)
 		if (slot && *slot)
 			fprintf(opt->diffopt.file, "\t%s", *slot);
 	}
+	if (opt->historical_branch) {
+		show_historical_branch(opt);
+		return;
+	}
 	if (!opt->show_decorations)
 		return;
 	format_decorations(&sb, commit, opt->diffopt.use_color, NULL);
diff --git a/reflog-walk.c b/reflog-walk.c
index 82528f5b66..cc42fcd7d8 100644
--- a/reflog-walk.c
+++ b/reflog-walk.c
@@ -5,6 +5,8 @@
 #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"
@@ -19,6 +21,7 @@ struct complete_reflogs {
 		timestamp_t timestamp;
 		int tz;
 		char *message;
+		char *branch;
 		bool user_operation;
 	} *items;
 	int nr, alloc;
@@ -40,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;
 	item->user_operation = true;
 	array->nr++;
 	return 0;
@@ -55,6 +59,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);
@@ -108,6 +113,80 @@ static void mark_operations(struct complete_reflogs *array)
 	}
 }
 
+static char *branch_label(const char *name, size_t len)
+{
+	struct object_id oid;
+	const char *short_name;
+
+	if (len == the_hash_algo->hexsz && !get_oid_hex(name, &oid))
+		return xstrfmt("detached at %s",
+			       repo_find_unique_abbrev(the_repository, &oid,
+						       DEFAULT_ABBREV));
+	if (skip_prefix(name, "refs/heads/", &short_name))
+		len -= short_name - name;
+	else
+		short_name = name;
+	return xmemdupz(short_name, len);
+}
+
+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;
+
+	if (!skip_prefix(message, REFLOG_SWITCH_PREFIX, &move->from) &&
+	    !skip_prefix(message, REFLOG_RENAME_PREFIX, &move->from))
+		return false;
+	to = strstr(move->from, " to ");
+	if (!to)
+		return false;
+	move->from_len = to - move->from;
+	move->to = to + strlen(" to ");
+	move->to_len = strcspn(move->to, "\n");
+	return true;
+}
+
+static void mark_branches(struct complete_reflogs *array)
+{
+	char *branch = NULL;
+
+	if (strcmp(array->ref, "HEAD"))
+		return;
+
+	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)
+			for (int j = i + 1; j < array->nr; j++)
+				array->items[j].branch =
+					branch_label(move.to, move.to_len);
+		item->branch = branch_label(move.to, move.to_len);
+		free(branch);
+		branch = branch_label(move.from, move.from_len);
+	}
+
+	if (!branch) {
+		struct ref_store *refs = get_main_ref_store(the_repository);
+		const char *head = refs_resolve_ref_unsafe(refs, "HEAD", 0,
+							   NULL, NULL);
+
+		if (head && skip_prefix(head, "refs/heads/", &head))
+			for (int i = 0; i < array->nr; i++)
+				array->items[i].branch = xstrdup(head);
+	}
+	free(branch);
+}
+
 static struct complete_reflogs *read_complete_reflog(const char *ref)
 {
 	struct complete_reflogs *reflogs =
@@ -178,13 +257,16 @@ void init_reflog_walk(struct reflog_walk_info **info)
 	(*info)->complete_reflogs.strdup_strings = 1;
 }
 
-void reflog_walk_mark(struct reflog_walk_info *info, bool operations)
+void reflog_walk_mark(struct reflog_walk_info *info, bool operations,
+		      bool historical_branch)
 {
 	struct string_list_item *item;
 
 	for_each_string_list_item(item, &info->complete_reflogs) {
 		if (operations)
 			mark_operations(item->util);
+		if (historical_branch)
+			mark_branches(item->util);
 	}
 }
 
@@ -287,6 +369,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,
@@ -300,12 +392,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;
 	}
@@ -340,6 +427,18 @@ void get_reflog_message(struct strbuf *sb,
 	strbuf_add(sb, info->message, len);
 }
 
+const char *get_reflog_branch(struct reflog_walk_info *reflog_info)
+{
+	struct commit_reflog *commit_reflog = reflog_info->last_commit_reflog;
+
+	if (!commit_reflog)
+		return NULL;
+
+	if (strcmp(commit_reflog->reflogs->ref, "HEAD"))
+		return short_ref(commit_reflog->reflogs);
+	return commit_reflog->reflogs->items[commit_reflog->recno+1].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 1d86795f84..223f4a0dd3 100644
--- a/reflog-walk.h
+++ b/reflog-walk.h
@@ -7,13 +7,15 @@ struct date_mode;
 
 void init_reflog_walk(struct reflog_walk_info **info);
 void reflog_walk_info_release(struct reflog_walk_info *info);
-void reflog_walk_mark(struct reflog_walk_info *info, bool operations);
+void reflog_walk_mark(struct reflog_walk_info *info, bool operations,
+		      bool historical_branch);
 int add_reflog_for_walk(struct reflog_walk_info *info,
 			struct commit *commit, const char *name);
 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);
+const char *get_reflog_branch(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/revision.c b/revision.c
index 88785651fe..0367ad9b5e 100644
--- a/revision.c
+++ b/revision.c
@@ -2622,6 +2622,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
 		revs->show_signature = 1;
 	} else if (!strcmp(arg, "--no-show-signature")) {
 		revs->show_signature = 0;
+	} else if (!strcmp(arg, "--historical-branch")) {
+		revs->historical_branch = 1;
 	} else if (!strcmp(arg, "--show-linear-break")) {
 		revs->break_bar = "                    ..........";
 		revs->track_linear = 1;
@@ -3249,11 +3251,15 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
 	if (!revs->reflog_info && revs->grep_filter.use_reflog_filter)
 		die(_("the option '%s' requires '%s'"), "--grep-reflog", "--walk-reflogs");
 
+	if (!revs->reflog_info && revs->historical_branch)
+		die(_("the option '%s' requires '%s'"), "--historical-branch", "--walk-reflogs");
+
 	if (!revs->reflog_info && revs->reflog_operations)
 		die(_("the option '%s' requires '%s'"), "--operations", "--walk-reflogs");
 
 	if (revs->reflog_info)
-		reflog_walk_mark(revs->reflog_info, revs->reflog_operations);
+		reflog_walk_mark(revs->reflog_info, revs->reflog_operations,
+				 revs->historical_branch);
 
 	if (revs->line_level_traverse &&
 	    (revs->full_diff ||
diff --git a/revision.h b/revision.h
index c4f5e06698..6a0fd120e5 100644
--- a/revision.h
+++ b/revision.h
@@ -269,6 +269,7 @@ struct rev_info {
 			show_notes_given:1,
 			show_notes_by_default:1,
 			show_signature:1,
+			historical_branch:1,
 			pretty_given:1,
 			abbrev_commit:1,
 			abbrev_commit_given:1,
diff --git a/t/t1414-reflog-walk.sh b/t/t1414-reflog-walk.sh
index e15f6f2381..eaf8187ba7 100755
--- a/t/t1414-reflog-walk.sh
+++ b/t/t1414-reflog-walk.sh
@@ -159,6 +159,52 @@ test_expect_success '--operations shows a rebase as one entry' '
 	)
 '
 
+test_expect_success '--historical-branch names the branch of each entry' '
+	(
+		cd operations &&
+
+		cat >expect <<-\EOF &&
+		(topic) HEAD@{0}: rebase (finish): returning to refs/heads/topic
+		(topic) HEAD@{1}: rebase (pick): C
+		(topic) HEAD@{2}: rebase (pick): B
+		(topic) HEAD@{3}: rebase (pick): A
+		(topic) HEAD@{4}: rebase (start): checkout main
+		(topic) HEAD@{5}: checkout: moving from main to topic
+		(main) HEAD@{6}: commit: upstream
+		(main) HEAD@{7}: checkout: moving from topic to main
+		(topic) HEAD@{8}: commit: C
+		(topic) HEAD@{9}: commit: B
+		(topic) HEAD@{10}: commit: A
+		(topic) HEAD@{11}: checkout: moving from main to topic
+		(main) HEAD@{12}: commit (initial): base
+		EOF
+		git reflog --historical-branch >out &&
+		cut -d " " -f 2- out >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '--historical-branch takes the place of decorations' '
+	(
+		cd operations &&
+		hash=$(git rev-parse --short HEAD) &&
+		echo "<YELLOW>$hash<RESET><YELLOW> (<RESET><BOLD;GREEN>topic<RESET><YELLOW>)<RESET>" >expect &&
+		git reflog --historical-branch --decorate --color -1 >out &&
+		test_decode_color <out | sed "s/ HEAD@.*//" >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '--historical-branch names the ref of other reflogs' '
+	(
+		cd operations &&
+		echo "(main) main@{0}: commit: upstream" >expect &&
+		git reflog -1 main --historical-branch >out &&
+		cut -d " " -f 2- out >actual &&
+		test_cmp expect actual
+	)
+'
+
 test_expect_success '--operations shows a stopped rebase as one entry once it finishes' '
 	set_fake_editor &&
 	(
@@ -231,8 +277,55 @@ test_expect_success '--operations shows a pull --rebase as one entry' '
 	)
 '
 
-test_expect_success '--operations requires --walk-reflogs' '
+test_expect_success '--historical-branch names the current branch when HEAD never switched' '
+	(
+		cd downstream &&
+		echo "(topic)" >expect &&
+		git reflog --historical-branch >out &&
+		cut -d " " -f 2 out | sort -u >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '--historical-branch follows a renamed branch' '
+	(
+		cd operations &&
+		test_commit D &&
+		git branch -m topic renamed &&
+		test_commit E &&
+
+		cat >expect <<-\EOF &&
+		(renamed) HEAD@{0}: commit: E
+		(renamed) HEAD@{1}: Branch: renamed refs/heads/topic to refs/heads/renamed
+		(topic) HEAD@{3}: commit: D
+		EOF
+		git reflog --historical-branch -3 >out &&
+		cut -d " " -f 2- out >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '--historical-branch shows where HEAD was detached' '
+	(
+		cd operations &&
+		git checkout --detach &&
+		test_commit F &&
+		git checkout renamed &&
+
+		cat >expect <<-EOF &&
+		(renamed) HEAD@{0}: checkout: moving from $(git rev-parse F) to renamed
+		(detached at $(git rev-parse --short F)) HEAD@{1}: commit: F
+		EOF
+		git reflog --historical-branch -2 >out &&
+		cut -d " " -f 2- out >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '--operations and --historical-branch require --walk-reflogs' '
 	test_must_fail git log --operations 2>err &&
+	test_grep "requires .--walk-reflogs." err &&
+	test_must_fail git log --historical-branch 2>err &&
 	test_grep "requires .--walk-reflogs." err
 '
 
-- 
gitgitgadget

      parent reply	other threads:[~2026-10-15 13:14 UTC|newest]

Thread overview: 4+ 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 13:13 ` [PATCH 2/3] reflog: add --operations to show one entry per command Harald Nordgren via GitGitGadget
2026-10-15 13:13 ` 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=4efe74b04e3c69b78caa4acd24c95a8cb64d0c57.1792070038.git.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=haraldnordgren@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