loading...

Git – Altering Commits

How to Change the server name on Windows Server 2019

A commit records the history of your work and keeps your
changes sacrosanct, but the commit itself isn’t cast in stone. Git provides
several tools and commands specifically designed to help you modify and
improve the commit history cataloged within your repository.

There are many valid reasons why you might modify or rework a commit
or your overall commit sequence.

  • You can fix a problem before it becomes a legacy.

  • You can decompose a large, sweeping change into a number of small,
    thematic commits. Conversely, you can combine individual changes into a
    larger commit.

  • You can incorporate review feedback and suggestions.

  • You can reorder commits into a sequence that doesn’t break a build
    requirement.

  • You can order commits into a more logical sequence.

  • You can remove debug code committed accidentally.

As you’ll see in Chapter 12, which
explains how to share a repository, there are many more reasons to change
commits prior to publishing your repository.

In general, you should feel empowered to alter a commit or a commit
sequence if your effort makes it cleaner and more understandable. Of course,
as with all of software development, there is a trade-off between repeated
overrefinement and acceptance of something that is satisfactory. You should
strive for clean, well-structured patches that have concise meaning for both
you and your collaborators. However, there comes a time when good enough is
good enough.

Philosophy of Altering History

When it comes to manipulating the development history, there
are several schools of thought.

One philosophy might be termed realistic history: every commit is
retained and nothing is altered.

One variant is a fine-grained realistic history, where you
commit every change as soon as possible, ensuring that each and every step
is saved for posterity. Another option is didactic realistic history,
where you take your time and commit your best work only at convenient and
suitable moments.

Given the opportunity to adjust the history—possibly cleaning up a
bad intermediate design decision or rearranging commits into a more
logical flow—you can create a more idealistic
history.

As a developer, you may find value in the full, fine-grained
realistic history, because it might provide archaeological details on how
some good or bad idea developed. A complete narrative may provide insight into
the introduction of a bug, or explicate a meticulous bug fix. In fact, an
analysis of the history may even yield insight into how a developer or
team of developers works and how the development process can be
improved.

Many of those details might be lost if a revised history removes
intermediate steps. Was a developer able to simply intuit such a good
solution? Or did it take several iterations of refinement? What is the
root cause of a bug? If the intermediate steps are not captured in the
commit history, answers to those types of questions may be lost.

On the other hand, having a clean history showing well-defined
steps, each with logical forward progress, can often be a joy to read and
a pleasure to work with. There is, moreover, no need to worry about the
vagaries of a possibly broken or suboptimal step in the repository
history. Also, other developers reading the history may thereby learn a
better development technique and style.

So is a detailed realistic history without information loss the best
approach? Or is a clean history better? Perhaps an intermediate
representation of the development is warranted. Or, with a clever use of
Git branches, perhaps you could represent both a fine-grained realistic
history and an idealized history in the same repository.

Git gives you the ability to clean up the actual history and turn it
into a more idealized or cleaner one before it is published or committed
to public record. Whether you choose to do so, to keep a detailed record
without alteration, or to pick some middle ground is entirely up to you
and your project policies.

Caution About Altering History

As a general guideline, you should feel free to alter and
improve your repository commit history as long as no other
developer[22] has obtained a copy of your repository. Or, to be more
pedantic, you can alter a specific branch of your repository as long as no
one has a copy of that branch. The notion to keep in mind is you shouldn’t
rewrite, alter, or change any part of a branch that’s been made available
and might be present in a different repository.

For example, let’s say you’ve worked on your master branch and made commits A through D
available to another developer, as shown in Figure 10-1. Once you make your
development history available to another developer, that chronicle is
known as a published history.

Figure 10-1. Your published history

Let’s say you then do further development and produce new commits
W through Z as unpublished history on the same branch.
This is pictured in Figure 10-2.

Figure 10-2. Your unpublished history

In this situation, you should be very careful to leave commits
earlier than W alone. However, until
you republish your master branch, there
is no reason you can’t modify the commits W through Z.
This could include reordering, combining, and removing one or more commits
or, obviously, adding even more commits as new development.

You might end up with a new and improved commit history, as depicted
in Figure 10-3. In this example, commits
X and Y have been combined into one new commit; commit
W has been slightly altered to yield a
new, similar commit W'; commit Z has been moved earlier in the history; and new
commit P has been introduced.

Figure 10-3. Your new history

This chapter explores techniques to help you alter and improve your
commit history. It is for you to judge whether the new history is better,
when the history is good enough, and when the history is ready to be
published.

Using git reset

The git reset command
changes your repository and working directory to a known state.
Specifically, git reset adjusts the
HEAD ref to a given
commit and, by default, updates the index to
match that commit. If desired, git
reset
can also modify your working directory to mirror the
revision of your project represented by the given commit.

You might construe git reset as
destructive because it can overwrite and destroy changes in
your working directory. Indeed, data can be lost. Even if you have a
backup of your files, you might not be able to recover your work. However,
the whole point of this command is to establish and recover known states
for the HEAD, index, and working
directory.

The git reset command has three
main options: --soft, --mixed, and
--hard.

git reset –soft
commit

The --soft changes the HEAD ref to point to the given
commit. The contents of your index and
working directory are left unchanged. This version of the command
has the least effect, changing only the state of a
symbolic reference so it points to a new commit.

git reset –mixed
commit

--mixed changes HEAD to point to the given
commit. Your index contents are also
modified to align with the tree structure named by
commit, but your working directory contents are left unchanged.
This version of the command leaves your index as if you had just
staged all the changes represented by
commit, and it tells you what remains
modified in your working directory.

