Skip to content

Instantly share code, notes, and snippets.

@nhed
Last active July 3, 2024 03:11
Show Gist options
  • Save nhed/96b61606005dcbbeca6f394031973291 to your computer and use it in GitHub Desktop.
Save nhed/96b61606005dcbbeca6f394031973291 to your computer and use it in GitHub Desktop.

Scratching my head regarding an answer on stackoverflow https://stackoverflow.com/a/69949813/652904

Setup

$ for x in {1..9}; do echo "${x}" > "${x}.txt"; git add "${x}.txt"; git commit -m "Adding ${x}.txt"; done
[dummy fc791a8] Adding 1.txt
 1 file changed, 1 insertion(+)
 create mode 100644 1.txt
[dummy c060042] Adding 2.txt
 1 file changed, 1 insertion(+)
 create mode 100644 2.txt
[dummy 3e57b24] Adding 3.txt
 1 file changed, 1 insertion(+)
 create mode 100644 3.txt
[dummy 6bf3d29] Adding 4.txt
 1 file changed, 1 insertion(+)
 create mode 100644 4.txt
[dummy d5047a3] Adding 5.txt
 1 file changed, 1 insertion(+)
 create mode 100644 5.txt
[dummy 07e264a] Adding 6.txt
 1 file changed, 1 insertion(+)
 create mode 100644 6.txt
[dummy 14b5a8d] Adding 7.txt
 1 file changed, 1 insertion(+)
 create mode 100644 7.txt
[dummy eeebb3f] Adding 8.txt
 1 file changed, 1 insertion(+)
 create mode 100644 8.txt
[dummy 805e990] Adding 9.txt
 1 file changed, 1 insertion(+)
 create mode 100644 9.txt

Yielding

$ git log --pretty=oneline --abbrev-commit origin/main..HEAD
805e990 (HEAD -> dummy) Adding 9.txt
eeebb3f Adding 8.txt
14b5a8d Adding 7.txt
07e264a Adding 6.txt
d5047a3 Adding 5.txt
6bf3d29 Adding 4.txt
3e57b24 Adding 3.txt
c060042 Adding 2.txt
fc791a8 Adding 1.txt

First try both forms in bash

With && : (dropping 3.txt)

$ git rebase --onto 3e57b24^ 3e57b24 && :
Successfully rebased and updated refs/heads/dummy.

$ git log --pretty=oneline --abbrev-commit origin/main..HEAD
ccfd21f (HEAD -> dummy) Adding 9.txt
3c0a22f Adding 8.txt
9ab6818 Adding 7.txt
5466ebc Adding 6.txt
15a93e2 Adding 5.txt
ddaf52d Adding 4.txt
c060042 Adding 2.txt
fc791a8 Adding 1.txt

Without && : (dropping 4.txt)

$ git rebase --onto ddaf52d^ ddaf52d
Successfully rebased and updated refs/heads/dummy.

$ git log --pretty=oneline --abbrev-commit origin/main..HEAD
5d943c7 (HEAD -> dummy) Adding 9.txt
7d90139 Adding 8.txt
30058d0 Adding 7.txt
bb3e237 Adding 6.txt
7c909b0 Adding 5.txt
c060042 Adding 2.txt
fc791a8 Adding 1.txt

So far so good!

With the following 2 git aliases

$ grep drop-commit- ~/.gitconfig
  drop-commit-without = !git rebase --onto $1^ $1
  drop-commit-with = !git rebase --onto $1^ $1 && :

With && : (dropping 5.txt)

$ git drop-commit-with 7c909b0
Successfully rebased and updated refs/heads/dummy.

$ git log --pretty=oneline --abbrev-commit origin/main..HEAD
5b560df (HEAD -> dummy) Adding 9.txt
cee9f09 Adding 8.txt
2a1dae3 Adding 7.txt
11a7733 Adding 6.txt
c060042 Adding 2.txt
fc791a8 Adding 1.txt

Without && : (trying to drop 6.txt)

