threads / discuss / 58105

How to display "HEAD~*" in "git log"

Subject: How to display "HEAD~*" in "git log"

## tl;dr

11 messages between Jul 5, 2022 and Jul 7, 2022.

replies: 10people: 4as markdown or json

wuzhouhui· Jul 5, 2022, 07:11 UTC · lore
Hi

I frequently use "git rebase" to move commit to a specific location, to know which commit ID as param of "git rebase", I have to list all commits by "git log --oneline", then copy specific commit ID, and executing "git rebase" as

   git rebase -i <copied commit ID>~

because SHA sum is hard to memorize, so I have to use copy and past, which is too boring. So, I wonder if there is a way to let "git log" display commits like following:

   HEAD   <one line commit message>
   HEAD~1 <one line commit message>
HEAD~2 <one line commit message>
HEAD~3 <one line commit message>
   ...

With these "HEAD~*", I can easily directly type them and no need to move my fingers out of keyboard.

Thanks.
Konstantin Khomoutov· Jul 5, 2022, 09:25 UTC · re: wuzhouhui · lore

Re: How to display "HEAD~*" in "git log"

On Tue, Jul 05, 2022 at 03:11:49PM +0800, wuzhouhui wrote:
Show 19 quoted lines
> I frequently use "git rebase" to move commit to a specific location, to
> know which commit ID as param of "git rebase", I have to list all commits
> by "git log --oneline", then copy specific commit ID, and executing "git
> rebase" as
> 
>   git rebase -i <copied commit ID>~
> 
> because SHA sum is hard to memorize, so I have to use copy and past,
> which is too boring. So, I wonder if there is a way to let "git log"
> display commits like following:
> 
>   HEAD   <one line commit message>
>   HEAD~1 <one line commit message>
> HEAD~2 <one line commit message>
> HEAD~3 <one line commit message>
>   ...
> 
> With these "HEAD~*", I can easily directly type them and no need to
> move my fingers out of keyboard.

You can script this. Provided you have a POSIX-compatible shell (such as Bash), the encantation would read something like

 $ git log --oneline | { n=0; while read line; do printf '%d\t%s\n' $n "$line"; done; }
This is admittedly not convenient so it makes sense to turn into an alias:
 [alias]
   relog = "!relog() { git log --oneline \"$@\" | { n=0; while read line; do printf 'HEAD~%d\t%s\n' $n \"$line\"; n=$((n+1)); done; }; }; relog"
Then the call
 $ git relog
would output something like

HEAD~0 deadbeef Commit message HEAD~1 fadedfac Another commit message HEAD~2 12345678 Yet another commit message

...and so on.

It's easy to make that script not output "~0" for the very first entry and output just "~" instead of "~1" for the second, but I what's presented is enough for an example.

Konstantin Khomoutov· Jul 5, 2022, 09:30 UTC · re: Konstantin Khomoutov · lore

Re: How to display "HEAD~*" in "git log"

On Tue, Jul 05, 2022 at 12:25:14PM +0300, Konstantin Khomoutov wrote:
[...]
> This is admittedly not convenient so it makes sense to turn into an alias:
> 
>  [alias]
>    relog = "!relog() { git log --oneline \"$@\" | { n=0; while read line; do printf 'HEAD~%d\t%s\n' $n \"$line\"; n=$((n+1)); done; }; }; relog"
[...]

An alternative to an alias it creating a specially named script file which must be executable and located in a directorly listed in the environment variable PATH. For instance, if you have ~/bin listed in your PATH, you can create a file named "git-relog" containing the encantation from my first mail in this thread, then call

  $ git relog
and that script will be found and executed.
wuzhouhui· Jul 5, 2022, 09:36 UTC · re: Konstantin Khomoutov · lore

Re: How to display "HEAD~*" in "git log"