Note that --mixed is the default mode for
git reset.

git reset –hard
commit

This variant changes the HEAD ref to point to the given
commit. The contents of your index are
also modified to agree with the tree structure named by the named
commit. Furthermore, your working
directory contents are changed to reflect the state of the tree
represented by the given commit.

When changing your working directory, the complete directory
structure is altered to correspond to the given
commit. Modifications are lost and new
files are removed. Files that are in the given
commit but no longer exist in your
working directory are reinstated.

These effects are summarized in Table 10-1.

Table 10-1. git reset option effects
Option HEAD Index Working directory
--soft Yes No No
--mixed Yes Yes No
--hard Yes Yes Yes

The git reset command also saves
the original HEAD value in the ref
ORIG_HEAD. This is useful, for example,
if you wish to use that original HEAD’s
commit log message as the basis for some follow-up commit.

In terms of the object model, git
reset
moves the current branch HEAD within the commit graph to a specific
commit. If you specify --hard, your working directory is
transformed as well.

Let’s look at some examples of how git
reset
operates.

In the following example, the file foo.c has been accidentally staged in the
index. Using git status reveals that it
will be committed:

    $ git add foo.c
    # Oops!  Didn't mean to add foo.c!

    $ git status
    # On branch master
    # Changes to be committed:
    #   (use "git reset HEAD <file>..." to unstage)
    #
    #    new file:   foo.c
    #

As suggested, to avoid committing the file, use git reset HEAD to unstage it:

    $ git ls-files
    foo.c
    main.c

    $ git reset HEAD foo.c

    $ git ls-files
    main.c

In the commit represented by HEAD, there is no pathname foo.c (or else git add
foo.c
would be superfluous). Here, git
reset
on HEAD for foo.c might be paraphrased as With
respect to file foo.c, make my index
look like it did in HEAD, where it
wasn’t present.
Or, in other words, Remove foo.c from the
index.

Another common use for git
reset
is to simply redo or eliminate the topmost commit on a
branch. As an example, let’s set up a branch with two commits on
it.

    $ git init
    Initialized empty Git repository in /tmp/reset/.git/
    $ echo foo >> master_file
    $ git add master_file 
    $ git commit
    Created initial commit e719b1f: Add master_file to master branch.
     1 files changed, 1 insertions(+), 0 deletions(-)
     create mode 100644 master_file

    $ echo "more foo" >> master_file
    $ git commit master_file 
    Created commit 0f61a54: Add more foo.
     1 files changed, 1 insertions(+), 0 deletions(-)

    $ git show-branch --more=5
    [master] Add more foo.
    [master^] Add master_file to master branch.

Suppose you now realize that the second commit is wrong and you want
to go back and do it differently. This is a classic application of
git reset –mixed HEAD^. Recall (from
Identifying Commits of Chapter 6) that HEAD^
references the commit parent of the current master HEAD and represents the state immediately
prior to completing the second, faulty commit.

    # --mixed is the default
    $ git reset HEAD^
    master_file: locally modified

    $ git show-branch --more=5
    [master] Add master_file to master branch.

    $ cat master_file 
    foo
    more foo

After git reset HEAD^, Git has
left the new state of the master_file
and the entire working directory just as it was immediately prior to
making the Add more foo commit.

Because the --mixed option resets the index, you
must restage any changes you want in the new commit. This gives you the
opportunity to reedit master_file,
add other files, or perform other changes before making a new
commit.

    $ echo "even more foo" >> master_file
    $ git commit master_file 
    Created commit 04289da: Updated foo.
     1 files changed, 2 insertions(+), 0 deletions(-)

    $ git show-branch --more=5
    [master] Updated foo.
    [master^] Add master_file to master branch.

Now only two commits have been made on the
master branch, not three.

Similarly, if you have no need to change the index (because
everything was staged correctly) but you want to adjust the commit
message, then you can use --soft instead:

    $ git reset --soft HEAD^
    $ git commit

The git reset –soft
HEAD^
command moves you back to the prior place in the commit
graph but keeps the index exactly the same. Everything is staged just as
it was prior to the git reset command.
You just get another shot at the commit message.

Tip

But now that you understand that command, don’t use it. Instead,
read about git commit –amend , which
follows!

Suppose, however, that you want to eliminate the second
commit entirely and don’t care about its content. In this case, use the
--hard option:

    $ git reset --hard HEAD^
    HEAD is now at e719b1f Add master_file to master branch.

    $ git show-branch --more=5
    [master] Add master_file to master branch.

Just as with --mixed, the --hard
option has the effect of pulling the master branch back to its immediately prior
state. It also modifies the working directory to mirror the prior
( HEAD^) state as well. Specifically,
the state of the master_file in your
working directory is modified to again contain just the one, original
line:

    $ cat master_file
    foo

Although the examples all use HEAD in some form, you can apply git reset to any commit in the repository. For
example, to eliminate several commits on your current branch, you could
use git reset –hard HEAD~3 or even
git reset –hard master~3.

But be careful. Just because you can name other commits using a
branch name, this is not the same as checking the branch out. Throughout
the git reset operation, you remain on
the same branch. You can alter your working directory to look
like
the head of a different branch, but you are still on your
original branch.

To illustrate the use of git
reset
with other branches, let’s add a second branch called
dev and add a new file to it.

    # Should already be on master, but be sure.
    $ git checkout master
    Already on "master"

    $ git checkout -b dev
    $ echo bar >> dev_file
    $ git add dev_file
    $ git commit
    Created commit 7ecdc78: Add dev_file to dev branch
     1 files changed, 1 insertions(+), 0 deletions(-)
     create mode 100644 dev_file

Back on the master branch, there
is only one file:

    $ git checkout master
    Switched to branch "master"

    $ git rev-parse HEAD
    e719b1fe81035c0bb5e1daaa6cd81c7350b73976

    $ git rev-parse master
    e719b1fe81035c0bb5e1daaa6cd81c7350b73976

    $ ls
    master_file

By using --soft, only the HEAD reference is changed.

    # Change HEAD to point to the dev commit
    $ git reset --soft dev

    $ git rev-parse HEAD
    7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f

    $ ls
    master_file

    $ git show-branch
    ! [dev] Add dev_file to dev branch
     * [master] Add dev_file to dev branch
    --
    +* [dev] Add dev_file to dev branch

It certainly seems as if the master branch and the dev branch are at the same commit. And, to a
limited extent, they are—you’re still on the master branch, and that’s good—but doing this
operation leaves things in a peculiar state. To wit, if you made a commit
now, what would happen? The HEAD points
to a commit that has the file dev_file in it, but that file isn’t in the
master branch.

    $ echo "Funny" >> new
    $ git add new
    $ git commit -m "Which commit parent?"
    Created commit f48bb36: Which commit parent?
     2 files changed, 1 insertions(+), 1 deletions(-)
     delete mode 100644 dev_file
     create mode 100644 new

    $ git show-branch
    ! [dev] Add dev_file to dev branch
     * [master] Which commit parent?
    --
     * [master] Which commit parent?
    +* [dev] Add dev_file to dev branch

Git correctly added new and has
evidently determined that dev_file
isn’t present in this commit. But why did Git remove
this dev_file? Git is correct that
dev_file isn’t part of this commit,
but it’s misleading to say that it was removed because it was never there
in the first place! So why did Git elect to remove the file? The answer is
that Git uses the commit to which HEAD
points at the time a new commit is made. Let’s see what that was:

    $ git cat-file -p HEAD
    tree 948ed823483a0504756c2da81d2e6d8d3cd95059
    parent 7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f
    author Jon Loeliger <jdl@example.com> 1229631494 -0600
    committer Jon Loeliger <jdl@example.com> 1229631494 -0600

    Which commit parent?

The parent of this commit is 7ecdc7, which you can see is the tip of the
dev branch and not master. But this commit was made while on the
master branch. The mix-up shouldn’t
come as a surprise, because master HEAD
was changed to point at the dev
HEAD
!

At this point, you might conclude that the last commit is totally
bogus and should be removed entirely. And well you should. It is a
confused state that shouldn’t be allowed to remain in the
repository.

Just as the earlier example showed, this seems like an
excellent opportunity for the git reset –hard
HEAD^
command. But now things are in a bit of pickle.

The obvious approach to get to the previous version of the master HEAD is simply to use HEAD^, like this:

    # Make sure we're on the master branch first
    $ git checkout master

    # BAD EXAMPLE!
    # Reset back to master's prior state
    $ git reset --hard HEAD^

So what’s the problem? You just saw that HEAD’s parent points to dev and not to the prior
commit on the original master
branch.

    # Yep, HEAD^ points to the dev HEAD.  Darn.
    $ git rev-parse HEAD^
    7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f

There are several ways of determining the commit to which
the master branch should, in fact, be
reset.

    $ git log
    commit f48bb36016e9709ccdd54488a0aae1487863b937
    Author: Jon Loeliger <jdl@example.com>
    Date:   Thu Dec 18 14:18:14 2008 -0600

    Which commit parent?

    commit 7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f
    Author: Jon Loeliger <jdl@example.com>
    Date:   Thu Dec 18 13:05:08 2008 -0600

    Add dev_file to dev branch

    commit e719b1fe81035c0bb5e1daaa6cd81c7350b73976
    Author: Jon Loeliger <jdl@example.com>
    Date:   Thu Dec 18 11:44:45 2008 -0600

    Add master_file to master branch.

The last commit ( e719b1f) is the
correct one.

Another method uses the reflog, which
is a history of changes to refs within your repository.

    $ git reflog
    f48bb36... HEAD@{0}: commit: Which commit parent?
    7ecdc78... HEAD@{1}: dev: updating HEAD
    e719b1f... HEAD@{2}: checkout: moving from dev to master
    7ecdc78... HEAD@{3}: commit: Add dev_file to dev branch
    e719b1f... HEAD@{4}: checkout: moving from master to dev
    e719b1f... HEAD@{5}: checkout: moving from master to master
    e719b1f... HEAD@{6}: HEAD^: updating HEAD
    04289da... HEAD@{7}: commit: Updated foo.
    e719b1f... HEAD@{8}: HEAD^: updating HEAD
    72c001c... HEAD@{9}: commit: Add more foo.
    e719b1f... HEAD@{10}: HEAD^: updating HEAD
    0f61a54... HEAD@{11}: commit: Add more foo.

Reading through this list, the third line down records a switch from
the dev branch to the master branch. At that time, e719b1f was the master
HEAD
. So, once again, you could directly use e719b1f or you could use the symbolic name
HEAD@{2}.

    $ git rev-parse HEAD@{2}
    e719b1fe81035c0bb5e1daaa6cd81c7350b73976

    $ git reset --hard HEAD@{2}
    HEAD is now at e719b1f Add master_file to master branch.

    $ git show-branch
    ! [dev] Add dev_file to dev branch
     * [master] Add master_file to master branch.
    --
    +  [dev] Add dev_file to dev branch
    +* [master] Add master_file to master branch.

As just shown, the reflog can frequently be used to help locate
prior state information for refs such as branch names.

Similarly, it is wrong to try and change branches using
git reset –hard.

    $ git reset --hard dev
    HEAD is now at 7ecdc78 Add dev_file to dev branch

    $ ls
    dev_file  master_file

Again, this appears to be correct. In this
case, the working directory has even been populated with the correct files
from the dev branch. But it didn’t
really work. The master branch remains
current.

    $ git branch
      dev
    * master

Just as in the previous example, a commit at this point would cause
the graph to be confused. And, as before, the proper action is to
determine the correct state and reset to that:

    $ git reset --hard e719b1f

Or, possibly, even:

    $ git reset --soft e719b1f

Using --soft, the working directory is not
modified, which means that your working directory now represents the total
content (files and directories) present in the tip of the dev branch. Furthermore, because HEAD now correctly points to the original tip of
the master branch as it used to, a
commit at this point would yield a valid graph with
the new master state identical to the
tip of the dev branch.

That may or may not be what you want, of course. But you can do
it.

Using git cherry-pick

The command git cherry-pick
commit
applies the changes introduced
by the named commit on the current branch. It
will introduce a new, distinct commit. Strictly speaking, using git cherry-pick doesn’t
alter the existing history within a repository;
instead, it adds to the history.

As with other Git operations that introduce changes via the process
of applying a diff, you may need to resolve conflicts to fully apply the
changes from the given commit.

The command git cherry-pick is
typically used to introduce particular commits from one branch within a
repository onto a different branch. A common use is to forward- or
back-port commits from a maintenance branch to a development
branch.

In Figure 10-4, the
dev branch has normal development,
whereas the rel_2.3 contains commits
for the maintenance of release 2.3.

Figure 10-4. Before git cherry-pick of one commit

During the course of normal development, a bug is fixed on the
development line with commit F. If that
bug turns out to be present in the 2.3
release also, the bug fix, F, can be
made to the rel_2.3 branch using
git cherry-pick:

    $ git checkout rel_2.3

    $ git cherry-pick dev~2    # commit F, above

After cherry-pick, the graph
resembles Figure 10-5.

Figure 10-5. After git cherry-pick of one commit

In Figure 10-5, commit
F' is substantially similar to commit
F, but it is a new commit and will have
to be adjusted—perhaps with conflict resolutions—to account for its
application to commit Z rather than
commit E. None of the commits following
F are applied after F'; only the named commit is picked and
applied.

Another common use for cherry-pick is to rebuild a series of commits by
selectively picking a batch from one branch and introducing them onto a
new branch.

Suppose you had a series of commits on your development branch,
my_dev, as shown in Figure 10-6, and you wanted to
introduce them onto the master branch
but in a substantially different order.

Figure 10-6. Before git cherry-pick shuffle

To apply them on the master
branch in the order Y, W, X,
Z, you could use the following
commands.

    $ git checkout master
    $ git cherry-pick my_dev^     # Y
    $ git cherry-pick my_dev~3    # W
    $ git cherry-pick my_dev~2    # X
    $ git cherry-pick my_dev      # Z

Afterward, your commit history would look something like Figure 10-7.

Figure 10-7. After git cherry-pick shuffle

In situations like this, where the order of commits undergoes fairly
volatile changes, it is quite likely that you will have to resolve
conflicts. It depends entirely on the relationship between the commits. If they
are highly coupled and change overlapping lines, then you will have
conflicts that need to be resolved. If they are highly independent, then
you will be able to move them around quite readily.

Originally, the git cherry-pick
command selected and reapplied one commit at a time. However, in later
versions of Git, git cherry-pick
allowed a range of commits to be selected and reapplied in a single
command. For example, the following command:

    # on branch master
    $ git cherry-pick X..Z

would apply new commits X',
Y', and Z' on the master branch. This is particularly handy in
porting or moving a large sequence of commits from one line of development
to another without necessarily using the entire source branch at one
time.

Using git revert

The git revert
commit
command is substantially
similar to the command git cherry-pick
commit
with one important difference:
it applies the inverse of the given
commit. Thus, this command is used to introduce
a new commit that reverses the effects of a given commit.

Like git cherry-pick, the
revert doesn’t
alter the existing history within a repository.
Instead it adds a new commit to the history.

A common application for git
revert
is to undo the effects of a commit that is
buried, perhaps deeply, in the history of a branch. In Figure 10-8, a history of changes have
been built up on the master branch. For some reason, perhaps through
testing, commit D has been deemed
faulty.

Figure 10-8. Before simple git revert

One way to fix the situation is to simply make edits to undo the
effects of D and then commit the
reversal directly. You might also note in your commit message that the
purpose of this commit is to revert the changes that were caused by the
earlier commit.

An easier approach is to simply run git
revert
:

    $ git revert master~3    # commit D

The result look likes Figure 10-9, where commit D' is the inverse of commit D.

Figure 10-9. After simple git revert

reset, revert, and checkout

The three Git commands reset,
revert, and checkout can be somewhat confusing, because all
appear to perform similar operations. Another reason these three commands
can be confusing is that other VCSs have different meanings for the words
reset, revert, and checkout.

However, there are some good guidelines and rules for when each
command should and should not be used.

If you want to change to a different branch, use git checkout. Your current branch and HEAD ref change to match the tip of the given
branch.

The git reset command
does not change your branch. However, if you supply the name of a branch,
it will change the state of your current working directory to
look like the tip of the named branch. In other
words, git reset is intended to reset
the current branch’s HEAD
reference.

Because git reset –hard is
designed to recover to a known state, it is also capable of clearing out
failed or stale merge efforts, whereas git
checkout
will not. Thus, if there were a pending merge commit
and you attempted to recover using git
checkout
instead of git reset
–hard
, your next commit would erroneously be a merge
commit.

The confusion with git checkout
is due to its additional ability to extract a file from the object store
and put it into your working directory, possibly replacing a version in
your working directory in the process. Sometimes the version of that file
is one corresponding to the current HEAD version and sometimes it is an earlier
version.

    # Checkout file.c from index
    $ git checkout -- path/to/file.c

    # Checkout file.c from rev v2.3
    $ git checkout v2.3 -- some/file.c

Git calls this checking out a path.

In the former case, obtaining the current version from the object
store appears to be a form of a reset operation—that is,
your local working directory edits of the file are discarded because the
file is reset to its current, HEAD
version. That is double-plus ungood Git thinking.

In the latter case, an earlier version of the file is pulled out of
the object store and placed into your working directory. This has the
appearance of being a revert operation on the file. That,
too, is double-plus ungood Git thinking.

In both cases, it is improper to think of the operation as a Git
reset or a revert. In both cases, the file is checked out
from a particular commit: HEAD and
v2.3, respectively.

The git revert command
works on full commits, not on files.

If another developer has cloned your repository or fetched some of
your commits, there are implications for changing the commit history. In
this case, you probably should not use commands that alter
history
within your repository. Instead, use git revert; do not use git reset nor the git
commit –amend
command described in the next section.

Changing the Top Commit

One of the easiest ways to alter the most recent commit on
your current branch is with git commit
–amend
. Typically, amend implies that the commit has
fundamentally the same content but some aspect
requires adjustment or tidying. The actual commit object that is
introduced into the object store will, of course, be different.

A frequent use of git commit
–amend
is to fix typos immediately after a commit. This is not
the only use, however as with any commit, this command can amend any file
or files in the repository and, indeed, can add or delete a file as part
of the new commit.

As with a normal git commit
command, git commit –amend prompts you
with an editor session in which you may also alter the commit
message.

For example, suppose you are working on a speech and made the
following recent commit:

    $ git show
    commit 0ba161a94e03ab1e2b27c2e65e4cbef476d04f5d
    Author: Jon Loeliger <jdl@example.com>
    Date:   Thu Jun 26 15:14:03 2008 -0500

    Initial speech

    diff --git a/speech.txt b/speech.txt
    new file mode 100644
    index 0000000..310bcf9
    --- /dev/null
    +++ b/speech.txt
    @@ -0,0 +1,5 @@
    +Three score and seven years ago
    +our fathers brought forth on this continent,
    +a new nation, conceived in Liberty,
    +and dedicated to the proposition
    +that all men are created equal.

At this point, the commit is stored in Git’s object repository,
albeit with small errors in the prose. To make corrections, you could
simply edit the file again and make a second commit. That would leave a
history like this:

    $ git show-branch --more=5
    [master] Fix timeline typo
    [master^] Initial speech

However, if you wish to leave a slightly cleaner commit history in
your repository, then you can alter this commit directly and replace
it.

To do this, fix the file in your working directory. Correct the
typos and add or remove files as needed. As with any commit, update the
index with your changes using commands such as git add or git
rm
. Then issue the git commit
–amend
command.

    # edit speech.txt as needed.

    $ git diff
    diff --git a/speech.txt b/speech.txt
    index 310bcf9..7328a76 100644
    --- a/speech.txt
    +++ b/speech.txt
    @@ -1,5 +1,5 @@
    -Three score and seven years ago
    +Four score and seven years ago
     our fathers brought forth on this continent,
     a new nation, conceived in Liberty,
     and dedicated to the proposition
    -that all men are created equal.
    +that all men and women are created equal.

    $ git add speech.txt

    $ git commit --amend

    # Also edit the "Initial speech" commit message if desired
    # In this example it was changed a bit...

With an amendment, anyone can see that the original commit has been
modified and that it replaces the existing commit.

    $ git show-branch --more=5
    [master] Initial speech that sounds familiar.

    $ git show
    commit 47d849c61919f05da1acf983746f205d2cdb0055
    Author: Jon Loeliger <jdl@example.com>
    Date:   Thu Jun 26 15:14:03 2008 -0500

    Initial speech that sounds familiar.

    diff --git a/speech.txt b/speech.txt
    new file mode 100644
    index 0000000..7328a76
    --- /dev/null
    +++ b/speech.txt
    @@ -0,0 +1,5 @@
    +Four score and seven years ago
    +our fathers brought forth on this continent,
    +a new nation, conceived in Liberty,
    +and dedicated to the proposition
    +that all men and women are created equal.

This command can edit the meta-information on a commit. For
example, by specifying --author you can alter the author
of the commit:

    $ git commit --amend --author "Bob Miller <kbob@example.com>"
    # ...just close the editor...

    $ git log
    commit 0e2a14f933a3aaff9edd848a862e783d986f149f
    Author: Bob Miller <kbob@example.com>
    Date:   Thu Jun 26 15:14:03 2008 -0500

    Initial speech that sounds familiar.

Pictorially, altering the top commit using git commit –amend changes the commit graph from
that shown in Figure 10-10 to that
shown in Figure 10-11.

Figure 10-10. Commit graph before git commit –amend
Figure 10-11. Commit graph after git commit –amend

Here, the substance of the C
commit is still the same, but it has been altered to obtain C'. The HEAD
ref has been changed from the old commit, C, so that it points to the replacement ref,
C'.

Rebasing Commits

The git rebase command is
used to alter where a sequence of commits is based. This command requires
at least the name of the other branch onto which your commits will be
relocated. By default, the commits from the current branch that are not
already on the other branch are rebased.

A common use for git rebase is to
keep a series of commits that you are developing up-to-date with respect
to another branch, usually a master
branch or a tracking branch from another repository.

In Figure 10-12, two branches have
been developed. Originally, the topic
branch started on the master branch
when it was at commit B. In the
meantime, it has progressed to commit E.

Figure 10-12. Before git rebase

You can keep your commit series up-to-date with respect to the
master branch by writing the commits so
that they are based on commit E rather
than B. Because the topic branch needs to be the current branch, you
can use either:

    $ git checkout topic
    $ git rebase master

or

    $ git rebase master topic

After the rebase operation is complete, the new commit graph
resembles Figure 10-13.

Figure 10-13. After git rebase

Using the git rebase
command in situations like the one shown in Figure 10-12 is often called forward
porting
. In this example, the topic branch topic has been forward ported to the master branch.

There is no magic to a rebase being a forward or a backward
port; both are possible using git
rebase
. The interpretation is usually left to a more fundamental
understanding of what functionality is considered ahead of or behind
another functionality.

In the context of a repository that you have cloned from somewhere
else, it is common to forward port your development branch or branches
onto the origin/master tracking branch
like this using the git rebase
operation. In Chapter 12, you will see
how this operation is requested frequently by a repository maintainer
using a phrase such as Please rebase your patch to the
tip-of-master.

The git rebase command
may also be used to completely transplant a line of development from one
branch to an entirely different branch using the --onto
option.

For example, suppose you’ve developed a new feature on the feature branch with the commits P and Q,
which were based on the maint branch as
shown in Figure 10-14. To
transplant the P and Q commits on the feature branch from the maint to the master branch, issue the command:

    $ git rebase --onto master maint^ feature
Figure 10-14. Before git rebase transplant

The resulting commit graph looks like Figure 10-15.

Figure 10-15. After git rebase transplant

The rebase operation relocates commits one at a time from each
respective original commit location to a new commit base. As a result,
each commit that is moved might have conflicts to resolve.

If a conflict is found, the rebase operation suspends its processing
temporarily so you can resolve the conflict. Any conflict during the
rebase process that needs to be resolved should be handled as described in
A Merge with a Conflict of Chapter 9.

Once all conflicts are resolved and the index has been
updated with the results, the rebase operation can be resumed using the
git rebase –continue command. The
command resumes its operation by committing the resolved conflict and
proceeding to the next commit in the series being rebased.

If, while inspecting a rebase conflict, you decide that this
particular commit really isn’t necessary, then you can also instruct the
git rebase command to simply skip this
commit and move to the next by using git rebase
–skip
. This may not be the correct thing to do, especially if
subsequent commits in the series really depend on the changes introduced
by this one. The problems are likely to snowball in this case, so it’s
better to truly resolve the conflict.

Finally, if the rebase
operation turns out to be the totally wrong thing to do, git rebase –abort abandons the operation and
restores the repository to the state prior to issuing the original
git rebase.

Using git rebase -i

Suppose you start writing a haiku and manage to compose
two full lines before checking it in:

    $ git init
    Initialized empty Git repository in .git/
    $ git config user.email "jdl@example.com"

    $ cat haiku
    Talk about colour
    No jealous behaviour here

    $ git add haiku
    $ git commit -m "Start my haiku"
    Created initial commit a75f74e: Start my haiku
     1 files changed, 2 insertions(+), 0 deletions(-)
     create mode 100644 haiku

Your writing continues, but you decide you really should use the
American spelling of color instead of the British. So, you make a commit
to change it:

    $ git diff
    diff --git a/haiku b/haiku
    index 088bea0..958aff0 100644
    --- a/haiku
    +++ b/haiku
    @@ -1,2 +1,2 @@
    -Talk about colour
    +Talk about color
     No jealous behaviour here

    $ git commit -a -m "Use color instead of colour"
    Created commit 3d0f83b: Use color instead of colour
     1 files changed, 1 insertions(+), 1 deletions(-)

Finally, you develop the final line and commit it:

    $ git diff
    diff --git a/haiku b/haiku
    index 958aff0..cdeddf9 100644
    --- a/haiku
    +++ b/haiku
    @@ -1,2 +1,3 @@
     Talk about color
     No jealous behaviour here
    +I favour red wine

    $ git commit -a -m "Finish my colour haiku"
    Created commit 799dba3: Finish my colour haiku
     1 files changed, 1 insertions(+), 0 deletions(-)

However, again you have spelling quandary and decide to change all
British ou spellings to the American o
spelling:

    $ git diff
    diff --git a/haiku b/haiku
    index cdeddf9..064c1b5 100644
    --- a/haiku
    +++ b/haiku
    @@ -1,3 +1,3 @@
     Talk about color
    -No jealous behaviour here
    -I favour red wine
    +No jealous behavior here
    +I favor red wine

    $ git commit -a -m "Use American spellings"
    Created commit b61b041: Use American spellings
     1 files changed, 2 insertions(+), 2 deletions(-)

