[git] How to modify a specified commit?

I usually submit a list of commits for review. If I have the following commits:

  1. HEAD
  2. Commit3
  3. Commit2
  4. Commit1

...I know that I can modify head commit with git commit --amend. But how can I modify Commit1, given that it is not the HEAD commit?

This question is related to git git-rewrite-history

The answer is


To get a non-interactive command, put a script with this content in your PATH:

#!/bin/sh
#
# git-fixup
# Use staged changes to modify a specified commit
set -e
cmt=$(git rev-parse $1)
git commit --fixup="$cmt"
GIT_EDITOR=true git rebase -i --autosquash "$cmt~1"

Use it by staging your changes (with git add) and then run git fixup <commit-to-modify>. Of course, it will still be interactive if you get conflicts.


Completely non-interactive command(1)

I just thought I'd share an alias that I'm using for this. It's based on non-interactive interactive rebase. To add it to your git, run this command (explanation given below):

git config --global alias.amend-to '!f() { SHA=`git rev-parse "$1"`; git commit --fixup "$SHA" && GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^"; }; f'

Or, a version that can also handle unstaged files (by stashing and then un-stashing them):

git config --global alias.amend-to '!f() { SHA=`git rev-parse "$1"`; git stash -k && git commit --fixup "$SHA" && GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^" && git stash pop; }; f'

The biggest advantage of this command is the fact that it's no-vim.


(1)given that there are no conflicts during rebase, of course

Usage

git amend-to <REV> # e.g.
git amend-to HEAD~1
git amend-to aaaa1111

The name amend-to seems appropriate IMHO. Compare the flow with --amend:

git add . && git commit --amend --no-edit
# vs
git add . && git amend-to <REV>

Explanation

  • git config --global alias.<NAME> '!<COMMAND>' - creates a global git alias named <NAME> that will execute non-git command <COMMAND>
  • f() { <BODY> }; f - an "anonymous" bash function.
  • SHA=`git rev-parse "$1"`; - converts the argument to git revision, and assigns the result to variable SHA
  • git commit --fixup "$SHA" - fixup-commit for SHA. See git-commit docs
  • GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^"
    • git rebase --interactive "$SHA^" part has been covered by other answers.
    • --autosquash is what's used in conjunction with git commit --fixup, see git-rebase docs for more info
    • GIT_SEQUENCE_EDITOR=true is what makes the whole thing non-interactive. This hack I learned from this blog post.

I solved this,

1) by creating new commit with changes i want..

r8gs4r commit 0

2) i know which commit i need to merge with it. which is commit 3.

so, git rebase -i HEAD~4 # 4 represents recent 4 commit (here commit 3 is in 4th place)

3) in interactive rebase recent commit will located at bottom. it will looks alike,

pick q6ade6 commit 3
pick vr43de commit 2
pick ac123d commit 1
pick r8gs4r commit 0

4) here we need to rearrange commit if you want to merge with specific one. it should be like,

parent
|_child

pick q6ade6 commit 3
f r8gs4r commit 0
pick vr43de commit 2
pick ac123d commit 1

after rearrange you need to replace p pick with f (fixup will merge without commit message) or s (squash merge with commit message can change in run time)

and then save your tree.

now merge done with existing commit.

Note: Its not preferable method unless you're maintain on your own. if you have big team size its not a acceptable method to rewrite git tree will end up in conflicts which you know other wont. if you want to maintain you tree clean with less commits can try this and if its small team otherwise its not preferable.....


git stash + rebase automation

For when I need to modify an old commit a lot of times for Gerrit reviews, I've been doing:

git-amend-old() (
  # Stash, apply to past commit, and rebase the current branch on to of the result.
  current_branch="$(git rev-parse --abbrev-ref HEAD)"
  apply_to="$1"
  git stash
  git checkout "$apply_to"
  git stash apply
  git add -u
  git commit --amend --no-edit
  new_sha="$(git log --format="%H" -n 1)"
  git checkout "$current_branch"
  git rebase --onto "$new_sha" "$apply_to"
)

GitHub upstream.

Usage:

  • modify source file, no need to git add if already in repo
  • git-amend-old $old_sha

I like this over --autosquash as it does not squash other unrelated fixups.


If for some reason you don't like interactive editors, you can use git rebase --onto.

Say you want to modify Commit1. First, branch from before Commit1:

git checkout -b amending [commit before Commit1]

Second, grab Commit1 with cherry-pick:

git cherry-pick Commit1

Now, amend your changes, creating Commit1':

git add ...
git commit --amend -m "new message for Commit1"

And finally, after having stashed any other changes, transplant the rest of your commits up to master on top of your new commit:

git rebase --onto amending Commit1 master

Read: "rebase, onto the branch amending, all commits between Commit1 (non-inclusive) and master (inclusive)". That is, Commit2 and Commit3, cutting the old Commit1 out entirely. You could just cherry-pick them, but this way is easier.

Remember to clean up your branches!

git branch -d amending

Came to this approach (and it is probably exactly the same as using interactive rebase) but for me it's kind of straightforward.

Note: I present this approach for the sake of illustration of what you can do rather than an everyday alternative. Since it has many steps (and possibly some caveats.)

Say you want to change commit 0 and you are currently on feature-branch

some-commit---0---1---2---(feature-branch)HEAD

Checkout to this commit and create a quick-branch. You can also clone your feature branch as a recovery point (before starting).

?(git checkout -b feature-branch-backup)
git checkout 0
git checkout -b quick-branch

You will now have something like this:

0(quick-branch)HEAD---1---2---(feature-branch)

Stage changes, stash everything else.

git add ./example.txt
git stash

Commit changes and checkout back to feature-branch

git commit --amend
git checkout feature-branch

You will now have something like this:

some-commit---0---1---2---(feature-branch)HEAD
           \
             ---0'(quick-branch)

Rebase feature-branch onto quick-branch (resolve any conflicts along the way). Apply stash and remove quick-branch.

git rebase quick-branch
git stash pop
git branch -D quick-branch

And you end up with:

some-commit---0'---1'---2'---HEAD(feature-branch)

Git will not duplicate (although I can't really say to what extent) the 0 commit when rebasing.

Note: all commit hashes are changed starting from the commit we originally intended to change.


Run:

$ git rebase --interactive commit_hash^

each ^ indicates how many commits back you want to edit, if it's only one (the commit hash that you specified), then you just add one ^.

Using Vim you change the words pick to reword for the commits you want to change, save and quit(:wq). Then git will prompt you with each commit that you marked as reword so you can change the commit message.

Each commit message you have to save and quit(:wq) to go to the next commit message

If you want to exit without applying the changes, press :q!

EDIT: to navigate in vim you use j to go up, k to go down, h to go left, and l to go right( all this in NORMAL mode, press ESC to go to NORMAL mode ). To edit a text, press i so that you enter the INSERT mode, where you insert text. Press ESC to go back to NORMAL mode :)

UPDATE: Here's a great link from github listing How to undo (almost) anything with git


If you haven't already pushed the commits then you can go back to a previous commit using git reset HEAD^[1,2,3,4...]

For example

git commit <file1> -m "Updated files 1 and 2"
git commit <file3> -m "Updated file 3"

Oops, forgot to add file2 to the first commit...

git reset HEAD^1 // because I only need to go back 1 commit

git add <file2>

This will add file2 to the first commit.


Well, this solution might sound very silly, but can save you in certain conditions.

A friend of mine just ran into accidentally committing very some huge files (four auto-generated files ranging between 3GB to 5GB each) and then made some additional code commits on top of that before realizing the problem that git push wasn't working any longer!

The files had been listed in .gitignore but after renaming the container folder, they got exposed and committed! And now there were a few more commits of the code on top of that, but push was running forever (trying to upload GB of data!) and finally would fail due to Github's file size limits.

The problem with interactive rebase or anything similar was that they would deal with poking around these huge files and would take forever to do anything. Nevertheless, after spending almost an hour in the CLI, we weren't sure if the files (and deltas) are actually removed from the history or simply not included in the current commits. The push wasn't working either and my friend was really stuck.

So, the solution I came up with was:

  1. Rename current git folder to ~/Project-old.
  2. Clone the git folder again from github (to ~/Project).
  3. Checkout to the same branch.
  4. Manually cp -r the files from ~/Project-old folder to ~/Project.
  5. Make sure the massive files, that are not needed to be checked in are mved, and included in .gitignore properly.
  6. Also make sure you don't overwrite .git folder in the recently-cloned ~/Project by the old one. That's where the logs of the problematic history lives!
  7. Now review the changes. It should be the union of all the recent commits, excluding the problematic files.
  8. Finally commit the changes, and it's good to be push'ed.

The biggest problem with this solution is, it deals with manual copying some files, and also it merges all the recent commits into one (obviously with a new commit-hash.) B

The big benefits are that, it is very clear in every step, it works great for huge files (as well as sensitive ones), and it doesn't leave any trace in history behind!


The best option is to use "Interactive rebase command".

The git rebase command is incredibly powerful. It allows you to edit commit messages, combine commits, reorder them ...etc.

Every time you rebase a commit a new SHA will be created for each commit regardless of the content will be changed or not! You should be careful when to use this command cause it may have drastic implications especially if you work in collaboration with other developers. They may start working with your commit while you're rebasing some. After you force to push the commits they will be out of sync and you may find out later in a messy situation. So be careful!

It's recommended to create a backup branch before rebasing so whenever you find things out of control you can return back to the previous state.

Now how to use this command?

git rebase -i <base> 

-i stand for "interactive". Note that you can perform a rebase in non-interactive mode. ex:

#interactivly rebase the n commits from the current position, n is a given number(2,3 ...etc)
git rebase -i HEAD~n 

HEAD indicates your current location(can be also branch name or commit SHA). The ~n means "n beforeé, so HEAD~n will be the list of "n" commits before the one you are currently on.

git rebase has different command like:

  • p or pick to keep commit as it is.
  • r or reword: to keep the commit's content but alter the commit message.
  • s or squash: to combine this commit's changes into the previous commit(the commit above it in the list).
  • ... etc.

    Note: It's better to get Git working with your code editor to make things simpler. Like for example if you use visual code you can add like this git config --global core.editor "code --wait". Or you can search in Google how to associate you preferred your code editor with GIT.

Example of git rebase

I wanted to change the last 2 commits I did so I process like this:

  1. Display the current commits:
    #This to show all the commits on one line
    $git log --oneline
    4f3d0c8 (HEAD -> documentation) docs: Add project description and included files"
    4d95e08 docs: Add created date and project title"
    eaf7978 (origin/master , origin/HEAD, master) Inital commit
    46a5819 Create README.md
    
  2. Now I use git rebase to change the 2 last commits messages: $git rebase -i HEAD~2 It opens the code editor and show this:

    pick 4d95e08 docs: Add created date and project title
    pick 4f3d0c8 docs: Add project description and included files
    
    # Rebase eaf7978..4f3d0c8 onto eaf7978 (2 commands)
    #
    # Commands:
    # p, pick <commit> = use commit
    # r, reword <commit> = use commit, but edit the commit message
    ...
    

    Since I want to change the commit message for this 2 commits. So I will type r or reword in place of pick. Then Save the file and close the tab. Note that rebase is executed in a multi-step process so the next step is to update the messages. Note also that the commits are displayed in reverse chronological order so the last commit is displayed in that one and the first commit in the first line and so forth.

  3. Update the messages: Update the first message:

    docs: Add created date and project title to the documentation "README.md"
    
    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    ...
    

    save and close Edit the second message

    docs: Add project description and included files to the documentation "README.md"
    
    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    ...
    

    save and close.

  4. You will get a message like this by the end of the rebase: Successfully rebased and updated refs/heads/documentation which means that you succeed. You can display the changes:

    5dff827 (HEAD -> documentation) docs: Add project description and included files to the documentation "README.md"
    4585c68 docs: Add created date and project title to the documentation "README.md"
    eaf7978 (origin/master, origin/HEAD, master) Inital commit
    46a5819 Create README.md
    

    I wish that may help the new users :).


Automated interactive rebase edit followed by commit revert ready for a do-over

I found myself fixing a past commit frequently enough that I wrote a script for it.

