This git alias allows you to remove specific changes from a past commit / from git history and place those changes into your working directory, outside of your git history.
For example, maybe a code reviewer has identified a few files or lines that belong in their own commit or pull request. This helps you do git commit surgery on specific commits without needing to manually re-play.
git edit {commithash}
(e.g. git edit HEAD
would edit the most recent commit or git edit c52b7bfe12c2f6082a69ea339eeec95a20532fa5
would edit a specific commit)
On Linux:
edit = !"f() { EDITED_STASH=0 && GIT_SEQUENCE_EDITOR=\"sed -i '1s/^pick/edit/'\" git rebase -i \"$1~\" && git reset HEAD~1 && git add -AN && git add -p && git commit -e -C HEAD@{1} && if [ -z \"$(git status --porcelain)\" ]; then git rebase --continue; else git add -A && git stash -q && EDITED_STASH=1 && git rebase --continue && git stash pop -q && EDITED_STASH=0 && git reset; fi || (echo 'Aborting the edit...' && if [ \"$EDITED_STASH\" = \"1\" ]; then git stash drop -q; fi && git rebase --abort && git add -A && git reset); }; f"
On OSX (sed -i
requires a ''
parameter)
edit = !"f() { EDITED_STASH=0 && GIT_SEQUENCE_EDITOR=\"sed -i '' '1s/^pick/edit/'\" git rebase -i \"$1~\" && git reset HEAD~1 && git add -AN && git add -p && git commit -e -C HEAD@{1} && if [ -z \"$(git status --porcelain)\" ]; then git rebase --continue; else git add -A && git stash -q && EDITED_STASH=1 && git rebase --continue && git stash pop -q && EDITED_STASH=0 && git reset; fi || (echo 'Aborting the edit...' && if [ \"$EDITED_STASH\" = \"1\" ]; then git stash drop -q; fi && git rebase --abort && git add -A && git reset); }; f"
Since the alias is so long, I recommend creating a ~/.gitconfig
file and adding the alias using your preferred editor:
[alias]
...
{insert_alias_code_here}
The alias does the following...
- Populate a critical status tracking variable (this is used to make sure the stash is kept consistent)
EDITED_STASH=0
- Start an interactive rebase to the specified commit, replacing
pick
withedit
for the first line (i.e. specifyingedit
for the passed in commit hash).
GIT_SEQUENCE_EDITOR=\"sed -i '1s/^pick/edit/'\" git rebase -i \"$1~\"
- Remove the staged commit content
git reset HEAD~1
- Mark all new files as being intended to commit (this allows you to selectively include only a portion of the new file.)
git add -AN
- Perform a patch add against the original commit content. This is where you can decide what NOT to include.
git add -p
- Perform the commit with only the patch edited changes, using the original commit message (and allowing you to edit that message).
git commit -e -C HEAD@{1}
- Check to see if there is anything sitting around NOT staged for commit (check to see if an edit actually happened)
if [ -z \"$(git status --porcelain)\" ];
- If there were no changes (if git status --porcelain returned
""
), then continue the rebase.
then git rebase --continue;
- Otherwise there are changes left over which have been edited out of the commit. We need to:
- Stage all of those changes.
else git add -A
- Stash those changes (and denote that we have stashed)
git stash -q && EDITED_STASH=1
- Invoke the rebase
git rebase --continue
- Pop the changes into our working directory (and register that the stash is clean)
git stash pop --quiet && EDITED_STASH=0
- Unstage the changes
git reset;
- And finally, if anything goes wrong at any step of the way, we want to abort the rebase and clean up any lingering git cache and stash changes, as if none of this ever happened.
(echo 'Aborting the edit...' && if [ \"$EDITED_STASH\" = \"1\" ]; then git stash drop -q; fi && git rebase --abort && git add -A && git reset); }; f"
-
It isn't currently possible to edit away a deleted file, though it is possible to edit away an added file.
-
If you remove all changes from a commit the edit will be aborted.
-
If your edits cause a later merge conflict (for instance if you remove a file from a commit that is edited later in your rebase) then the edit will be aborted.
-
If you ctrl-C out of the git add patch view, the alias will stop short (and you will need to manually type
git rebase --abort
.
Note that there is some off-site commentary about this gist.