At this point, you’ve accumulated a history of commits that looks
like this:

    $ git show-branch --more=4
    [master] Use American spellings
    [master^] Finish my colour haiku
    [master~2] Use color instead of colour
    [master~3] Start my haiku

After looking at the commit sequence or receiving review feedback,
you decide that you prefer to complete the haiku before correcting it
and want the following commit history:

    [master] Use American spellings
    [master^] Use color instead of colour
    [master~2] Finish my colour haiku
    [master~3] Start my haiku

But then you also notice that there’s no good reason to have two
similar commits that correct the spellings of different words. Thus, you
would also like to squash the
master and master^
into just one commit.

    [master] Use American spellings
    [master^] Finish my colour haiku
    [master~2] Start my haiku

Reordering, editing, removing, squashing multiple commits into
one, and splitting a commit into several are all easily performed by the
git rebase command using the
-i or --interactive option. This
command allows you to modify the commits that make up a branch and place
them back onto the same branch or onto a different branch.

A typical use, and one apropos for this example, modifies the same
branch in place. In this case there are three changesets between four
commits to be modified; git rebase -i
needs to be told the name of the commit beyond which you actually intend
to change.

    $ git rebase -i master~3

You will be placed in an editor on a file that looks like
this:

    pick 3d0f83b Use color instead of colour
    pick 799dba3 Finish my colour haiku
    pick b61b041 Use American spellings

    # Rebase a75f74e..b61b041 onto a75f74e
    #
    # Commands:
    #  pick = use commit
    #  edit = use commit, but stop for amending
    #  squash = use commit, but meld into previous commit
    #
    # If you remove a line here THAT COMMIT WILL BE LOST.
    # However, if you remove everything, the rebase will be aborted.
    #

The first three lines list the commits within the editable commit
range you specified on the command line. The commits are initially
listed in order from oldest to most recent and have the
pick verb on each one. If you were to leave the
editor now, each commit would be picked (in
order), applied to the target branch, and committed. The lines preceded
by a # are helpful reminders and
comments that are ignored by the program.

At this point, however, you are free to reorder the commits,
squash commits together, change a commit, or delete one entirely. To
follow the listed steps, simply reorder the commits in your editor as
follows and exit it:

    pick 799dba3 Finish my colour haiku
    pick 3d0f83b Use color instead of colour
    pick b61b041 Use American spellings

Recall that the very first commit for the rebase is the
Start my haiku commit. The next commit will become
Finish my colour haiku, followed by the Use color
and Use American … commits.

    $ git rebase -i master~3

    # reorder the first two commits and exit your editor

    Successfully rebased and updated refs/heads/master.

    $ git show-branch --more=4
    [master] Use American spellings
    [master^] Use color instead of colour
    [master~2] Finish my colour haiku
    [master~3] Start my haiku

Here, the history of commits has been rewritten; the two spelling
commits are together and the two writing commits are together.

Still following the outlined order, your next step is to
squash the two spelling commits into just one
commit. Again, issue the git rebase -i
master~3
command. This time, convert the commit list
from

    pick d83f7ed Finish my colour haiku
    pick 1f7342b Use color instead of colour
    pick 1915dae Use American spellings

to

    pick d83f7ed Finish my colour haiku
    pick 1f7342b Use color instead of colour
    squash 1915dae Use American spellings

The third commit will be squashed into the immediately preceding
commit, and the new commit log message template will be formed from the
combination of the commits being squashed together.

In this example, the two commit log messages are joined and
offered in an editor:

    # This is a combination of two commits.
    # The first commits message is:

    Use color instead of colour

    # This is the 2nd commit message:

    Use American spellings

These messages can be edited down to just

    Use American spellings

Again, all # lines are
ignored.

Finally, the results of the rebase sequence can be seen:

    $ git rebase -i master~3

    # squash and rewrite the commit log message

    Created commit cf27784: Use American spellings
     1 files changed, 3 insertions(+), 3 deletions(-)
    Successfully rebased and updated refs/heads/master.

    $ git show-branch --more=4
    [master] Use American spellings
    [master^] Finish my colour haiku
    [master~2] Start my haiku

Although the reordering and squash steps demonstrated here
occurred in two separate invocations of git
rebase -i master~3
, the two phases could have been performed
in one. It is also perfectly valid to squash multiple sequential commits
into one commit in a single step.

rebase Versus merge

In addition to the problem of simply altering history, the
rebase operation has further ramifications of which you should be
aware.

Rebasing a sequence of commits to the tip of a branch is similar
to merging the two branches; in either case, the new head of that branch
will have the combined effect of both branches represented.

You might ask yourself Should I use merge or rebase on my
sequence of commits?
In Chapter 12, this will become an important
question—especially when multiple developers, repositories, and branches
come into play.

The process of rebasing a sequence of commits causes Git to
generate an entirely new sequences of commits. They have new SHA1 commit
IDs, are based on a new initial state, and represent different diffs
even though they involve changes that achieve the same ultimate
state.

When faced with a situation like that of Figure 10-12, rebasing it into Figure 10-13 doesn’t present a problem because
no other commit relies on the branch being rebased. However, even within
your own repository you might have additional branches based on the one
you wish to rebase. Consider the graph shown in Figure 10-16.

Figure 10-16. Before git rebase multibranch

You might think that executing the command:

    # Move onto tip of master the dev branch
    $ git rebase master dev