Here's the workflow:

  1. git commit-edit <commit-hash>
    

    This will drop you at the commit you want to edit.

  2. Fix and stage the commit as you wish it had been in the first place.

    (You may want to use git stash save to keep any files you're not committing)

  3. Redo the commit with --amend, eg:

    git commit --amend
    
  4. Complete the rebase:

    git rebase --continue
    

For the above to work, put the below script into an executable file called git-commit-edit somewhere in your $PATH:

#!/bin/bash

set -euo pipefail

script_name=${0##*/}

warn () { printf '%s: %s\n' "$script_name" "$*" >&2; }
die () { warn "$@"; exit 1; }

[[ $# -ge 2 ]] && die "Expected single commit to edit. Defaults to HEAD~"

# Default to editing the parent of the most recent commit
# The most recent commit can be edited with `git commit --amend`
commit=$(git rev-parse --short "${1:-HEAD~}")
message=$(git log -1 --format='%h %s' "$commit")

if [[ $OSTYPE =~ ^darwin ]]; then
  sed_inplace=(sed -Ei "")
else
  sed_inplace=(sed -Ei)
fi

export GIT_SEQUENCE_EDITOR="${sed_inplace[*]} "' "s/^pick ('"$commit"' .*)/edit \\1/"'
git rebase --quiet --interactive --autostash --autosquash "$commit"~
git reset --quiet @~ "$(git rev-parse --show-toplevel)"  # Reset the cache of the toplevel directory to the previous commit
git commit --quiet --amend --no-edit --allow-empty  #  Commit an empty commit so that that cache diffs are un-reversed

echo
echo "Editing commit: $message" >&2
echo

Interactive rebase with --autosquash is something I frequently use when I need to fixup previous commits deeper in the history. It essentially speeds up the process that ZelluX's answer illustrates, and is especially handy when you have more than one commit you need to edit.

From the documentation:

--autosquash

When the commit log message begins with "squash! …?" (or "fixup! …?"), and there is a commit whose title begins with the same …?, automatically modify the todo list of rebase -i so that the commit marked for squashing comes right after the commit to be modified

Assume you have a history that looks like this:

$ git log --graph --oneline
* b42d293 Commit3
* e8adec4 Commit2
* faaf19f Commit1

and you have changes that you want to amend to Commit2 then commit your changes using

$ git commit -m "fixup! Commit2"

alternatively you can use the commit-sha instead of the commit message, so "fixup! e8adec4 or even just a prefix of the commit message.

Then initiate an interactive rebase on the commit before

$ git rebase e8adec4^ -i --autosquash

your editor will open with the commits already correctly ordered

pick e8adec4 Commit2
fixup 54e1a99 fixup! Commit2
pick b42d293 Commit3

all you need to do is save and exit


For me it was for removing some credentials from a repo. I tried rebasing and ran into a ton of seemingly unrelated conflicts along the way when trying to rebase --continue. Don't bother attempting to rebase yourself, use the tool called BFG (brew install bfg) on mac.


Based on Documentation

Amending the message of older or multiple commit messages

git rebase -i HEAD~3 

The above displays a list of the last 3 commits on the current branch, change 3 to something else if you want more. The list will look similar to the following:

pick e499d89 Delete CNAME
pick 0c39034 Better README
pick f7fde4a Change the commit message but push the same commit.

Replace pick with reword before each commit message you want to change. Let say you change the second commit in the list, your file will look like the following:

pick e499d89 Delete CNAME
reword 0c39034 Better README
pick f7fde4a Change the commit message but push the same commit.

Save and close the commit list file, this will pop up a new editer for you to change your commit message, change the commit message and save.

Finaly Force-push the amended commits.

git push --force

Use the awesome interactive rebase:

git rebase -i @~9   # Show the last 9 commits in a text editor

Find the commit you want, change pick to e (edit), and save and close the file. Git will rewind to that commit, allowing you to either:

  • use git commit --amend to make changes, or
  • use git reset @~ to discard the last commit, but not the changes to the files (i.e. take you to the point you were at when you'd edited the files, but hadn't committed yet).

The latter is useful for doing more complex stuff like splitting into multiple commits.

Then, run git rebase --continue, and Git will replay the subsequent changes on top of your modified commit. You may be asked to fix some merge conflicts.

Note: @ is shorthand for HEAD, and ~ is the commit before the specified commit.

Read more about rewriting history in the Git docs.


Don't be afraid to rebase

ProTip™:   Don't be afraid to experiment with "dangerous" commands that rewrite history* — Git doesn't delete your commits for 90 days by default; you can find them in the reflog:

$ git reset @~3   # go back 3 commits
$ git reflog
c4f708b HEAD@{0}: reset: moving to @~3
2c52489 HEAD@{1}: commit: more changes
4a5246d HEAD@{2}: commit: make important changes
e8571e4 HEAD@{3}: commit: make some changes
... earlier commits ...
$ git reset 2c52489
... and you're back where you started

* Watch out for options like --hard and --force though — they can discard data.
* Also, don't rewrite history on any branches you're collaborating on.



On many systems, git rebase -i will open up Vim by default. Vim doesn't work like most modern text editors, so take a look at how to rebase using Vim. If you'd rather use a different editor, change it with git config --global core.editor your-favorite-text-editor.