* [PATCH 0/3] reflog: add --operations and --historical-branch
@ 2026-10-15 13:13 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
` (5 more replies)
0 siblings, 6 replies; 17+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-10-15 13:13 UTC (permalink / raw)
To: git; +Cc: Harald Nordgren
Create a better reflog. Follows the discussion in
https://lore.kernel.org/git/akZfm-igZKeHaDST@pks.im/.
"git reflog" lists every step of a rebase and every branch switch, which
makes it hard to see what was done and on which branch. This adds two
options to "git reflog" and "git log -g":
* --operations shows one entry per command, folding a rebase into the entry
that finished or aborted it, and leaves out entries that change no
branch.
* --historical-branch decorates each entry with the branch HEAD was on when
it was recorded.
Harald Nordgren (3):
reflog: name the strings that mark switches, renames and rebase steps
reflog: add --operations to show one entry per command
reflog: add --historical-branch to name the branch of each entry
Documentation/git-reflog.adoc | 4 +
Documentation/rev-list-options.adoc | 19 +++
builtin/branch.c | 3 +-
builtin/checkout.c | 3 +-
builtin/rebase.c | 17 +--
log-tree.c | 21 +++
object-name.c | 3 +-
reflog-walk.c | 172 ++++++++++++++++++++++--
reflog-walk.h | 3 +
reflog.h | 6 +
revision.c | 14 ++
revision.h | 2 +
sequencer.c | 13 +-
t/t1414-reflog-walk.sh | 198 ++++++++++++++++++++++++++++
wt-status.c | 3 +-
15 files changed, 455 insertions(+), 26 deletions(-)
base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2448%2FHaraldNordgren%2Freflog-operations-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2448/HaraldNordgren/reflog-operations-v1
Pull-Request: https://github.com/git/git/pull/2448
--
gitgitgadget
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH 1/3] reflog: name the strings that mark switches, renames and rebase steps
2026-10-15 13:13 [PATCH 0/3] reflog: add --operations and --historical-branch Harald Nordgren via GitGitGadget
@ 2026-10-15 13:13 ` Harald Nordgren via GitGitGadget
2026-10-15 14:35 ` Kristoffer Haugsbakk
2026-10-15 13:13 ` [PATCH 2/3] reflog: add --operations to show one entry per command Harald Nordgren via GitGitGadget
` (4 subsequent siblings)
5 siblings, 1 reply; 17+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-10-15 13:13 UTC (permalink / raw)
To: git; +Cc: Harald Nordgren, Harald Nordgren
From: Harald Nordgren <haraldnordgren@gmail.com>
"git checkout" records a move of HEAD in the reflog as "checkout:
moving from <old> to <new>", "git branch -m" records a rename of the
current branch as "Branch: renamed <old> to <new>", and "git rebase"
marks where it starts and ends with steps like "rebase (start)" and
"rebase (finish)". "@{-<n>}" and "git status" read the switch message
back, each with its own copy of the prefix, and a later commit adds a
reader for all of them.
Name these strings in reflog.h and use them where the messages are
written and read, so they cannot drift apart.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
builtin/branch.c | 3 ++-
builtin/checkout.c | 3 ++-
builtin/rebase.c | 17 +++++++++--------
object-name.c | 3 ++-
reflog.h | 6 ++++++
sequencer.c | 13 +++++++------
wt-status.c | 3 ++-
7 files changed, 30 insertions(+), 18 deletions(-)
diff --git a/builtin/branch.c b/builtin/branch.c
index a613148fc7..30062ceb7c 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -13,6 +13,7 @@
#include "editor.h"
#include "environment.h"
#include "refs.h"
+#include "reflog.h"
#include "commit.h"
#include "gettext.h"
#include "object-name.h"
@@ -669,7 +670,7 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int
strbuf_addf(&logmsg, "Branch: copied %s to %s",
oldref.buf, newref.buf);
else
- strbuf_addf(&logmsg, "Branch: renamed %s to %s",
+ strbuf_addf(&logmsg, REFLOG_RENAME_PREFIX "%s to %s",
oldref.buf, newref.buf);
if (!copy && !(oldref_usage & IS_ORPHAN) &&
diff --git a/builtin/checkout.c b/builtin/checkout.c
index c0f0d2c700..9f0f09e50f 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -25,6 +25,7 @@
#include "preload-index.h"
#include "read-cache.h"
#include "refs.h"
+#include "reflog.h"
#include "remote.h"
#include "repo-settings.h"
#include "resolve-undo.h"
@@ -999,7 +1000,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
reflog_msg = getenv("GIT_REFLOG_ACTION");
if (!reflog_msg)
- strbuf_addf(&msg, "checkout: moving from %s to %s",
+ strbuf_addf(&msg, REFLOG_SWITCH_PREFIX "%s to %s",
old_desc ? old_desc : "(invalid)", new_branch_info->name);
else
strbuf_insertstr(&msg, 0, reflog_msg);
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 535db60181..26ef589e89 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -17,6 +17,7 @@
#include "strvec.h"
#include "dir.h"
#include "refs.h"
+#include "reflog.h"
#include "config.h"
#include "unpack-trees.h"
#include "lockfile.h"
@@ -601,11 +602,11 @@ static int move_to_original_branch(struct rebase_options *opts)
if (!opts->onto)
BUG("move_to_original_branch without onto");
- strbuf_addf(&branch_reflog, "%s (finish): %s onto %s",
- opts->reflog_action,
+ strbuf_addf(&branch_reflog, "%s (%s): %s onto %s",
+ opts->reflog_action, REFLOG_STEP_FINISH,
opts->head_name, oid_to_hex(&opts->onto->object.oid));
- strbuf_addf(&head_reflog, "%s (finish): returning to %s",
- opts->reflog_action, opts->head_name);
+ strbuf_addf(&head_reflog, "%s (%s): returning to %s",
+ opts->reflog_action, REFLOG_STEP_FINISH, opts->head_name);
ropts.branch = opts->head_name;
ropts.flags = RESET_WORKING_TREE_REFS_ONLY |
RESET_WORKING_TREE_UPDATE_HEAD;
@@ -1413,8 +1414,8 @@ int cmd_rebase(int argc,
if (read_basic_state(&options))
exit(1);
- strbuf_addf(&head_msg, "%s (abort): returning to %s",
- options.reflog_action,
+ strbuf_addf(&head_msg, "%s (%s): returning to %s",
+ options.reflog_action, REFLOG_STEP_ABORT,
options.head_name ? options.head_name
: oid_to_hex(&options.orig_head->object.oid));
ropts.oid = &options.orig_head->object.oid;
@@ -1884,8 +1885,8 @@ int cmd_rebase(int argc,
printf(_("First, rewinding head to replay your work on top of "
"it...\n"));
- strbuf_addf(&msg, "%s (start): checkout %s",
- options.reflog_action, options.onto_name);
+ strbuf_addf(&msg, "%s (%s): checkout %s", options.reflog_action,
+ REFLOG_STEP_START, options.onto_name);
ropts.oid = &options.onto->object.oid;
ropts.orig_head = &options.orig_head->object.oid;
ropts.flags = RESET_WORKING_TREE_DETACH |
diff --git a/object-name.c b/object-name.c
index 4eda8c8eac..bb349014ee 100644
--- a/object-name.c
+++ b/object-name.c
@@ -13,6 +13,7 @@
#include "tree.h"
#include "tree-walk.h"
#include "refs.h"
+#include "reflog.h"
#include "remote.h"
#include "dir.h"
#include "odb.h"
@@ -1251,7 +1252,7 @@ static int grab_nth_branch_switch(const char *refname UNUSED,
const char *match = NULL, *target = NULL;
size_t len;
- if (skip_prefix(message, "checkout: moving from ", &match))
+ if (skip_prefix(message, REFLOG_SWITCH_PREFIX, &match))
target = strstr(match, " to ");
if (!match || !target)
diff --git a/reflog.h b/reflog.h
index 3bdd1ca8a5..32c3737db4 100644
--- a/reflog.h
+++ b/reflog.h
@@ -2,6 +2,12 @@
#define REFLOG_H
#include "refs.h"
+#define REFLOG_SWITCH_PREFIX "checkout: moving from "
+#define REFLOG_RENAME_PREFIX "Branch: renamed "
+#define REFLOG_STEP_START "start"
+#define REFLOG_STEP_FINISH "finish"
+#define REFLOG_STEP_ABORT "abort"
+
#define REFLOG_EXPIRE_TOTAL (1 << 0)
#define REFLOG_EXPIRE_UNREACH (1 << 1)
diff --git a/sequencer.c b/sequencer.c
index efbfd1315b..c55df2d38f 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -31,6 +31,7 @@
#include "merge-ort.h"
#include "merge-ort-wrappers.h"
#include "refs.h"
+#include "reflog.h"
#include "sparse-index.h"
#include "strvec.h"
#include "quote.h"
@@ -5011,8 +5012,8 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,
RESET_WORKING_TREE_UPDATE_HEAD |
RESET_WORKING_TREE_UPDATE_ORIG_HEAD |
RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK,
- .head_msg = reflog_message(opts, "start", "checkout %s",
- onto_name),
+ .head_msg = reflog_message(opts, REFLOG_STEP_START,
+ "checkout %s", onto_name),
.default_reflog_action = sequencer_reflog_action(opts)
};
if (reset_working_tree(r, &ropts)) {
@@ -5310,16 +5311,16 @@ cleanup_head_ref:
res = error(_("could not read 'onto'"));
goto cleanup_head_ref;
}
- msg = reflog_message(opts, "finish", "%s onto %s",
- head_ref.buf, buf.buf);
+ msg = reflog_message(opts, REFLOG_STEP_FINISH,
+ "%s onto %s", head_ref.buf, buf.buf);
if (refs_update_ref(get_main_ref_store(the_repository), msg, head_ref.buf, &head, &orig,
REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
res = error(_("could not update %s"),
head_ref.buf);
goto cleanup_head_ref;
}
- msg = reflog_message(opts, "finish", "returning to %s",
- head_ref.buf);
+ msg = reflog_message(opts, REFLOG_STEP_FINISH,
+ "returning to %s", head_ref.buf);
if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
res = error(_("could not update HEAD to %s"),
head_ref.buf);
diff --git a/wt-status.c b/wt-status.c
index 57772c7501..ffd54353a8 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -22,6 +22,7 @@
#include "strvec.h"
#include "remote.h"
#include "refs.h"
+#include "reflog.h"
#include "submodule.h"
#include "column.h"
#include "read-cache.h"
@@ -1790,7 +1791,7 @@ static int grab_1st_switch(const char *refname UNUSED,
struct grab_1st_switch_cbdata *cb = cb_data;
const char *target = NULL, *end;
- if (!skip_prefix(message, "checkout: moving from ", &message))
+ if (!skip_prefix(message, REFLOG_SWITCH_PREFIX, &message))
return 0;
target = strstr(message, " to ");
if (!target)
--
gitgitgadget
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH 2/3] reflog: add --operations to show one entry per command
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 ` 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
` (3 subsequent siblings)
5 siblings, 0 replies; 17+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-10-15 13:13 UTC (permalink / raw)
To: git; +Cc: Harald Nordgren, Harald Nordgren
From: Harald Nordgren <haraldnordgren@gmail.com>
After a rebase, "git reflog" shows one entry for every commit the
rebase replayed, plus one for its start and one for its end. Together
with an entry for every branch switch, finding where a branch was
before the rebase means reading past all of them.
Teach "git reflog" and "git log -g" an "--operations" option that
shows each rebase as the single entry that finished or aborted it, and
leaves out entries that change no branch, like branch switches and the
"reset: moving to HEAD" that "git stash" and "git merge --abort" leave
behind:
$ git reflog --operations
4e5f6a7 HEAD@{0}: rebase (finish): returning to refs/heads/topic
8c9d0e1 HEAD@{5}: commit: fix typo
1a2b3c4 HEAD@{7}: commit: add tests
The HEAD@{<n>} numbers keep their usual values, so the previous entry
on the same branch can be used to undo the rebase:
$ git reset --hard HEAD@{7}
Commands run while a rebase is stopped, such as an amend after an
"edit", are folded into that rebase, and a "git pull --rebase" is shown
as one entry the same way.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
Documentation/git-reflog.adoc | 3 +
Documentation/rev-list-options.adoc | 11 +++
reflog-walk.c | 61 +++++++++++++++-
reflog-walk.h | 1 +
revision.c | 8 +++
revision.h | 1 +
t/t1414-reflog-walk.sh | 105 ++++++++++++++++++++++++++++
7 files changed, 188 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc
index 38af0c977a..672b9ea72e 100644
--- a/Documentation/git-reflog.adoc
+++ b/Documentation/git-reflog.adoc
@@ -41,6 +41,9 @@ command-line (or `HEAD`, by default). The reflog covers all recent
actions, and in addition the `HEAD` reflog records branch switching.
`git reflog show` is an alias for `git log -g --abbrev-commit
--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.
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 bd9f345690..a40ba2641b 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -348,6 +348,17 @@ See also linkgit:git-reflog[1].
+
Under `--pretty=reference`, this information will not be shown at all.
+`--operations`::
+ Show one reflog entry for each command that changed a branch.
+ A rebase is shown as only the entry that finished or aborted it,
+ including any commands run while it was stopped, such as
+ `git commit --amend`. Switching branches is not shown, and
+ neither is a reset that left `HEAD` where it was, such as the one
+ `git stash` does.
+ The previous entry on the same branch is the state before the
+ rebase started. 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/reflog-walk.c b/reflog-walk.c
index 4dbeaa93a7..82528f5b66 100644
--- a/reflog-walk.c
+++ b/reflog-walk.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "commit.h"
#include "refs.h"
+#include "reflog.h"
#include "diff.h"
#include "repository.h"
#include "revision.h"
@@ -18,6 +19,7 @@ struct complete_reflogs {
timestamp_t timestamp;
int tz;
char *message;
+ bool user_operation;
} *items;
int nr, alloc;
};
@@ -38,6 +40,7 @@ static int read_one_reflog(const char *refname UNUSED,
item->timestamp = timestamp;
item->tz = tz;
item->message = xstrdup(message);
+ item->user_operation = true;
array->nr++;
return 0;
}
@@ -65,6 +68,46 @@ static void complete_reflogs_clear(void *util, const char *str UNUSED)
free_complete_reflog(array);
}
+static bool has_rebase_step(const char *message, const char *step)
+{
+ size_t end = strcspn(message, ":\n");
+ size_t len = strlen(step);
+
+ return end >= len + 3 && message[end - 1] == ')' &&
+ !strncmp(message + end - len - 3, " (", 2) &&
+ !strncmp(message + end - len - 1, step, len);
+}
+
+static void mark_operations(struct complete_reflogs *array)
+{
+ int start = -1;
+
+ for (int i = 0; i < array->nr; i++) {
+ struct reflog_info *item = &array->items[i];
+ const char *msg = item->message;
+
+ if (has_rebase_step(msg, REFLOG_STEP_FINISH) ||
+ has_rebase_step(msg, REFLOG_STEP_ABORT)) {
+ if (start >= 0)
+ for (int j = start; j < i; j++)
+ array->items[j].user_operation = false;
+ start = -1;
+ item->user_operation = true;
+ continue;
+ }
+ if (has_rebase_step(msg, REFLOG_STEP_START))
+ start = i;
+
+ if (starts_with(msg, REFLOG_SWITCH_PREFIX))
+ item->user_operation = false;
+ else if (starts_with(msg, REFLOG_RENAME_PREFIX))
+ item->user_operation = true;
+ else
+ item->user_operation =
+ !oideq(&item->ooid, &item->noid);
+ }
+}
+
static struct complete_reflogs *read_complete_reflog(const char *ref)
{
struct complete_reflogs *reflogs =
@@ -135,6 +178,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)
+{
+ struct string_list_item *item;
+
+ for_each_string_list_item(item, &info->complete_reflogs) {
+ if (operations)
+ mark_operations(item->util);
+ }
+}
+
void reflog_walk_info_release(struct reflog_walk_info *info)
{
size_t i;
@@ -342,8 +395,12 @@ static struct commit *next_reflog_commit(struct commit_reflog *log)
{
for (; log->recno >= 0; log->recno--) {
struct reflog_info *entry = &log->reflogs->items[log->recno];
- struct object *obj = parse_object(the_repository,
- &entry->noid);
+ struct object *obj;
+
+ if (!entry->user_operation)
+ continue;
+
+ obj = parse_object(the_repository, &entry->noid);
if (obj && obj->type == OBJ_COMMIT)
return (struct commit *)obj;
diff --git a/reflog-walk.h b/reflog-walk.h
index 989583dc55..1d86795f84 100644
--- a/reflog-walk.h
+++ b/reflog-walk.h
@@ -7,6 +7,7 @@ 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);
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,
diff --git a/revision.c b/revision.c
index a83e499047..88785651fe 100644
--- a/revision.c
+++ b/revision.c
@@ -2458,6 +2458,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
commit_list_insert(c, &revs->ancestry_path_bottoms);
} else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
init_reflog_walk(&revs->reflog_info);
+ } else if (!strcmp(arg, "--operations")) {
+ revs->reflog_operations = 1;
} else if (!strcmp(arg, "--default")) {
if (argc <= 1)
return error("bad --default argument");
@@ -3247,6 +3249,12 @@ 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->reflog_operations)
+ die(_("the option '%s' requires '%s'"), "--operations", "--walk-reflogs");
+
+ if (revs->reflog_info)
+ reflog_walk_mark(revs->reflog_info, revs->reflog_operations);
+
if (revs->line_level_traverse &&
(revs->full_diff ||
(revs->diffopt.output_format &
diff --git a/revision.h b/revision.h
index e5dabd18ce..c4f5e06698 100644
--- a/revision.h
+++ b/revision.h
@@ -221,6 +221,7 @@ struct rev_info {
exclude_first_parent_only:1,
line_level_traverse:1,
tree_blobs_in_commit_order:1,
+ reflog_operations:1,
/*
* Blobs are shown without regard for their existence.
diff --git a/t/t1414-reflog-walk.sh b/t/t1414-reflog-walk.sh
index be6c3f472c..e15f6f2381 100755
--- a/t/t1414-reflog-walk.sh
+++ b/t/t1414-reflog-walk.sh
@@ -5,6 +5,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
test_expect_success 'set up some reflog entries' '
test_commit one &&
@@ -131,6 +132,110 @@ test_expect_success 'walk prefers reflog to ref tip' '
test_cmp expect actual
'
+test_expect_success '--operations shows a rebase as one entry' '
+ git init operations &&
+ (
+ cd operations &&
+ test_commit base &&
+ git switch -c topic &&
+ test_commit A &&
+ test_commit B &&
+ test_commit C &&
+ git switch main &&
+ test_commit upstream &&
+ git switch topic &&
+ git rebase main &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} rebase (finish): returning to refs/heads/topic
+ HEAD@{6} commit: upstream
+ HEAD@{8} commit: C
+ HEAD@{9} commit: B
+ HEAD@{10} commit: A
+ HEAD@{12} commit (initial): base
+ EOF
+ git log -g --format="%gd %gs" HEAD --operations >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations shows a stopped rebase as one entry once it finishes' '
+ set_fake_editor &&
+ (
+ cd operations &&
+ FAKE_LINES="edit 1 2" git rebase -i HEAD~2 &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} rebase: fast-forward
+ HEAD@{1} rebase (start): checkout HEAD~2
+ EOF
+ git log -g --operations --format="%gd %gs" -2 >actual &&
+ test_cmp expect actual &&
+
+ git commit --amend -m "B amended" &&
+ git rebase --continue &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} rebase (finish): returning to refs/heads/topic
+ HEAD@{5} rebase (finish): returning to refs/heads/topic
+ EOF
+ git log -g --operations --format="%gd %gs" -2 >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations shows an aborted rebase as one entry' '
+ (
+ cd operations &&
+ FAKE_LINES="edit 1 2" git rebase -i HEAD~2 &&
+ git rebase --abort &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} rebase (abort): returning to refs/heads/topic
+ HEAD@{3} rebase (finish): returning to refs/heads/topic
+ EOF
+ git log -g --operations --format="%gd %gs" -2 >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations leaves out the reset done by git stash' '
+ (
+ cd operations &&
+ echo change >>C.t &&
+ git stash &&
+
+ echo "HEAD@{1} rebase (abort): returning to refs/heads/topic" >expect &&
+ git log -g --operations --format="%gd %gs" -1 >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations shows a pull --rebase as one entry' '
+ git clone operations downstream &&
+ (
+ cd operations &&
+ test_commit upstream-change
+ ) &&
+ (
+ cd downstream &&
+ test_commit local-change &&
+ git pull --rebase &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} pull --rebase (finish): returning to refs/heads/topic
+ HEAD@{3} commit: local-change
+ EOF
+ git log -g --operations --format="%gd %gs" -2 >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations requires --walk-reflogs' '
+ test_must_fail git log --operations 2>err &&
+ test_grep "requires .--walk-reflogs." err
+'
+
test_expect_success 'rev-list -g complains when there are no reflogs' '
test_must_fail git rev-list -g
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH 3/3] reflog: add --historical-branch to name the branch of each entry
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
2026-10-15 14:36 ` [PATCH 0/3] reflog: add --operations and --historical-branch Kristoffer Haugsbakk
` (2 subsequent siblings)
5 siblings, 0 replies; 17+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-10-15 13:13 UTC (permalink / raw)
To: git; +Cc: Harald Nordgren, Harald Nordgren
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
^ permalink raw reply related [flat|nested] 17+ messages in thread
* Re: [PATCH 1/3] reflog: name the strings that mark switches, renames and rebase steps
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
0 siblings, 1 reply; 17+ messages in thread
From: Kristoffer Haugsbakk @ 2026-10-15 14:35 UTC (permalink / raw)
To: git, GGG; +Cc: Harald Nordgren
On Thu, Oct 15, 2026, at 15:13, Harald Nordgren via GitGitGadget wrote:
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> "git checkout" records a move of HEAD in the reflog as "checkout:
> moving from <old> to <new>", "git branch -m" records a rename of the
> current branch as "Branch: renamed <old> to <new>", and "git rebase"
> marks where it starts and ends with steps like "rebase (start)" and
> "rebase (finish)". "@{-<n>}" and "git status" read the switch message
> back, each with its own copy of the prefix,
Here it feels like we are thrown into an enumeration without knowing
what for. What is the problem to solve?
> and a later commit adds a
> reader for all of them.
This clause at the end of the six-line sentence seems to reveal why: we
are about to use these strings in more than one place, so we want to
introduce constants in order to not duplicate anything (no drifting
apart). But the why here should have been put at the start. And the
phrasing is also so passive that I had to do a double-take as to what
“commit” meant. A commit in the reflog under discussion? Or a commit
that *we*/*I* are about to make?
The context is that you are about to use these prefixes in more than one
place. And the principle you are following is no duplication. Then the
details follow. If the details of exactly what they say is worth stating
in the commit message.
>
> Name these strings in reflog.h and use them where the messages are
> written and read, so they cannot drift apart.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
>[snip]
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH 0/3] reflog: add --operations and --historical-branch
2026-10-15 13:13 [PATCH 0/3] reflog: add --operations and --historical-branch Harald Nordgren via GitGitGadget
` (2 preceding siblings ...)
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 ` Kristoffer Haugsbakk
2026-10-15 15:24 ` Harald Nordgren
2026-10-16 18:36 ` D. Ben Knoble
2026-10-18 18:27 ` [PATCH v2 " Harald Nordgren via GitGitGadget
5 siblings, 1 reply; 17+ messages in thread
From: Kristoffer Haugsbakk @ 2026-10-15 14:36 UTC (permalink / raw)
To: git, GGG; +Cc: Harald Nordgren
On Thu, Oct 15, 2026, at 15:13, Harald Nordgren via GitGitGadget wrote:
> Create a better reflog. Follows the discussion in
> https://lore.kernel.org/git/akZfm-igZKeHaDST@pks.im/.
>
> "git reflog" lists every step of a rebase and every branch switch, which
> makes it hard to see what was done and on which branch. This adds two
> options to "git reflog" and "git log -g":
>
> * --operations shows one entry per command, folding a rebase into the entry
> that finished or aborted it, and leaves out entries that change no
> branch.
> * --historical-branch decorates each entry with the branch HEAD was on when
> it was recorded.
This sounds very useful. That you rebase many commits and then the reset
points (where each one started and ended) get obscured in the reflog is
classic for me. And I think many have had the same issue. This is why I
sometimes use `ORIG_HEAD` instead, or maybe make a backup ref.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH 0/3] reflog: add --operations and --historical-branch
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-16 18:26 ` D. Ben Knoble
0 siblings, 2 replies; 17+ messages in thread
From: Harald Nordgren @ 2026-10-15 15:24 UTC (permalink / raw)
To: Kristoffer Haugsbakk; +Cc: git, GGG
> This sounds very useful. That you rebase many commits and then the reset
> points (where each one started and ended) get obscured in the reflog is
> classic for me. And I think many have had the same issue. This is why I
> sometimes use `ORIG_HEAD` instead, or maybe make a backup ref.
Yes! I love the reflog. But I'm also endlessly confused by it.
Sometimes I'll just pick all the commits one by one and do 'git show'
on them to figure out which one to actually pick. Not a great
experience.
Harald
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH 0/3] reflog: add --operations and --historical-branch
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
1 sibling, 1 reply; 17+ messages in thread
From: Kristoffer Haugsbakk @ 2026-10-15 15:35 UTC (permalink / raw)
To: Harald Nordgren; +Cc: git, GGGGGG
On Thu, Oct 15, 2026, at 17:24, Harald Nordgren wrote:
>> This sounds very useful. That you rebase many commits and then the reset
>> points (where each one started and ended) get obscured in the reflog is
>> classic for me. And I think many have had the same issue. This is why I
>> sometimes use `ORIG_HEAD` instead, or maybe make a backup ref.
>
> Yes! I love the reflog. But I'm also endlessly confused by it.
>
> Sometimes I'll just pick all the commits one by one and do 'git show'
> on them to figure out which one to actually pick. Not a great
> experience.
:)
I got a bit curious about this forced workflow now. I tried the first
terminal navigate-and-click Git tool I could think of, tig(1), and it
turns out that you can do this:
$ tig reflog
$ # navigate up/down j/k
$ # See the commit message and diff with `d`
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH 0/3] reflog: add --operations and --historical-branch
2026-10-15 15:35 ` Kristoffer Haugsbakk
@ 2026-10-15 20:47 ` Harald Nordgren
0 siblings, 0 replies; 17+ messages in thread
From: Harald Nordgren @ 2026-10-15 20:47 UTC (permalink / raw)
To: Kristoffer Haugsbakk; +Cc: git, GGGGGG
> > Sometimes I'll just pick all the commits one by one and do 'git show'
> > on them to figure out which one to actually pick. Not a great
> > experience.
>
> :)
>
> I got a bit curious about this forced workflow now. I tried the first
> terminal navigate-and-click Git tool I could think of, tig(1), and it
> turns out that you can do this:
>
> $ tig reflog
> $ # navigate up/down j/k
> $ # See the commit message and diff with `d`
I don't use tig, lazygit, github desktop or vscode’s version control
extension, not because they’re not good (they probably are) but only
because I don't want to be removed from the fundamentals. I want to
feel the pain of using git :)
But that seems like an improvement, yes, thanks for pointing it out!
Good news is that git options are passed through to tig, so with this
new feature we could run:
tig reflog --operations
Harald
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH 1/3] reflog: name the strings that mark switches, renames and rebase steps
2026-10-15 14:35 ` Kristoffer Haugsbakk
@ 2026-10-15 20:52 ` Harald Nordgren
0 siblings, 0 replies; 17+ messages in thread
From: Harald Nordgren @ 2026-10-15 20:52 UTC (permalink / raw)
To: Kristoffer Haugsbakk; +Cc: git, GGG
> [snip]
>
> The context is that you are about to use these prefixes in more than one
> place. And the principle you are following is no duplication. Then the
> details follow. If the details of exactly what they say is worth stating
> in the commit message.
Thanks!
Harald
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH 0/3] reflog: add --operations and --historical-branch
2026-10-15 15:24 ` Harald Nordgren
2026-10-15 15:35 ` Kristoffer Haugsbakk
@ 2026-10-16 18:26 ` D. Ben Knoble
1 sibling, 0 replies; 17+ messages in thread
From: D. Ben Knoble @ 2026-10-16 18:26 UTC (permalink / raw)
To: Harald Nordgren; +Cc: Kristoffer Haugsbakk, git, GGG
On Thu, Oct 15, 2026 at 12:04 PM Harald Nordgren
<haraldnordgren@gmail.com> wrote:
>
> > This sounds very useful. That you rebase many commits and then the reset
> > points (where each one started and ended) get obscured in the reflog is
> > classic for me. And I think many have had the same issue. This is why I
> > sometimes use `ORIG_HEAD` instead, or maybe make a backup ref.
>
> Yes! I love the reflog. But I'm also endlessly confused by it.
>
> Sometimes I'll just pick all the commits one by one and do 'git show'
> on them to figure out which one to actually pick. Not a great
> experience.
Perhaps `git log -g -p` in that case? :)
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH 0/3] reflog: add --operations and --historical-branch
2026-10-15 13:13 [PATCH 0/3] reflog: add --operations and --historical-branch Harald Nordgren via GitGitGadget
` (3 preceding siblings ...)
2026-10-15 14:36 ` [PATCH 0/3] reflog: add --operations and --historical-branch Kristoffer Haugsbakk
@ 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
5 siblings, 1 reply; 17+ messages in thread
From: D. Ben Knoble @ 2026-10-16 18:36 UTC (permalink / raw)
To: Harald Nordgren via GitGitGadget; +Cc: git, Harald Nordgren
[Apologies for replying in the wrong chain; it appears I haven't
received 3/3 yet and it's easier for me to reply to a message I have
than one I can only see in lore.]
On Thu, Oct 15, 2026 at 9:20 AM Harald Nordgren via GitGitGadget
<gitgitgadget@gmail.com> wrote:
> reflog: add --historical-branch to name the branch of each entry
> 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
Notice how the "HEAD@" column is now not aligned? I find it really convient to
have fixed-width data at the front of the line so that I can visually pick out
pieces in columns.
So I'd suggest an alternat end-of-line format:
$ git reflog --historical-branch
4e5f6a7 HEAD@{0}: commit: add tests (topic)
1a2b3c4 HEAD@{1}: checkout: moving from main to topic (topic)
8c9d0e1 HEAD@{2}: commit: fix typo (main)
That might make picking out the branch name a little harder, so I suppose you
could try right before the messages?
$ git reflog --historical-branch
4e5f6a7 HEAD@{0}: (topic) commit: add tests
1a2b3c4 HEAD@{1}: (topic) checkout: moving from main to topic
8c9d0e1 HEAD@{2}: (main) commit: fix typo
or (since the "operation" prefixes" are useful)
$ git reflog --historical-branch
4e5f6a7 HEAD@{0}: commit: (topic) add tests
1a2b3c4 HEAD@{1}: checkout: (topic) moving from main to topic
8c9d0e1 HEAD@{2}: commit: (main) fix typo
? Obviously the misalignment in all the examples (except the EOL format) gets
worse when you have some short and some long branch names.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH 0/3] reflog: add --operations and --historical-branch
2026-10-16 18:36 ` D. Ben Knoble
@ 2026-10-17 10:03 ` Harald Nordgren
0 siblings, 0 replies; 17+ messages in thread
From: Harald Nordgren @ 2026-10-17 10:03 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: Harald Nordgren via GitGitGadget, git
> $ git reflog --historical-branch
> 4e5f6a7 HEAD@{0}: (topic) commit: add tests
> 1a2b3c4 HEAD@{1}: (topic) checkout: moving from main to topic
> 8c9d0e1 HEAD@{2}: (main) commit: fix typo
Thanks!
This is my favorite then.
Harald
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v2 0/3] reflog: add --operations and --historical-branch
2026-10-15 13:13 [PATCH 0/3] reflog: add --operations and --historical-branch Harald Nordgren via GitGitGadget
` (4 preceding siblings ...)
2026-10-16 18:36 ` D. Ben Knoble
@ 2026-10-18 18:27 ` 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
` (2 more replies)
5 siblings, 3 replies; 17+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-10-18 18:27 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, D. Ben Knoble, Harald Nordgren
Create a better reflog. Follows the discussion in
https://lore.kernel.org/git/akZfm-igZKeHaDST@pks.im/.
"git reflog" lists every step of a rebase and every branch switch, which
makes it hard to see what was done and on which branch. This adds two
options to "git reflog" and "git log -g":
* --operations shows one entry per command, folding a rebase into the entry
that finished or aborted it, and leaves out entries that change no
branch.
* --historical-branch decorates each entry with the branch HEAD was on when
it was recorded.
Changes in v2:
* Move the "historical branch" rightward in the display for cleaner
alignment.
* Improve commit messages.
Harald Nordgren (3):
reflog: name the strings that mark switches, renames and rebase steps
reflog: add --operations to show one entry per command
reflog: add --historical-branch to name the branch of each entry
Documentation/git-reflog.adoc | 4 +
Documentation/rev-list-options.adoc | 21 +++
builtin/branch.c | 3 +-
builtin/checkout.c | 3 +-
builtin/rebase.c | 17 +--
log-tree.c | 28 +++-
object-name.c | 3 +-
reflog-walk.c | 183 +++++++++++++++++++++++--
reflog-walk.h | 6 +-
reflog.h | 6 +
revision.c | 14 ++
revision.h | 2 +
sequencer.c | 13 +-
t/t1414-reflog-walk.sh | 198 ++++++++++++++++++++++++++++
wt-status.c | 3 +-
15 files changed, 471 insertions(+), 33 deletions(-)
base-commit: c51408889c3b372814bc20b9431c3b74d0d268eb
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2448%2FHaraldNordgren%2Freflog-operations-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2448/HaraldNordgren/reflog-operations-v2
Pull-Request: https://github.com/git/git/pull/2448
Range-diff vs v1:
1: d95ed5bd67 ! 1: 745929bd80 reflog: name the strings that mark switches, renames and rebase steps
@@ Metadata
## Commit message ##
reflog: name the strings that mark switches, renames and rebase steps
- "git checkout" records a move of HEAD in the reflog as "checkout:
- moving from <old> to <new>", "git branch -m" records a rename of the
- current branch as "Branch: renamed <old> to <new>", and "git rebase"
- marks where it starts and ends with steps like "rebase (start)" and
- "rebase (finish)". "@{-<n>}" and "git status" read the switch message
- back, each with its own copy of the prefix, and a later commit adds a
- reader for all of them.
+ The next commit adds "git reflog --operations", which finds branch
+ switches, branch renames and the start and end of a rebase by the
+ messages they leave in the reflog. The commands that write these
+ messages spell out the strings by hand, and "@{-<n>}" and "git status"
+ each keep their own copy of the switch prefix to read it back. Reading
+ them in one more place would add yet another copy that has to stay in
+ sync.
- Name these strings in reflog.h and use them where the messages are
- written and read, so they cannot drift apart.
+ Name the strings once in reflog.h and use the names wherever the
+ 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 written
+ by "git rebase".
+
+ No change in behavior.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
2: 579e13b36a = 2: 1c47ba225e reflog: add --operations to show one entry per command
3: 4efe74b04e ! 3: aa5a98b80d reflog: add --historical-branch to name the branch of each entry
@@ Commit message
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:
+ that shows the branch each entry was made on before its message, in
+ place of the refs that point at the commit now. The hashes and
+ selectors stay lined up in their columns, and 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
+ 4e5f6a7 HEAD@{0}: (topic) commit: add tests
+ 1a2b3c4 HEAD@{1}: (topic) checkout: moving from main to topic
+ 8c9d0e1 HEAD@{2}: (main) 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,
@@ Documentation/git-reflog.adoc: actions, and in addition the `HEAD` reflog record
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.
++each entry shows the branch it was made on before its message.
The "list" subcommand lists all refs which have a corresponding reflog.
@@ Documentation/rev-list-options.adoc: Under `--pretty=reference`, this informatio
rebase started. It is an error to use this option unless
`--walk-reflogs` is in use.
++ifndef::git-rev-list[]
+`--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.
++ Show the branch `HEAD` was on when each reflog entry was
++ recorded, such as `(topic)`, before the reflog message, 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.
++endif::git-rev-list[]
+
`--merge`::
Show commits touching conflicted paths in the range `HEAD...<other>`,
@@ log-tree.c: void format_decorations(struct strbuf *sb,
}
}
-+static void show_historical_branch(struct rev_info *opt)
++static void format_historical_branch(struct strbuf *sb,
++ struct rev_info *opt)
+{
+ const char *branch = get_reflog_branch(opt->reflog_info);
+ enum git_colorbool use_color = opt->diffopt.use_color;
@@ log-tree.c: void format_decorations(struct strbuf *sb,
+
+ 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);
++ strbuf_addf(sb, "%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)
@@ log-tree.c: 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)
+- if (!opt->show_decorations)
++ if (!opt->show_decorations || opt->historical_branch)
return;
format_decorations(&sb, commit, opt->diffopt.use_color, NULL);
+ fputs(sb.buf, opt->diffopt.file);
+@@ log-tree.c: void show_log(struct rev_info *opt)
+ graph_show_oneline(opt->graph);
+ }
+ if (opt->reflog_info) {
++ struct strbuf branch = STRBUF_INIT;
++
+ /*
+ * setup_revisions() ensures that opt->reflog_info
+ * and opt->graph cannot both be set,
+ * so we don't need to worry about printing the
+ * graph info here.
+ */
++ if (opt->historical_branch)
++ format_historical_branch(&branch, opt);
+ show_reflog_message(opt->reflog_info,
+ opt->commit_format == CMIT_FMT_ONELINE,
+ opt->date_mode,
+- opt->date_mode_explicit);
++ opt->date_mode_explicit,
++ branch.buf);
++ strbuf_release(&branch);
+ if (opt->commit_format == CMIT_FMT_ONELINE)
+ return;
+ }
## reflog-walk.c ##
@@
@@ reflog-walk.c: void get_reflog_message(struct strbuf *sb,
const char *get_reflog_ident(struct reflog_walk_info *reflog_info)
{
struct commit_reflog *commit_reflog = reflog_info->last_commit_reflog;
+@@ reflog-walk.c: timestamp_t get_reflog_timestamp(struct reflog_walk_info *reflog_info)
+ }
+
+ void show_reflog_message(struct reflog_walk_info *reflog_info, int oneline,
+- struct date_mode dmode, int force_date)
++ struct date_mode dmode, int force_date,
++ const char *branch)
+ {
+ if (reflog_info && reflog_info->last_commit_reflog) {
+ struct commit_reflog *commit_reflog = reflog_info->last_commit_reflog;
+@@ reflog-walk.c: void show_reflog_message(struct reflog_walk_info *reflog_info, int oneline,
+ info = &commit_reflog->reflogs->items[commit_reflog->recno+1];
+ get_reflog_selector(&selector, reflog_info, dmode, force_date, 0);
+ if (oneline) {
+- printf("%s: %s", selector.buf, info->message);
++ printf("%s: %s%s", selector.buf, branch,
++ info->message);
+ }
+ else {
+- printf("Reflog: %s (%s)\nReflog message: %s",
+- selector.buf, info->email, info->message);
++ printf("Reflog: %s (%s)\nReflog message: %s%s",
++ selector.buf, info->email, branch,
++ info->message);
+ }
+
+ strbuf_release(&selector);
## reflog-walk.h ##
@@ reflog-walk.h: struct date_mode;
@@ reflog-walk.h: struct date_mode;
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);
+- struct date_mode, int force_date);
++ struct date_mode, int force_date,
++ const char *branch);
void get_reflog_message(struct strbuf *sb,
struct reflog_walk_info *reflog_info);
+const char *get_reflog_branch(struct reflog_walk_info *reflog_info);
@@ t/t1414-reflog-walk.sh: test_expect_success '--operations shows a rebase as one
+ 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
++ HEAD@{0}: (topic) rebase (finish): returning to refs/heads/topic
++ HEAD@{1}: (topic) rebase (pick): C
++ HEAD@{2}: (topic) rebase (pick): B
++ HEAD@{3}: (topic) rebase (pick): A
++ HEAD@{4}: (topic) rebase (start): checkout main
++ HEAD@{5}: (topic) checkout: moving from main to topic
++ HEAD@{6}: (main) commit: upstream
++ HEAD@{7}: (main) checkout: moving from topic to main
++ HEAD@{8}: (topic) commit: C
++ HEAD@{9}: (topic) commit: B
++ HEAD@{10}: (topic) commit: A
++ HEAD@{11}: (topic) checkout: moving from main to topic
++ HEAD@{12}: (main) commit (initial): base
+ EOF
+ git reflog --historical-branch >out &&
+ cut -d " " -f 2- out >actual &&
@@ t/t1414-reflog-walk.sh: test_expect_success '--operations shows a rebase as one
+ (
+ cd operations &&
+ hash=$(git rev-parse --short HEAD) &&
-+ echo "<YELLOW>$hash<RESET><YELLOW> (<RESET><BOLD;GREEN>topic<RESET><YELLOW>)<RESET>" >expect &&
++ echo "<YELLOW>$hash<RESET> HEAD@{0}: <YELLOW>(<RESET><BOLD;GREEN>topic<RESET><YELLOW>)<RESET> rebase (finish): returning to refs/heads/topic" >expect &&
+ git reflog --historical-branch --decorate --color -1 >out &&
-+ test_decode_color <out | sed "s/ HEAD@.*//" >actual &&
++ test_decode_color <out >actual &&
+ test_cmp expect actual
+ )
+'
@@ t/t1414-reflog-walk.sh: test_expect_success '--operations shows a rebase as one
+test_expect_success '--historical-branch names the ref of other reflogs' '
+ (
+ cd operations &&
-+ echo "(main) main@{0}: commit: upstream" >expect &&
++ echo "main@{0}: (main) commit: upstream" >expect &&
+ git reflog -1 main --historical-branch >out &&
+ cut -d " " -f 2- out >actual &&
+ test_cmp expect actual
@@ t/t1414-reflog-walk.sh: test_expect_success '--operations shows a pull --rebase
+ cd downstream &&
+ echo "(topic)" >expect &&
+ git reflog --historical-branch >out &&
-+ cut -d " " -f 2 out | sort -u >actual &&
++ cut -d " " -f 3 out | sort -u >actual &&
+ test_cmp expect actual
+ )
+'
@@ t/t1414-reflog-walk.sh: test_expect_success '--operations shows a pull --rebase
+ 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
++ HEAD@{0}: (renamed) commit: E
++ HEAD@{1}: (renamed) Branch: renamed refs/heads/topic to refs/heads/renamed
++ HEAD@{3}: (topic) commit: D
+ EOF
+ git reflog --historical-branch -3 >out &&
+ cut -d " " -f 2- out >actual &&
@@ t/t1414-reflog-walk.sh: test_expect_success '--operations shows a pull --rebase
+ 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
++ HEAD@{0}: (renamed) checkout: moving from $(git rev-parse F) to renamed
++ HEAD@{1}: (detached at $(git rev-parse --short F)) commit: F
+ EOF
+ git reflog --historical-branch -2 >out &&
+ cut -d " " -f 2- out >actual &&
--
gitgitgadget
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v2 1/3] reflog: name the strings that mark switches, renames and rebase steps
2026-10-18 18:27 ` [PATCH v2 " Harald Nordgren via GitGitGadget
@ 2026-10-18 18:27 ` Harald Nordgren via GitGitGadget
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
2 siblings, 0 replies; 17+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-10-18 18:27 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, D. Ben Knoble, Harald Nordgren,
Harald Nordgren
From: Harald Nordgren <haraldnordgren@gmail.com>
The next commit adds "git reflog --operations", which finds branch
switches, branch renames and the start and end of a rebase by the
messages they leave in the reflog. The commands that write these
messages spell out the strings by hand, and "@{-<n>}" and "git status"
each keep their own copy of the switch prefix to read it back. Reading
them in one more place would add yet another copy that has to stay in
sync.
Name the strings once in reflog.h and use the names wherever the
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 written
by "git rebase".
No change in behavior.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
builtin/branch.c | 3 ++-
builtin/checkout.c | 3 ++-
builtin/rebase.c | 17 +++++++++--------
object-name.c | 3 ++-
reflog.h | 6 ++++++
sequencer.c | 13 +++++++------
wt-status.c | 3 ++-
7 files changed, 30 insertions(+), 18 deletions(-)
diff --git a/builtin/branch.c b/builtin/branch.c
index a613148fc7..30062ceb7c 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -13,6 +13,7 @@
#include "editor.h"
#include "environment.h"
#include "refs.h"
+#include "reflog.h"
#include "commit.h"
#include "gettext.h"
#include "object-name.h"
@@ -669,7 +670,7 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int
strbuf_addf(&logmsg, "Branch: copied %s to %s",
oldref.buf, newref.buf);
else
- strbuf_addf(&logmsg, "Branch: renamed %s to %s",
+ strbuf_addf(&logmsg, REFLOG_RENAME_PREFIX "%s to %s",
oldref.buf, newref.buf);
if (!copy && !(oldref_usage & IS_ORPHAN) &&
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 2d575a4f57..f011284084 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -25,6 +25,7 @@
#include "preload-index.h"
#include "read-cache.h"
#include "refs.h"
+#include "reflog.h"
#include "remote.h"
#include "repo-settings.h"
#include "resolve-undo.h"
@@ -999,7 +1000,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
reflog_msg = getenv("GIT_REFLOG_ACTION");
if (!reflog_msg)
- strbuf_addf(&msg, "checkout: moving from %s to %s",
+ strbuf_addf(&msg, REFLOG_SWITCH_PREFIX "%s to %s",
old_desc ? old_desc : "(invalid)", new_branch_info->name);
else
strbuf_insertstr(&msg, 0, reflog_msg);
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 535db60181..26ef589e89 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -17,6 +17,7 @@
#include "strvec.h"
#include "dir.h"
#include "refs.h"
+#include "reflog.h"
#include "config.h"
#include "unpack-trees.h"
#include "lockfile.h"
@@ -601,11 +602,11 @@ static int move_to_original_branch(struct rebase_options *opts)
if (!opts->onto)
BUG("move_to_original_branch without onto");
- strbuf_addf(&branch_reflog, "%s (finish): %s onto %s",
- opts->reflog_action,
+ strbuf_addf(&branch_reflog, "%s (%s): %s onto %s",
+ opts->reflog_action, REFLOG_STEP_FINISH,
opts->head_name, oid_to_hex(&opts->onto->object.oid));
- strbuf_addf(&head_reflog, "%s (finish): returning to %s",
- opts->reflog_action, opts->head_name);
+ strbuf_addf(&head_reflog, "%s (%s): returning to %s",
+ opts->reflog_action, REFLOG_STEP_FINISH, opts->head_name);
ropts.branch = opts->head_name;
ropts.flags = RESET_WORKING_TREE_REFS_ONLY |
RESET_WORKING_TREE_UPDATE_HEAD;
@@ -1413,8 +1414,8 @@ int cmd_rebase(int argc,
if (read_basic_state(&options))
exit(1);
- strbuf_addf(&head_msg, "%s (abort): returning to %s",
- options.reflog_action,
+ strbuf_addf(&head_msg, "%s (%s): returning to %s",
+ options.reflog_action, REFLOG_STEP_ABORT,
options.head_name ? options.head_name
: oid_to_hex(&options.orig_head->object.oid));
ropts.oid = &options.orig_head->object.oid;
@@ -1884,8 +1885,8 @@ int cmd_rebase(int argc,
printf(_("First, rewinding head to replay your work on top of "
"it...\n"));
- strbuf_addf(&msg, "%s (start): checkout %s",
- options.reflog_action, options.onto_name);
+ strbuf_addf(&msg, "%s (%s): checkout %s", options.reflog_action,
+ REFLOG_STEP_START, options.onto_name);
ropts.oid = &options.onto->object.oid;
ropts.orig_head = &options.orig_head->object.oid;
ropts.flags = RESET_WORKING_TREE_DETACH |
diff --git a/object-name.c b/object-name.c
index 6546685760..5b6ce8e0e0 100644
--- a/object-name.c
+++ b/object-name.c
@@ -13,6 +13,7 @@
#include "tree.h"
#include "tree-walk.h"
#include "refs.h"
+#include "reflog.h"
#include "remote.h"
#include "dir.h"
#include "odb.h"
@@ -1251,7 +1252,7 @@ static int grab_nth_branch_switch(const char *refname UNUSED,
const char *match = NULL, *target = NULL;
size_t len;
- if (skip_prefix(message, "checkout: moving from ", &match))
+ if (skip_prefix(message, REFLOG_SWITCH_PREFIX, &match))
target = strstr(match, " to ");
if (!match || !target)
diff --git a/reflog.h b/reflog.h
index 3bdd1ca8a5..32c3737db4 100644
--- a/reflog.h
+++ b/reflog.h
@@ -2,6 +2,12 @@
#define REFLOG_H
#include "refs.h"
+#define REFLOG_SWITCH_PREFIX "checkout: moving from "
+#define REFLOG_RENAME_PREFIX "Branch: renamed "
+#define REFLOG_STEP_START "start"
+#define REFLOG_STEP_FINISH "finish"
+#define REFLOG_STEP_ABORT "abort"
+
#define REFLOG_EXPIRE_TOTAL (1 << 0)
#define REFLOG_EXPIRE_UNREACH (1 << 1)
diff --git a/sequencer.c b/sequencer.c
index efbfd1315b..c55df2d38f 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -31,6 +31,7 @@
#include "merge-ort.h"
#include "merge-ort-wrappers.h"
#include "refs.h"
+#include "reflog.h"
#include "sparse-index.h"
#include "strvec.h"
#include "quote.h"
@@ -5011,8 +5012,8 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,
RESET_WORKING_TREE_UPDATE_HEAD |
RESET_WORKING_TREE_UPDATE_ORIG_HEAD |
RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK,
- .head_msg = reflog_message(opts, "start", "checkout %s",
- onto_name),
+ .head_msg = reflog_message(opts, REFLOG_STEP_START,
+ "checkout %s", onto_name),
.default_reflog_action = sequencer_reflog_action(opts)
};
if (reset_working_tree(r, &ropts)) {
@@ -5310,16 +5311,16 @@ cleanup_head_ref:
res = error(_("could not read 'onto'"));
goto cleanup_head_ref;
}
- msg = reflog_message(opts, "finish", "%s onto %s",
- head_ref.buf, buf.buf);
+ msg = reflog_message(opts, REFLOG_STEP_FINISH,
+ "%s onto %s", head_ref.buf, buf.buf);
if (refs_update_ref(get_main_ref_store(the_repository), msg, head_ref.buf, &head, &orig,
REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) {
res = error(_("could not update %s"),
head_ref.buf);
goto cleanup_head_ref;
}
- msg = reflog_message(opts, "finish", "returning to %s",
- head_ref.buf);
+ msg = reflog_message(opts, REFLOG_STEP_FINISH,
+ "returning to %s", head_ref.buf);
if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
res = error(_("could not update HEAD to %s"),
head_ref.buf);
diff --git a/wt-status.c b/wt-status.c
index 57772c7501..ffd54353a8 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -22,6 +22,7 @@
#include "strvec.h"
#include "remote.h"
#include "refs.h"
+#include "reflog.h"
#include "submodule.h"
#include "column.h"
#include "read-cache.h"
@@ -1790,7 +1791,7 @@ static int grab_1st_switch(const char *refname UNUSED,
struct grab_1st_switch_cbdata *cb = cb_data;
const char *target = NULL, *end;
- if (!skip_prefix(message, "checkout: moving from ", &message))
+ if (!skip_prefix(message, REFLOG_SWITCH_PREFIX, &message))
return 0;
target = strstr(message, " to ");
if (!target)
--
gitgitgadget
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 2/3] reflog: add --operations to show one entry per command
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-18 18:27 ` 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
2 siblings, 0 replies; 17+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-10-18 18:27 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, D. Ben Knoble, Harald Nordgren,
Harald Nordgren
From: Harald Nordgren <haraldnordgren@gmail.com>
After a rebase, "git reflog" shows one entry for every commit the
rebase replayed, plus one for its start and one for its end. Together
with an entry for every branch switch, finding where a branch was
before the rebase means reading past all of them.
Teach "git reflog" and "git log -g" an "--operations" option that
shows each rebase as the single entry that finished or aborted it, and
leaves out entries that change no branch, like branch switches and the
"reset: moving to HEAD" that "git stash" and "git merge --abort" leave
behind:
$ git reflog --operations
4e5f6a7 HEAD@{0}: rebase (finish): returning to refs/heads/topic
8c9d0e1 HEAD@{5}: commit: fix typo
1a2b3c4 HEAD@{7}: commit: add tests
The HEAD@{<n>} numbers keep their usual values, so the previous entry
on the same branch can be used to undo the rebase:
$ git reset --hard HEAD@{7}
Commands run while a rebase is stopped, such as an amend after an
"edit", are folded into that rebase, and a "git pull --rebase" is shown
as one entry the same way.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
Documentation/git-reflog.adoc | 3 +
Documentation/rev-list-options.adoc | 11 +++
reflog-walk.c | 61 +++++++++++++++-
reflog-walk.h | 1 +
revision.c | 8 +++
revision.h | 1 +
t/t1414-reflog-walk.sh | 105 ++++++++++++++++++++++++++++
7 files changed, 188 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc
index 38af0c977a..672b9ea72e 100644
--- a/Documentation/git-reflog.adoc
+++ b/Documentation/git-reflog.adoc
@@ -41,6 +41,9 @@ command-line (or `HEAD`, by default). The reflog covers all recent
actions, and in addition the `HEAD` reflog records branch switching.
`git reflog show` is an alias for `git log -g --abbrev-commit
--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.
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 bd9f345690..a40ba2641b 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -348,6 +348,17 @@ See also linkgit:git-reflog[1].
+
Under `--pretty=reference`, this information will not be shown at all.
+`--operations`::
+ Show one reflog entry for each command that changed a branch.
+ A rebase is shown as only the entry that finished or aborted it,
+ including any commands run while it was stopped, such as
+ `git commit --amend`. Switching branches is not shown, and
+ neither is a reset that left `HEAD` where it was, such as the one
+ `git stash` does.
+ The previous entry on the same branch is the state before the
+ rebase started. 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/reflog-walk.c b/reflog-walk.c
index 4dbeaa93a7..82528f5b66 100644
--- a/reflog-walk.c
+++ b/reflog-walk.c
@@ -3,6 +3,7 @@
#include "git-compat-util.h"
#include "commit.h"
#include "refs.h"
+#include "reflog.h"
#include "diff.h"
#include "repository.h"
#include "revision.h"
@@ -18,6 +19,7 @@ struct complete_reflogs {
timestamp_t timestamp;
int tz;
char *message;
+ bool user_operation;
} *items;
int nr, alloc;
};
@@ -38,6 +40,7 @@ static int read_one_reflog(const char *refname UNUSED,
item->timestamp = timestamp;
item->tz = tz;
item->message = xstrdup(message);
+ item->user_operation = true;
array->nr++;
return 0;
}
@@ -65,6 +68,46 @@ static void complete_reflogs_clear(void *util, const char *str UNUSED)
free_complete_reflog(array);
}
+static bool has_rebase_step(const char *message, const char *step)
+{
+ size_t end = strcspn(message, ":\n");
+ size_t len = strlen(step);
+
+ return end >= len + 3 && message[end - 1] == ')' &&
+ !strncmp(message + end - len - 3, " (", 2) &&
+ !strncmp(message + end - len - 1, step, len);
+}
+
+static void mark_operations(struct complete_reflogs *array)
+{
+ int start = -1;
+
+ for (int i = 0; i < array->nr; i++) {
+ struct reflog_info *item = &array->items[i];
+ const char *msg = item->message;
+
+ if (has_rebase_step(msg, REFLOG_STEP_FINISH) ||
+ has_rebase_step(msg, REFLOG_STEP_ABORT)) {
+ if (start >= 0)
+ for (int j = start; j < i; j++)
+ array->items[j].user_operation = false;
+ start = -1;
+ item->user_operation = true;
+ continue;
+ }
+ if (has_rebase_step(msg, REFLOG_STEP_START))
+ start = i;
+
+ if (starts_with(msg, REFLOG_SWITCH_PREFIX))
+ item->user_operation = false;
+ else if (starts_with(msg, REFLOG_RENAME_PREFIX))
+ item->user_operation = true;
+ else
+ item->user_operation =
+ !oideq(&item->ooid, &item->noid);
+ }
+}
+
static struct complete_reflogs *read_complete_reflog(const char *ref)
{
struct complete_reflogs *reflogs =
@@ -135,6 +178,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)
+{
+ struct string_list_item *item;
+
+ for_each_string_list_item(item, &info->complete_reflogs) {
+ if (operations)
+ mark_operations(item->util);
+ }
+}
+
void reflog_walk_info_release(struct reflog_walk_info *info)
{
size_t i;
@@ -342,8 +395,12 @@ static struct commit *next_reflog_commit(struct commit_reflog *log)
{
for (; log->recno >= 0; log->recno--) {
struct reflog_info *entry = &log->reflogs->items[log->recno];
- struct object *obj = parse_object(the_repository,
- &entry->noid);
+ struct object *obj;
+
+ if (!entry->user_operation)
+ continue;
+
+ obj = parse_object(the_repository, &entry->noid);
if (obj && obj->type == OBJ_COMMIT)
return (struct commit *)obj;
diff --git a/reflog-walk.h b/reflog-walk.h
index 989583dc55..1d86795f84 100644
--- a/reflog-walk.h
+++ b/reflog-walk.h
@@ -7,6 +7,7 @@ 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);
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,
diff --git a/revision.c b/revision.c
index a83e499047..88785651fe 100644
--- a/revision.c
+++ b/revision.c
@@ -2458,6 +2458,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
commit_list_insert(c, &revs->ancestry_path_bottoms);
} else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
init_reflog_walk(&revs->reflog_info);
+ } else if (!strcmp(arg, "--operations")) {
+ revs->reflog_operations = 1;
} else if (!strcmp(arg, "--default")) {
if (argc <= 1)
return error("bad --default argument");
@@ -3247,6 +3249,12 @@ 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->reflog_operations)
+ die(_("the option '%s' requires '%s'"), "--operations", "--walk-reflogs");
+
+ if (revs->reflog_info)
+ reflog_walk_mark(revs->reflog_info, revs->reflog_operations);
+
if (revs->line_level_traverse &&
(revs->full_diff ||
(revs->diffopt.output_format &
diff --git a/revision.h b/revision.h
index e5dabd18ce..c4f5e06698 100644
--- a/revision.h
+++ b/revision.h
@@ -221,6 +221,7 @@ struct rev_info {
exclude_first_parent_only:1,
line_level_traverse:1,
tree_blobs_in_commit_order:1,
+ reflog_operations:1,
/*
* Blobs are shown without regard for their existence.
diff --git a/t/t1414-reflog-walk.sh b/t/t1414-reflog-walk.sh
index be6c3f472c..e15f6f2381 100755
--- a/t/t1414-reflog-walk.sh
+++ b/t/t1414-reflog-walk.sh
@@ -5,6 +5,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
test_expect_success 'set up some reflog entries' '
test_commit one &&
@@ -131,6 +132,110 @@ test_expect_success 'walk prefers reflog to ref tip' '
test_cmp expect actual
'
+test_expect_success '--operations shows a rebase as one entry' '
+ git init operations &&
+ (
+ cd operations &&
+ test_commit base &&
+ git switch -c topic &&
+ test_commit A &&
+ test_commit B &&
+ test_commit C &&
+ git switch main &&
+ test_commit upstream &&
+ git switch topic &&
+ git rebase main &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} rebase (finish): returning to refs/heads/topic
+ HEAD@{6} commit: upstream
+ HEAD@{8} commit: C
+ HEAD@{9} commit: B
+ HEAD@{10} commit: A
+ HEAD@{12} commit (initial): base
+ EOF
+ git log -g --format="%gd %gs" HEAD --operations >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations shows a stopped rebase as one entry once it finishes' '
+ set_fake_editor &&
+ (
+ cd operations &&
+ FAKE_LINES="edit 1 2" git rebase -i HEAD~2 &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} rebase: fast-forward
+ HEAD@{1} rebase (start): checkout HEAD~2
+ EOF
+ git log -g --operations --format="%gd %gs" -2 >actual &&
+ test_cmp expect actual &&
+
+ git commit --amend -m "B amended" &&
+ git rebase --continue &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} rebase (finish): returning to refs/heads/topic
+ HEAD@{5} rebase (finish): returning to refs/heads/topic
+ EOF
+ git log -g --operations --format="%gd %gs" -2 >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations shows an aborted rebase as one entry' '
+ (
+ cd operations &&
+ FAKE_LINES="edit 1 2" git rebase -i HEAD~2 &&
+ git rebase --abort &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} rebase (abort): returning to refs/heads/topic
+ HEAD@{3} rebase (finish): returning to refs/heads/topic
+ EOF
+ git log -g --operations --format="%gd %gs" -2 >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations leaves out the reset done by git stash' '
+ (
+ cd operations &&
+ echo change >>C.t &&
+ git stash &&
+
+ echo "HEAD@{1} rebase (abort): returning to refs/heads/topic" >expect &&
+ git log -g --operations --format="%gd %gs" -1 >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations shows a pull --rebase as one entry' '
+ git clone operations downstream &&
+ (
+ cd operations &&
+ test_commit upstream-change
+ ) &&
+ (
+ cd downstream &&
+ test_commit local-change &&
+ git pull --rebase &&
+
+ cat >expect <<-\EOF &&
+ HEAD@{0} pull --rebase (finish): returning to refs/heads/topic
+ HEAD@{3} commit: local-change
+ EOF
+ git log -g --operations --format="%gd %gs" -2 >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--operations requires --walk-reflogs' '
+ test_must_fail git log --operations 2>err &&
+ test_grep "requires .--walk-reflogs." err
+'
+
test_expect_success 'rev-list -g complains when there are no reflogs' '
test_must_fail git rev-list -g
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 3/3] reflog: add --historical-branch to name the branch of each entry
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-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 ` Harald Nordgren via GitGitGadget
2 siblings, 0 replies; 17+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-10-18 18:27 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, D. Ben Knoble, Harald Nordgren,
Harald Nordgren
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 shows the branch each entry was made on before its message, in
place of the refs that point at the commit now. The hashes and
selectors stay lined up in their columns, and the name is colored like
a branch decoration:
$ git reflog --historical-branch
4e5f6a7 HEAD@{0}: (topic) commit: add tests
1a2b3c4 HEAD@{1}: (topic) checkout: moving from main to topic
8c9d0e1 HEAD@{2}: (main) 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 | 10 +++
log-tree.c | 28 ++++++-
reflog-walk.c | 124 +++++++++++++++++++++++++---
reflog-walk.h | 7 +-
revision.c | 8 +-
revision.h | 1 +
t/t1414-reflog-walk.sh | 95 ++++++++++++++++++++-
8 files changed, 258 insertions(+), 18 deletions(-)
diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc
index 672b9ea72e..04ed7849c2 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 shows the branch it was made on before its message.
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..8c0010695b 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -359,6 +359,16 @@ 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.
+ifndef::git-rev-list[]
+`--historical-branch`::
+ Show the branch `HEAD` was on when each reflog entry was
+ recorded, such as `(topic)`, before the reflog message, 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.
+endif::git-rev-list[]
+
`--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..b2d9a954e7 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -421,6 +421,24 @@ void format_decorations(struct strbuf *sb,
}
}
+static void format_historical_branch(struct strbuf *sb,
+ 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);
+ strbuf_addf(sb, "%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,7 +449,7 @@ void show_decorations(struct rev_info *opt, struct commit *commit)
if (slot && *slot)
fprintf(opt->diffopt.file, "\t%s", *slot);
}
- if (!opt->show_decorations)
+ if (!opt->show_decorations || opt->historical_branch)
return;
format_decorations(&sb, commit, opt->diffopt.use_color, NULL);
fputs(sb.buf, opt->diffopt.file);
@@ -834,16 +852,22 @@ void show_log(struct rev_info *opt)
graph_show_oneline(opt->graph);
}
if (opt->reflog_info) {
+ struct strbuf branch = STRBUF_INIT;
+
/*
* setup_revisions() ensures that opt->reflog_info
* and opt->graph cannot both be set,
* so we don't need to worry about printing the
* graph info here.
*/
+ if (opt->historical_branch)
+ format_historical_branch(&branch, opt);
show_reflog_message(opt->reflog_info,
opt->commit_format == CMIT_FMT_ONELINE,
opt->date_mode,
- opt->date_mode_explicit);
+ opt->date_mode_explicit,
+ branch.buf);
+ strbuf_release(&branch);
if (opt->commit_format == CMIT_FMT_ONELINE)
return;
}
diff --git a/reflog-walk.c b/reflog-walk.c
index 82528f5b66..0cdca76efd 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;
@@ -365,7 +464,8 @@ timestamp_t get_reflog_timestamp(struct reflog_walk_info *reflog_info)
}
void show_reflog_message(struct reflog_walk_info *reflog_info, int oneline,
- struct date_mode dmode, int force_date)
+ struct date_mode dmode, int force_date,
+ const char *branch)
{
if (reflog_info && reflog_info->last_commit_reflog) {
struct commit_reflog *commit_reflog = reflog_info->last_commit_reflog;
@@ -375,11 +475,13 @@ void show_reflog_message(struct reflog_walk_info *reflog_info, int oneline,
info = &commit_reflog->reflogs->items[commit_reflog->recno+1];
get_reflog_selector(&selector, reflog_info, dmode, force_date, 0);
if (oneline) {
- printf("%s: %s", selector.buf, info->message);
+ printf("%s: %s%s", selector.buf, branch,
+ info->message);
}
else {
- printf("Reflog: %s (%s)\nReflog message: %s",
- selector.buf, info->email, info->message);
+ printf("Reflog: %s (%s)\nReflog message: %s%s",
+ selector.buf, info->email, branch,
+ info->message);
}
strbuf_release(&selector);
diff --git a/reflog-walk.h b/reflog-walk.h
index 1d86795f84..8951251f8a 100644
--- a/reflog-walk.h
+++ b/reflog-walk.h
@@ -7,13 +7,16 @@ 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);
+ struct date_mode, int force_date,
+ const char *branch);
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..6d5272aacf 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 &&
+ HEAD@{0}: (topic) rebase (finish): returning to refs/heads/topic
+ HEAD@{1}: (topic) rebase (pick): C
+ HEAD@{2}: (topic) rebase (pick): B
+ HEAD@{3}: (topic) rebase (pick): A
+ HEAD@{4}: (topic) rebase (start): checkout main
+ HEAD@{5}: (topic) checkout: moving from main to topic
+ HEAD@{6}: (main) commit: upstream
+ HEAD@{7}: (main) checkout: moving from topic to main
+ HEAD@{8}: (topic) commit: C
+ HEAD@{9}: (topic) commit: B
+ HEAD@{10}: (topic) commit: A
+ HEAD@{11}: (topic) checkout: moving from main to topic
+ HEAD@{12}: (main) 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> HEAD@{0}: <YELLOW>(<RESET><BOLD;GREEN>topic<RESET><YELLOW>)<RESET> rebase (finish): returning to refs/heads/topic" >expect &&
+ git reflog --historical-branch --decorate --color -1 >out &&
+ test_decode_color <out >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--historical-branch names the ref of other reflogs' '
+ (
+ cd operations &&
+ echo "main@{0}: (main) 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 3 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 &&
+ HEAD@{0}: (renamed) commit: E
+ HEAD@{1}: (renamed) Branch: renamed refs/heads/topic to refs/heads/renamed
+ HEAD@{3}: (topic) 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 &&
+ HEAD@{0}: (renamed) checkout: moving from $(git rev-parse F) to renamed
+ HEAD@{1}: (detached at $(git rev-parse --short F)) 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
^ permalink raw reply related [flat|nested] 17+ messages in thread
end of thread, other threads:[~2026-10-18 18:27 UTC | newest]
Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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-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-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
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.