On 7/5/22 17:30, Konstantin Khomoutov wrote:
Show 19 quoted lines
> On Tue, Jul 05, 2022 at 12:25:14PM +0300, Konstantin Khomoutov wrote:
>
> [...]
>> This is admittedly not convenient so it makes sense to turn into an alias:
>>
>>   [alias]
>>     relog = "!relog() { git log --oneline \"$@\" | { n=0; while read line; do printf 'HEAD~%d\t%s\n' $n \"$line\"; n=$((n+1)); done; }; }; relog"
> [...]
>
> An alternative to an alias it creating a specially named script file which
> must be executable and located in a directorly listed in the environment
> variable PATH.
> For instance, if you have ~/bin listed in your PATH, you can create a file
> named "git-relog" containing the encantation from my first mail in this
> thread, then call
>
>    $ git relog
>
> and that script will be found and executed.
Thanks for your reply, it does solve my problems.
kostix@bswap.ru· Jul 6, 2022, 12:38 UTC · re: wuzhouhui · lore

Re: How to display "HEAD~*" in "git log"

By the way, you can consider "going more interactive" with the approach which involves scripting. For instance, you can use any of the "fuzzy finders" such as the venerable fzf [1] to interactively select a commit to rebase - something like

  $ git log --oneline -50 | fzf | cut -d ' ' -f 1

would produce the log of the last 50 commits, allow you to interactively select a commit of interest by typing some of the characters of its message and/or hash and then print the selected hash.

This can be easily combined with rebasing: for instance, if you have the above encantation available as an alias named "select" or a custom script named "git-select", you could do

  $ git rebase -i $(git select)~
to not even copy or memorize anyting.
I, for one, routinely use fzf to pick a branch for checkout.
1. https://github.com/junegunn/fzf
Jeff King· Jul 6, 2022, 14:38 UTC · re: Konstantin Khomoutov · lore

Re: How to display "HEAD~*" in "git log"

On Tue, Jul 05, 2022 at 12:25:14PM +0300, Konstantin Khomoutov wrote:
Show 17 quoted lines
> > because SHA sum is hard to memorize, so I have to use copy and past,
> > which is too boring. So, I wonder if there is a way to let "git log"
> > display commits like following:
> > 
> >   HEAD   <one line commit message>
> >   HEAD~1 <one line commit message>
> > HEAD~2 <one line commit message>
> > HEAD~3 <one line commit message>
> >   ...
> > 
> > With these "HEAD~*", I can easily directly type them and no need to
> > move my fingers out of keyboard.
> 
> You can script this. Provided you have a POSIX-compatible shell (such as
> Bash), the encantation would read something like
> 
>  $ git log --oneline | { n=0; while read line; do printf '%d\t%s\n' $n "$line"; done; }

That will just number the commits linearly as they are printed. If your history has any branches or merges, eventually it will get out of sync.

You can use "name-rev" to annotate commits with names that respect the history. It only matches full oids, so try:

  git log --oneline --no-abbrev | git name-rev --stdin

If you want shorter hex oids, you can work around it with "--name-only" and a custom format:

  git log --format='%h (%H) %s' | git name-rev --stdin --name-only

Note that the names will be based on the nearest branches/tags. You can use "--refs" to limit it, but sadly there doesn't seem to be a way to specify just HEAD. That might be a fun and easy feature to add.

And finally, if you really like this, you can configure git-log's pager to always pipe through name-rev, like this:

  [pager]
        log = "git name-rev --stdin | less"

Then the usual "medium" output from git-log will have the annotations on the "commit" lines. Arguably git-log ought to be able to do this internally, but I don't think anybody has ever implemented it (and I wouldn't be surprised if it's a little challenging, just because you'd have two traversals going on at once in the same program).

Note that I don't use any of those myself. Long ago I taught my terminal to do keyboard selection of object ids for cut-and-paste. I think there are probably solutions for tmux and other programs you can find online. I shared mine for urxvt a while ago:

  https://lore.kernel.org/git/20170120192539.7jts6xqzx46unn7y@sigill.intra.peff.net/

though if anybody is interested in it, let me know because I can share a new version with some bug fixes and improvements since then.

-Peff
Konstantin Khomoutov· Jul 6, 2022, 16:28 UTC · re: Jeff King · lore

Re: How to display "HEAD~*" in "git log"

On Wed, Jul 06, 2022 at 10:38:37AM -0400, Jeff King wrote:
[...]
Show 7 quoted lines
> > You can script this. Provided you have a POSIX-compatible shell (such as
> > Bash), the encantation would read something like
> > 
> >  $ git log --oneline | { n=0; while read line; do printf '%d\t%s\n' $n "$line"; done; }
> 
> That will just number the commits linearly as they are printed. If your
> history has any branches or merges, eventually it will get out of sync.
Thanks, I did not think of this.
> You can use "name-rev" to annotate commits with names that respect the
> history. It only matches full oids, so try:
> 
>   git log --oneline --no-abbrev | git name-rev --stdin

I pondered about `git name-rev` but failed to figure out how to make it accept just "HEAD" as the point of reference. Passing it "--refs=HEAD" (or "--refs HEAD", FWIW) does not appear to do what I'd expect it to: a couple of top commits are not annotated at all, and then they start to get annotated relative to "origin/HEAD". Note that I've tested this on a detached HEAD which contains an unmerged line of history.

[...]
Jeff King· Jul 6, 2022, 16:49 UTC · re: Konstantin Khomoutov · lore

Re: How to display "HEAD~*" in "git log"

On Wed, Jul 06, 2022 at 07:28:56PM +0300, Konstantin Khomoutov wrote:
Show 11 quoted lines
> > You can use "name-rev" to annotate commits with names that respect the
> > history. It only matches full oids, so try:
> > 
> >   git log --oneline --no-abbrev | git name-rev --stdin
> 
> I pondered about `git name-rev` but failed to figure out how to make it accept
> just "HEAD" as the point of reference. Passing it "--refs=HEAD"
> (or "--refs HEAD", FWIW) does not appear to do what I'd expect it to: a couple
> of top commits are not annotated at all, and then they start to get annotated
> relative to "origin/HEAD". Note that I've tested this on a detached HEAD which
> contains an unmerged line of history.

Right. The problem is that it gets the set of possible ref tips with for_each_ref(), which will not include HEAD. And then worse, since the argument to "--refs" is a pattern, it matches junk like origin/HEAD.

You'd perhaps need a new option, something like:
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index c4dc143c4b..ec36e48ca3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -568,10 +568,12 @@ int cmd_name_rev(int argc, const char **argv, const char *prefix)
 {
 	struct object_array revs = OBJECT_ARRAY_INIT;
 	int all = 0, annotate_stdin = 0, transform_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
+	int head_only = 0;
 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
 	struct option opts[] = {
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
+		OPT_BOOL(0, "head", &head_only, N_("only use HEAD to name the commits")),
 		OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
 				   N_("only use refs matching <pattern>")),
 		OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
@@ -652,7 +654,10 @@ int cmd_name_rev(int argc, const char **argv, const char *prefix)
 
 	adjust_cutoff_timestamp_for_slop();
 
-	for_each_ref(name_ref, &data);
+	if (head_only)
+		head_ref(name_ref, &data);
+	else
+		for_each_ref(name_ref, &data);
 	name_tips();
 
 	if (annotate_stdin) {

though there are some open issues. E.g., can it be combined with other
--refs options? If not, should we detect that case and complain?

Another option is to just teach it to always look at HEAD and see if
name_ref() wants to accept it. Like:

diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index c4dc143c4b..afd9f93a95 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -652,6 +652,7 @@ int cmd_name_rev(int argc, const char **argv, const char *prefix)
 
 	adjust_cutoff_timestamp_for_slop();
 
+	head_ref(name_ref, &data);
 	for_each_ref(name_ref, &data);
 	name_tips();
 

That has the nice advantage of not needing a new option. But:

  - it does change the default output; it may be that people would
    prefer not to see HEAD~5, in favor of the actual branch name.

  - if you want just HEAD, you'll have to say so, and then ask to
    exclude origin/HEAD, etc, with a pattern. Like:

      git name-rev --stdin --refs=HEAD --exclude='*/*'

    which is perhaps a little counter-intuitive (though an example in
    the manpage might lead people in the right direction).

I dunno. I would not be sad to see either direction turned into a real
patch, if somebody cares to clean it up and make a convincing argument.
I doubt I would use either myself (nor have I ever really used name-rev
much, tbh).

-Peff
Konstantin Khomoutov· Jul 6, 2022, 18:21 UTC · re: Jeff King · lore

Re: How to display "HEAD~*" in "git log"

On Wed, Jul 06, 2022 at 12:49:08PM -0400, Jeff King wrote:
[...]
Show 10 quoted lines
> > I pondered about `git name-rev` but failed to figure out how to make it accept
> > just "HEAD" as the point of reference. Passing it "--refs=HEAD"
> > (or "--refs HEAD", FWIW) does not appear to do what I'd expect it to: a couple
> > of top commits are not annotated at all, and then they start to get annotated
> > relative to "origin/HEAD". Note that I've tested this on a detached HEAD which
> > contains an unmerged line of history.
> 
> Right. The problem is that it gets the set of possible ref tips with
> for_each_ref(), which will not include HEAD. And then worse, since the
> argument to "--refs" is a pattern, it matches junk like origin/HEAD.

Do I assume correctly that `git for-each-ref HEAD` does nothing, successfully, for the very same reason? If so, I wonder if this should be somehow reflected in the docs. I mean, I have always maintained an impression that things like HEAD, ORIGIN_HEAD, FETCH_HEAD etc are also "refs" - because they, well, reference commits or branches.

The gitglossary manual page of my Git 2.30.2 states that
| ref
|   A name that begins with refs/ (e.g. refs/heads/master)
| <...>
|   There are a few special-purpose refs that do not begin with refs/.
|   The most notable example is HEAD.
which suggests that HEAD is a ref.
Jeff King· Jul 7, 2022, 17:06 UTC · re: Konstantin Khomoutov · lore

Re: How to display "HEAD~*" in "git log"

On Wed, Jul 06, 2022 at 09:21:49PM +0300, Konstantin Khomoutov wrote:
Show 6 quoted lines
> > Right. The problem is that it gets the set of possible ref tips with
> > for_each_ref(), which will not include HEAD. And then worse, since the
> > argument to "--refs" is a pattern, it matches junk like origin/HEAD.
> 
> Do I assume correctly that `git for-each-ref HEAD` does nothing, successfully,
> for the very same reason?
Yes.
Show 14 quoted lines
> If so, I wonder if this should be somehow reflected in the docs.
> I mean, I have always maintained an impression that things like HEAD,
> ORIGIN_HEAD, FETCH_HEAD etc are also "refs" - because they, well, reference
> commits or branches.
> 
> The gitglossary manual page of my Git 2.30.2 states that
> 
> | ref
> |   A name that begins with refs/ (e.g. refs/heads/master)
> | <...>
> |   There are a few special-purpose refs that do not begin with refs/.
> |   The most notable example is HEAD.
> 
> which suggests that HEAD is a ref.

Yeah, I think the situation around those top-level names is...complicated. They act like refs when you name them, but they are often omitted from iteration of refs. And yet many spots (e.g., "rev-list --all") will include them explicitly. Documentation aside, I would not be surprised if there are still places in the code itself where there are inconsistencies. I feel like we've had a number of "oops, include HEAD in this corner case" patches pop up over the years. Maybe we've gotten them all now?

All of which is to say: you're definitely not wrong, but I'm not sure what simple documentation change would be helpful. The handling of HEAD in each specific situation could be mentioned in their respective documentation spots, but identifying all of those is tough. Possibly the glossary entry should say something like:

  Note that HEAD is sometimes includes in sets of refs (e.g., in `git
  rev-list --all`) and sometimes not (e.g., in `git for-each-ref`).
but that may be too vague to be helpful to anybody.
-Peff
Junio C Hamano· Jul 6, 2022, 17:31 UTC · re: Jeff King · lore

Re: How to display "HEAD~*" in "git log"

Jeff King <peff@peff.net> writes:
Show 5 quoted lines
> And finally, if you really like this, you can configure git-log's pager
> to always pipe through name-rev, like this:
>
>   [pager]
>         log = "git name-rev --stdin | less"

I was about to suggest "name-rev --annotate-stdin" (which happened during your sabbatical) but this is even better.

> ... (and I
> wouldn't be surprised if it's a little challenging, just because you'd
> have two traversals going on at once in the same program).
A little challenging ;-)

But it may not be so bad. The "name-rev" traversal uses none of the usual "revision" traversal machinery and keeps track of its state in its own commit slab. It would not be as hard as running two usual revision traversals simultaneously.

← back to recent threads