would yield the graph in Figure 10-17. But it does not. Your first
clue that it didn’t happen comes from the command’s output.

Figure 10-17. Desired git rebase multibranch
    $ git rebase master dev
    First, rewinding head to replay your work on top of it...
    Applying: X
    Applying: Y
    Applying: Z

This says that Git applied the commits for X, Y, and
Z only. Nothing was said about
P or Q, and instead you obtain the graph in Figure 10-18.

Figure 10-18. Actual git rebase multibranch

The commits X', Y', and Z'
are the new versions of the old commits that stem from B. The old X and Y
commits both still exist in the graph because they are still reachable
from the dev2 branch. However, the
original Z commit has been removed
because it is no longer reachable. The branch name that
was pointing to it has been moved to the new
version of that commit.

The branch history now looks like it has duplicate commit messages
in it, too:

    $ git show-branch
    * [dev] Z
     ! [dev2] Q
      ! [master] D
    ---
    *   [dev] Z
    *   [dev^] Y
    *   [dev~2] X
    * + [master] D
    * + [master^] C
     +  [dev2] Q
     +  [dev2^] P
     +  [dev2~2] Y
     +  [dev2~3] X
    *++ [master~2] B

But remember, these are different commits that do
essentially the same change. If you merge a branch with one of the new
commits into another branch that has one of the old commits, Git has no
way of knowing that you’re applying the same change twice. The result is
duplicate entries in git log, most
likely a merge conflict, and general confusion. It’s a situation that
you should find a way to clean up.

If this resulting graph is actually what you want, then you’re
done. More likely, moving the entire branch (including subbranches) is
what you really want. To achieve that graph, you will, in turn, need to
rebase the dev2 branch on the new
Y' commit on the dev branch:

    $ git rebase dev^ dev2
    First, rewinding head to replay your work on top of it...
    Applying: P
    Applying: Q

    $ git show-branch
    ! [dev] Z
     * [dev2] Q
      ! [master] D
    ---
     *  [dev2] Q
     *  [dev2^] P
    +   [dev] Z
    +*  [dev2~2] Y
    +*  [dev2~3] X
    +*+ [master] D

And this is the graph shown in Figure 10-17.

Another situation that can be extremely confusing is
rebasing a branch that has a merge on it. For example, suppose you had a
branch structure like that shown in Figure 10-19.

Figure 10-19. Before git rebase merge

If you want to move the entire dev branch structure from commit N down through to commit X off of B
and onto D, as shown in Figure 10-20, then you might expect simply
to use the command git rebase master
dev
.

Figure 10-20. Desired git rebase merge

Again, however, that command yields some surprising
results:

    $ git rebase master dev
    First, rewinding head to replay your work on top of it...
    Applying: X
    Applying: Y
    Applying: Z
    Applying: P
    Applying: N

It looks like it did the right thing. After
all, Git says that it applied all the (nonmerge) commit changes. But did
it really get things right?

    $ git show-branch
    * [dev] N
     ! [master] D
    --
    *  [dev] N
    *  [dev^] P
    *  [dev~2] Z
    *  [dev~3] Y
    *  [dev~4] X
    *+ [master] D

All those commits are now in one long string!

What happened here?

Git needs to move the portion of the graph reachable from dev back to the merge base at B, so it found the commits in the range
master..dev. To list all those
commits, Git performs a topological sort on that portion of the graph to
produce a linearized sequence of
all the commits in that range. Once that sequence has been determined,
Git applies the commits one at a time starting on the target commit,
D. Thus, we say that Rebase
has linearized the original branch history (with merges) onto the master
branch,
as shown in Figure 10-21.

Again, if that is what you wanted or if you don’t care that the
graph shape has been altered, then you are done. But if in such cases
you want to explicitly preserve the branching and merging structure of
the entire branch being rebased, then use the
--preserve-merges option.

    # This option is a version 1.6.1 feature

    $ git rebase --preserve-merges master dev
    Successfully rebased and updated refs/heads/dev.

Using my Git alias from Configuring an Alias of Chapter 3, we can see that the resulting graph
structure maintains the original merge structure.

    $ git show-graph
    * 061f9fd... N
    *   f669404... Merge branch 'dev2' into dev
    |\
    | * c386cfc... Z
    * | 38ab25e... P
    |/
    * b93ad42... Y
    * 65be7f1... X
    * e3b9e22... D
    * f2b96c4... C
    * 8619681... B
    * d6fba18... A
Figure 10-21. git rebase merge after linearization

And this looks like the graph in Figure 10-20.

Some of the principles for answering the rebase-versus-merge
question apply equally to your own repository as they do to a
distributed or multirepository scenario. In Chapter 13, you can read about the
additional implications that affect developers using other
repositories.

Depending on your development style and your ultimate intent,
having the original branch development history linearized when it is
rebased may or may not be acceptable. If you have already published or
provided the commits on the branch that you wish to rebase, consider the
negative ramifications on others.

If the rebase operation isn’t the right choice and you still need
the branch changes, then merging may be the correct choice.

The important concepts to remember are:

  • Rebase rewrites commits as new commits.

  • Old commits that are no longer reachable are gone.

  • Any user of one of the old, pre-rebase commits might be
    stranded.

  • If you have a branch that uses a pre-rebase commit, you might
    need to rebase it in turn.

  • If there is a user of a pre-rebase commit in a different
    repository, he still has a copy of that commit even though it has
    moved in your repository; the user will now have to fix up his
    commit history, too.


[22] That includes you too!

Comments are closed.

loading...