$ git drop-commit-without 11a7733
Successfully rebased and updated detached HEAD.

$ git log --pretty=oneline --abbrev-commit origin/main..HEAD
c060042 (HEAD) Adding 2.txt
fc791a8 Adding 1.txt

Instead of just dropping #6 it dropped #6, #7 & #8!

So I'm really curious what does && : do here because in classical bash : is a no-op and I'm used to using it with || to skip an error (say if set -e was in effect or in a makefile rule - but no idea what it's purpose is with && or if it has special meaning in git-config

@eyalroz
Copy link

eyalroz commented Jul 2, 2024

That does seem kind of weird. It must be a git-config semantics thing.

@nhed
Copy link
Author

nhed commented Jul 3, 2024

With GIT_TRACE=1 (and now using the same hash after reset so things are repeatable, didn't do that earlier)

$ GIT_TRACE=1 git drop-commit-with e296940
20:16:16.430273 git.c:750               trace: exec: git-drop-commit-with e296940
20:16:16.430355 run-command.c:657       trace: run_command: git-drop-commit-with e296940
20:16:16.431360 run-command.c:657       trace: run_command: 'git rebase --onto $1^ $1 && :' e296940
20:16:16.437961 git.c:463               trace: built-in: git rebase --onto e296940^ e296940
20:16:16.444331 run-command.c:657       trace: run_command: gpg2 --status-fd=2 -bsau 'Nevo Hed <nhed+github@starry.com>'
20:16:16.582725 run-command.c:657       trace: run_command: gpg2 --status-fd=2 -bsau 'Nevo Hed <nhed+github@starry.com>'
20:16:16.707207 run-command.c:657       trace: run_command: gpg2 --status-fd=2 -bsau 'Nevo Hed <nhed+github@starry.com>'
20:16:16.830922 run-command.c:657       trace: run_command: git notes copy --for-rewrite=rebase
20:16:16.832208 git.c:463               trace: built-in: git notes copy --for-rewrite=rebase
Successfully rebased and updated detached HEAD.

vs

$ GIT_TRACE=1 git drop-commit-without  e296940
20:17:44.674716 git.c:750               trace: exec: git-drop-commit-without e296940
20:17:44.674831 run-command.c:657       trace: run_command: git-drop-commit-without e296940
20:17:44.675893 run-command.c:657       trace: run_command: 'git rebase --onto $1^ $1' e296940
20:17:44.686734 git.c:463               trace: built-in: git rebase --onto e296940^ e296940 e296940
Successfully rebased and updated detached HEAD.

so in the failing case the hash gets rendered one additional time

@nhed
Copy link
Author

nhed commented Jul 3, 2024

So it's not some weird handling of : in git but seems rather to be how it handles aliases in general
it executes /bin/sh with the following args:

  • -c then
  • one string that has the body of the alias + "$@" (the args you provide to the alias, not yet expanded)
  • then argv[0] (which is ignored in our case but is basically a repeat of the alias body)
  • then all the args we provided

To simplify we can compare to these two

$ /bin/sh "-c" 'echo $1 $1 && : "$@"' '<ignored>' "a" "b" "c"
a a

$ /bin/sh "-c" 'echo $1 $1 "$@"' '<ignored>' "a" "b" "c"
a a a b c

In the echo case the the && terminates the echo args and the : is indeed the aforementioned no-op operator helping to separate the expanded $@ args making sure they drop on the floor. I presume that && : was selected over other options such as ; : or just # so that errors are clearly propagated.

So in summary - git does not know/care about the dollars in the alias and the && : is just there to block additional args from expanding.

To me it seems counter intuitive and dangerous detail to have to remember - most of my aliases are written in function syntax so it threw me for a loop when when I saw the $1.

In short - my gitconfig alias for this will be

drop-commit = "!f() { git rebase --onto ${1}^ ${1}; }; f"

Which is a tad uglier but usually I have them on multiple lines and the more complex ones are implemented as stand-alone scripts.

Maybe it's time for a more native git function syntax (even if just syntactic sugar,)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment