1

The git-rebase edit command seems to have the right idea, but we have to run git undo; git reset; to make changes available to edit....

Is there a good way to be able to cruise through an entire set of commits, pop each one into the working dir, one after the next?

Before I submit my PRs, I like to go through my commits. Instead of just looking at a read-only diff, I want to live edit each commit, as if I had just written it.

I also want the commit message to be pre-populated, don't want to go hunting for the correct commit message each time.

Devin Rhode
  • 23,026
  • 8
  • 58
  • 72

2 Answers2

1

As you mentioned, the edit cmd inside git-rebase doesn't quite do all the steps you need to do to actually edit (mutate) a commit.

First, you may decide, you don't want to look at merge conflicts.

git rebase --strategy recursive --strategy-option theirs -i <base-sha>

Since you are going to check/edit each commit... if you aren't changing too many things, you should be able to spot what is wrong about a commit, based on a previous change you did. Notably, if you added a code comment in an early commit, and saw a subsequent commit deletes that comment, you should simply restore the comment. This is easy in vscode's side by side diff view (which I use all the time).

Finally, we do need to use some sort of rebase exec command to pop changes into our working directory:

exec MSG=$(git log -1 --format=%B HEAD); git undo; git reset; echo "$MSG" > $GIT_DIR/LAST_COMMIT_MSG; echo "editing commit:\n\n$MSG\n";

Maybe, you use vscode, we can also open the edited files for you:

code $(git diff --staged --name-only)

UPDATE: This doesn't open the actual diffs, so is a waste for me, not included in final command. Maybe a vscode shortcut key would work, or if this entire review flow was simply packaged up into a vscode extension.

This exec command will always fail, so we'll want --no-reschedule-failed-exec

// Putting it all together ... there are further changes below.

GIT_SEQUENCE_EDITOR=: git rebase --exec 'MSG=$(git log -1 --format=%B HEAD); git undo; git restore --staged $(git diff --name-only --staged --diff-filter=r); echo "$MSG" > $GIT_DIR/LAST_COMMIT_MSG; echo "editing commit:\n\n$MSG\n";' --strategy recursive --no-reschedule-failed-exec --strategy-option theirs -i 315abbd5b

To cycle onto the next commit, simply run:

git add --all && git commit && git rebase --continue

We'll then need this prepare-commit-msg script to re-use the LAST_COMMIT_MSG file:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

COMMIT_MSG_FILE=$1
# COMMIT_SOURCE=$2
# SHA1=$3

if [ -f $GIT_DIR/LAST_COMMIT_MSG ]; then
  cat $GIT_DIR/LAST_COMMIT_MSG $COMMIT_MSG_FILE > temp_commit_msg && mv temp_commit_msg $COMMIT_MSG_FILE
  rm $GIT_DIR/LAST_COMMIT_MSG
fi

Add this few hooks to wipe any stale LAST_COMMIT_MSG:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

echo "some git-hook: wiping $GIT_DIR/LAST_COMMIT_MSG: $(cat $GIT_DIR/LAST_COMMIT_MSG)"
rm $GIT_DIR/LAST_COMMIT_MSG

This way, you just run git commit when you are done editing a commit, and, you'll get to re-use/tweak the original commit message.

If using husky, you'll need to:

echo ".husky/prepare-commit-msg" >> $(git rev-parse --show-toplevel)/../main-worktree/.git/info/exclude
echo ".husky/pre-rebase" >> $(git rev-parse --show-toplevel)/../main-worktree/.git/info/exclude
echo ".husky/post-rewrite" >> $(git rev-parse --show-toplevel)/../main-worktree/.git/info/exclude
echo ".husky/post-commit" >> $(git rev-parse --show-toplevel)/../main-worktree/.git/info/exclude
 # paste it in:
code -n .husky/prepare-commit-msg .husky/pre-rebase .husky/post-rewrite .husky/post-commit
chmod +x .husky/prepare-commit-msg
chmod +x .husky/pre-rebase
chmod +x .husky/post-rewrite
chmod +x .husky/post-commit

UPDATE: This whole command becomes quite a bear, so let's use an alias to clean it up: prints command 3 times, filling up half of screen

git config --global --edit

Add few aliases:

  next = "!sh -c 'git add --all && git commit $@ && git rebase --continue' -"
  redo = !echo "$(git log -1 --format=%B HEAD)" > $GIT_DIR/LAST_COMMIT_MSG && git undo && git restore --staged $(git diff --name-only --staged --diff-filter=ard) > /dev/null 2>&1 || true && cat $GIT_DIR/LAST_COMMIT_MSG && echo '' && git -c advice.addEmptyPathspec=false add -N $(git ls-files --others --exclude-standard) > /dev/null 2>&1 || true
  review-stack = "!GIT_SEQUENCE_EDITOR=: git rebase --exec 'git redo' --strategy recursive --no-reschedule-failed-exec --strategy-option theirs --interactive"

Finally, using alias:

git review-stack 315abbd5b

# Go to next commit, no frills:
git next --no-edit --no-verify

# If you made changes:
git next
Devin Rhode
  • 23,026
  • 8
  • 58
  • 72
  • This could be used to create a whole PR workflow, independent of your typical git hosts. Simply create an alias, `git approve-commit`, which will add some sort of "Signed-off-by: $(git config user.name)" to commit messages... Very rudimentary, yes, also not something I'll be doing anytime soon. – Devin Rhode Oct 06 '22 at 13:20
  • @torek have you ever needed to do something like this? I have a horribly confusing variable name at the bottom of my pr stack :( – Devin Rhode Oct 06 '22 at 14:03
  • git `review-stack` alias is dangerous. It probably shoudn't use `--strategy theirs`. – Devin Rhode Oct 06 '22 at 20:34
0

Prep:

  1. Add these git alias's:
git config --global --edit

Paste in

[alias]
  next = "!sh -c 'git add --all && git commit $@ && git rebase --continue' -"
  redo = !echo "$(git log -1 --format=%B HEAD)" > $GIT_DIR/LAST_COMMIT_MSG && git undo && git restore --staged $(git diff --name-only --staged --diff-filter=ard) > /dev/null 2>&1 || true && cat $GIT_DIR/LAST_COMMIT_MSG || true && echo '' && git -c advice.addEmptyPathspec=false add -N $(git ls-files --others --exclude-standard) > /dev/null 2>&1 || true

redo is a beast, I am NOT good at bash, basically cobbled this together using google + SO

  1. To pre-populate COMMIT_EDITMSG with your last commit message, setup these git hooks. Instructions here are just for husky for now, if you aren't using husky, it's even easier, just put hooks into .git/hooks dir.
# using worktrees:
LOCAL_GITIGNORE=$(git rev-parse --show-toplevel)/../main-worktree/.git/info/exclude
# not using worktrees:
LOCAL_GITIGNORE=$(git rev-parse --show-toplevel)/.git/info/exclude

echo ".husky/prepare-commit-msg" >> $LOCAL_GITIGNORE
echo ".husky/pre-rebase" >> $LOCAL_GITIGNORE
echo ".husky/post-rewrite" >> $LOCAL_GITIGNORE
echo ".husky/post-commit" >> $LOCAL_GITIGNORE
chmod +x .husky/prepare-commit-msg
chmod +x .husky/pre-rebase
chmod +x .husky/post-rewrite
chmod +x .husky/post-commit
code .husky/prepare-commit-msg .husky/pre-rebase .husky/post-rewrite .husky/post-commit

.husky/prepare-commit-msg is unique:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

COMMIT_MSG_FILE=$1
# COMMIT_SOURCE=$2
# SHA1=$3

if [ -f $GIT_DIR/LAST_COMMIT_MSG ]; then
  # vscode commits happen to run with this command:
  #   git -c user.useConfigOnly=true commit --quiet --allow-empty-message --file - [3774ms]
  # So, we won't want to use the last commit message in that case.
  # LAST_COMMIT_MSG will be cleaned up post-commit, if commit succeeds
  # 
  # TODO:
  #   if commit msg from vscode is empty (first line of $COMMIT_MSG_FILE is empty),
  #   And second line starts with a "#"
  #   then actually fill in the missing commit message!
  #   Maybe we can read `COMMIT_SOURCE=$2`
  #   instead of reading the `$COMMIT_MSG_FILE`
  #   https://www.google.com/search?q=bash+check+if+first+line+of+file+is+empty
  if [ "$(git config user.useConfigOnly)" != "true" ]; then
    cat $GIT_DIR/LAST_COMMIT_MSG $COMMIT_MSG_FILE > temp_commit_msg && mv temp_commit_msg $COMMIT_MSG_FILE
    # It's been used once, get rid of it?
    # rm $GIT_DIR/LAST_COMMIT_MSG;
    # This is cleaned up in post-commit hook.
    # So you can abort commit, edit more, re-commit, and still retain this commit message.
  fi
fi

The remaining 3 are all the same:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

if [ -f $GIT_DIR/LAST_COMMIT_MSG ]; then
  # echo "some git hook: wiping $GIT_DIR/LAST_COMMIT_MSG: $(cat $GIT_DIR/LAST_COMMIT_MSG)"
  rm $GIT_DIR/LAST_COMMIT_MSG
fi

Launch it!

git rebase --exec 'git redo' -i 315abbd5b

next commit

git next

To focus in on a certain variable name throughout a PR, you could run:

git rebase --exec 'ag -0 -l newVarNameOfInterest app/greenfield/project && git redo || echo "ok"' -i 315abbd5b

to install ag run brew install the_silver_searcher

Devin Rhode
  • 23,026
  • 8
  • 58
  • 72
  • This could be used to create a PR workflow, independent of your typical git hosts. Simply create an alias, `git approve-commit` (similar to `git next`), which will add some sort of `"Signed-off-by: $(git config user.name)"` to the end of commit messages. May want to use `git blame-someone-else` to retain original authorship. – Devin Rhode Oct 07 '22 at 02:49
  • Lastly, it would be nice if there was a way to tell vscode to open all the files you want to edit in a pure diff-like view like github.com. Basically, make the file buffers non-scrollable, and scrolling actually moves you through diffs. And, of course, why not integrate commenting on diffs too, while we are at it. – Devin Rhode Oct 07 '22 at 02:51
  • This can be nicely simplified by using the `git commit --reedit-message=` flag, this would also fix the loss of newlines, and retain timestamps of original authors. – Devin Rhode Oct 18 '22 at 01:30
  • It seems that `exec git undo --soft` has this issue: https://stackoverflow.com/questions/74338182/configure-git-rebase-to-not-continue-if-there-are-new-untracked-files - which `git redo` doesn't have (via the --intent-to-add flag) – Devin Rhode Nov 06 '22 at 17:33