Showing posts with label Git rebase. Show all posts
Showing posts with label Git rebase. Show all posts

Friday, 28 December 2018

GIT: Using git rebase Instead of git merge

Using the "git merge" command is probably the easiest way to integrate changes from one branch into another. However, it's not your only option: "git rebase" offers another, slightly different way of integration.

An Example Scenario

Let's take a simple scenario with the following two branches. Our goal is to integrate "branch-B" into "branch-A":

git rebase Avoids Merge Commits

Before we look at the exact steps that occur during a rebase, let's illustrate the differences between merge and rebase.
Using "git merge", the result of our integration would look like this:
Using "git rebase", the end result looks quite different - especially because no extra merge commit will be created. Rebase results in a "straight-line" commit history:
Neither of these scenarios is "better" or "worse" than the other. It's rather a matter of taste and convention in your team!

git rebase in Slow Motion

There's quite a lot happening behind the curtains when a rebase is performed.
In step 1, Git will prepare the receiving branch, in our case "branch-A": all commits that came after the two branches' common ancestor commit (C1) will be removed - just temporarily, of course! Think of them as being parked, ready to be reapplied later.
Step 2 is very easy: Git now integrates the commits from "branch-B". At the end of this step, both branches look exactly the same.
It's the third, final step that gives "rebase" its name: the parked commits from "branch-A" are now reapplied, on top of the just integrated commits. These commits are literally re-based, their new parent commit now being C4.

Be Careful with git rebase

The rebase command is a wonderful and very powerful tool for integrating changes. Keep in mind, though, that it rewrites your commit history.
Let's revisit our above example to illustrate this: before starting the rebase, C3's parent commit was C1. After the rebase, however, C3 is now based on C4!
A revision's parent commit is crucial information. When a commit is rebased onto a new parent, it will also receive a new commit hash. This effectively makes it a completely new commit!
Trouble begins when you rebase commits that have already been published on a remote server. These commits might already be part of your colleagues' work - and should not change their identities! Therefore, please never rebase published commits!

Using git rebase

Actually executing a rebase is dead simple. Make sure you are on the branch that should receive the changes and then simply type...
$ git rebase branch-B

GIT: What's a "detached HEAD" in Git?

It might very well be that you'll never come across this "mysterious" state in your Git career. However, if you do one day, you'd probably like to know what a "detached HEAD" is - and how you might have arrived at that state.

Understanding how "checkout" works

With the "git checkout" command, you determine which revision of your project you want to work on. Git then places all of that revision's files in your working copy folder.
Normally, you use a branch name to communicate with "git checkout":
$ git checkout development
However, you can also provide the SHA1 hash of a specific commit instead:
$ git checkout 56a4e5c08
Note: checking out '56a4e5c08'.

You are in 'detached HEAD' state...
This exact state - when a specific commit is checked out instead of a branch - is what's called a "detached HEAD".

The problem with a detached HEAD

The HEAD pointer in Git determines your current working revision (and thereby the files that are placed in your project's working directory). Normally, when checking out a proper branch name, Git automatically moves the HEAD pointer along when you create a new commit. You are automatically on the newest commit of the chosen branch.
When you instead choose to check out a commit hash, Git won't do this for you. The consequence is that when you make changes and commit them, these changes do NOT belong to any branch.
This means they can easily get lost once you check out a different revision or branch: not being recorded in the context of a branch, you lack the possibility to access that state easily (unless you have a brilliant memory and can remember the commit hash of that new commit...).

When a detached HEAD shows up

There are a handful of situations where detached HEAD states are common:
  • Submodules are indeed checked out at specific commits instead of branches.
  • Rebase works by creating a temporary detached HEAD state while it runs.

Where a detached HEAD should not show up

Additionally, another situation might spring to mind: what about going back in time to try out an older version of your project? For example in the context of a bug, you want to see how things worked in an older revision.
This is a perfectly valid and common use case. However, you don't have to maneuver yourself into a detached HEAD state to deal with it. Instead, remember how simple and cheap the whole concept of branching is in Git: you can simply create a (temporary) branch and delete it once you're done.
$ git checkout -b test-branch 56a4e5c08

...do your thing...

$ git checkout master
$ git branch -d test-branch

GIT: Rebase as an Alternative to Merge

While merging is definitely the easiest and most common way to integrate changes, it's not the only one: "Rebase" is an alternative means of integration.
NOTE
While rebasing definitely has its advantages over an off-the-shelf merge, it's also a matter of taste to a great extent: some teams prefer to use rebase, others prefer merge.
As rebasing is quite a bit more complex than merging, my recommendation is that you skip this chapter unless you and your team are absolutely sure you want to use it. Another option is to return to this chapter after you've had some practice with the basic workflow in Git.

Understanding Merge a Little Better

Before we can dive into rebase, we'll have to get into a little more detail about merge. When Git performs a merge, it looks for three commits:
  • (1) Common ancestor commit
    If you follow the history of two branches in a project, they always have at least one commit in common: at this point in time, both branches had the same content and then evolved differently.
  • (2) + (3) Endpoints of each branch
    The goal of an integration is to combine the current states of two branches. Therefore, their respective latest revisions are of special interest.
Combining these three commits will result in the integration we're aiming for.

Fast-Forward or Merge Commit

In very simple cases, one of the two branches doesn't have any new commits since the branching happened - its latest commit is still the common ancestor.
In this case, performing the integration is dead simple: Git can just add all the commits of the other branch on top of the common ancestor commit. In Git, this simplest form of integration is called a "fast-forward" merge. Both branches then share the exact same history.
In a lot of cases, however, both branches moved forward individually.
To make an integration, Git will have to create a new commit that contains the differences between them - the merge commit.

Human Commits & Merge Commits

Normally, a commit is carefully created by a human being. It's a meaningful unit that wraps only related changes and annotates them with a comment.
A merge commit is a bit different: instead of being created by a developer, it gets created automatically by Git. And instead of wrapping a set of related changes, its purpose is to connect two branches, just like a knot. If you want to understand a merge operation later, you need to take a look at the history of both branches and the corresponding commit graph.

Integrating with Rebase

Some people prefer to go without such automatic merge commits. Instead, they want the project's history to look as if it had evolved in a single, straight line. No indication remains that it had been split into multiple branches at some point.
Let's walk through a rebase operation step by step. The scenario is the same as in the previous examples: we want to integrate the changes from branch-B into branch-A, but now by using rebase.
The command for this is very plain:
$ git rebase branch-B
First, Git will "undo" all commits on branch-A that happened after the lines began to branch out (after the common ancestor commit). However, of course, it won't discard them: instead you can think of those commits as being "saved away temporarily".
Next, it applies the commits from branch-B that we want to integrate. At this point, both branches look exactly the same.
In the final step, the new commits on branch-A are now reapplied - but on a new position, on top of the integrated commits from branch-B (they are re-based).
The result looks like development had happened in a straight line. Instead of a merge commit that contains all the combined changes, the original commit structure was preserved.

The Pitfalls of Rebase

Of course, using rebase isn't just sunshine and roses. You can easily shoot yourself in the foot if you don't mind an important fact: rebase rewrites history.
As you might have noticed in the last diagram above, commit "C3*" has an asterisk symbol added. This is because, although it has the same contents as "C3", it's effectively a different commit. The reason for this is that it now has a new parent commit (C4, which it was rebased onto, compared to C1, when it was originally created).
A commit has only a handful of important properties like the author, date, changeset - and who its parent commit is. Changing any of this information effectively creates a completely new commit, with a new hash ID.
Rewriting history in such a way is unproblematic as long as it only affects commits that haven't been published, yet. If instead you're rewriting commits that have already been pushed to a public server, danger is at hand: another developer has probably already based work on the original C3 commit, making it indispensable for other newer commits. Now you introduce the contents of C3 another time (with C3*), and additionally try to remove the original C3 from the timeline with your rebase. This smells like trouble...
Therefore, you should use rebase only for cleaning up your local work - but never to rebase commits that have already been published.

GIT: How can I delete a commit in Git?

Git offers a couple of tools to delete (or undo) older commits. The first step, therefore, is to find out which tool is best for your scenario.

Restoring an old version of a project

Do you want to restore an older revision of your project - just as it was at a certain point in time?
In that case, the "git reset" command is what best suits your needs. Read more in our detailed post about how to reset to a previous revision.

Undoing only a specific commit

A different scenario is when you want to revert the effects of a certain commit - without deleting any commits that came afterwards.
This is a case for the "git revert" command. Interestingly, the command doesn't deleteanything; it creates a new commit that introduces changes which revert the effects of the specified commit. Our post explains the details of how to go about reverting commits.

Delete or change specific commits

Another use case might be to delete a commit "in the middle" of your history, without resetting your whole project to a previous revision.
In that case, we'll have to bring out the big guns: Git's "Interactive Rebase" tool is what we need here. Please note that this is not only the most powerful, but also the most dangerous of the three commands listed here. This is because it allows you to change your commit history quite drastically - which is a dangerous process.
Read more about Interactive Rebase, including what to watch out for.

GIT: How can I change the author name / email of a commit?

Before we jump into solutions, let's find out what exactly it is you want to accomplish:
a) Change the author information before making a commit
b) Change the author information after making a commit (i.e. for historical commits)

Changing Your Git Author Identity

There are three ways to change your committer identity in Git. All of these methods only affect future commits, not past ones!

Changing Your Committer Name & Email Globally

You can run the "git config" command with the --global flag; this will make sure all of your future commits use the given information:
$ git config --global user.name "John Doe"
$ git config --global user.email "john@doe.org"

Changing Your Committer Name & Email per Repository

If you want to use special settings only when working in a certain repository, you can simply omit the --global flag. This makes the configuration valid only in thatrepository:
$ git config user.name "John Doe"
$ git config user.email "john@doe.org"

Changing the Author Information Just for the Next Commit

Finally, with the --author flag, you can also overwrite the author information for just the next commit:
git commit --author="John Doe <john@doe.org>"

Editing the Author of Past Commits

NOTE

Editing Past Commits Rewrites History!

No matter how exactly we change the information of past commits, there's one thing to always keep in mind: if we do this, we are effectively rewriting commit history.
This is nothing to take lightly: you will create new commit objects in this process, which can become a serious problem for your collaborators - because they might have already based new work on some of the original commits.
Therefore, think twice before you rewrite your commit history!
There are three basic ways to edit your past commits:

Using --amend for the Very Last Commit

In case you want to change just the very last commit, Git offers a very easy way to do this:
git commit --amend --author="John Doe <john@doe.org>"
This effectively replaces the last commit with your "edited" version, correcting the wrong author information.

Using Interactive Rebase

Interactive Rebase is the Swiss Army Knife of tools in Git: it allows you to do and change almost anything. However, being as powerful as it is, this also means you can very easily shoot yourself in the foot. Use it with care (and possibly read up on it)!
The first step is to identify the last "good" commit and provide its hash to the rebase command:
$ git rebase -i -p 0ad14fa5
Your editor will open, requesting you to mark all the commits you want to change with the "edit" keyword.

Git will now walk you through each commit, giving you the chance to mold it as you desire:
Stopped at 5772b4bf2... Add images to about page
You can amend the commit now, with

    git commit --amend

Once you are satisfied with your changes, run

    git rebase --continue
Your job, now, is to correct the author information and then continue to the next concerned commit object until you've edited all the commits you just marked:
$ git commit --amend --author="John Doe <john@doe.org>" --no-edit
$ git rebase --continue

Using git filter-branch

Another way is to use Git's "filter-branch" command. It allows you to batch-process a (potentially large) number of commits with a script.
You can run the below sample script in your repository (filling in real values for the old and new email and name):
$ git filter-branch --env-filter '
WRONG_EMAIL="wrong@example.com"
NEW_NAME="New Name Value"
NEW_EMAIL="correct@example.com"

if [ "$GIT_COMMITTER_EMAIL" = "$WRONG_EMAIL" ]
then
    export GIT_COMMITTER_NAME="$NEW_NAME"
    export GIT_COMMITTER_EMAIL="$NEW_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$WRONG_EMAIL" ]
then
    export GIT_AUTHOR_NAME="$NEW_NAME"
    export GIT_AUTHOR_EMAIL="$NEW_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags
The same warning applies to this method as to the others mentioned: you are rewriting history with this command, creating new commit objects along the way!
Preferably, you should only do this in repositories that haven't been published / shared, yet. In any other case you should use it with extreme care - and only if you're aware of the side effects!

Thursday, 27 December 2018

GIT: How can I edit / fix the last commit's message?

Made a typo in your commit message? Or forgot to mention an important detail in the message? Correcting a commit message in Git can be very easy - if it's the very last commit you want to edit!

Amending the Last Commit

To change the last commit, you can simply commit again, using the --amend flag:
$ git commit --amend -m "New and correct message"
Simply put, this overwrites your last commit with a new one. This also means that you're not limited to just editing the commit's message: you could also add another couple of changes you forgot.
$ git add another/changed/file.txt
$ git commit --amend -m "message"
However, keep two important details in mind:
  1. Amend only works with the very last commit. If you notice your mistake only after adding another commit, amend won't help you much.
  2. Amend rewrites the commit history in your repository: the old commit is replaced by a completely new one (a new and different commit object). This makes it very important that you don't amend (= rewrite) commits that you've already publishedto a remote repository! Because in that case, your colleagues might have already based their work on this commit - which you would try to replace using "amend".
Therefore, use "amend" whenever you want to change / edit your very last and unpushed commit.

hanging Older Commits


If you want to change older commits, Git also has a tool for this use case:
$ git rebase --interactive
The "interactive rebase" command, however, is quite an advanced tool: very powerful and quite dangerous. You should definitely understand what you're doing before applying it! See here if you really need to use it.
The mode of action, however, is the same as with the --amend flag: you are rewriting history! Therefore, just as with amend, you should not use interactive rebasing on commits you have already pushed!

GIT: Rebasing branches

36.1. Rebasing branches

You can use Git to rebase one branch on another one. As described, the merge command combines the changes of two branches. If you rebase a branch called A onto another, the git command takes the changes introduced by the commits of branch A and applies them based on the HEAD of the other branch. After this operation the changes in the other branch are also available in branch A.
The process is displayed in the following picture. We want to rebase the branch called branch_1 onto master.
Rebasing a branch
Running the rebase command creates a new commit with the changes of the branch on top of the master branch.
Rebasing a branch result
Performing a rebase does not create a merge commit. The final result for the source code is the same as with merge but the commit history is cleaner; the history appears to be linear.
Rebase can be used to forward-port a feature branch in the local Git repository onto the changes of the master branch. This ensures that your feature is close to the tip of the upstream branch until it is finally published.
If you rewrite more than one commit by rebasing, you may have to solve conflicts per commit. In this case the merge operations might be simpler to be performed because you only have to solve merge conflicts once.
Also, if your policy requires that all commits result in correct software you have to test all the rewritten commits since they are "rewritten" by the rebase algorithm. Since merge/rebase/cherry-pick are purely text-based and do not understand the semantics of these texts they can end up with logically incorrect results. Hence, it might be more efficient to merge a long feature branch into upstream instead of rebasing it since you only have to review and test the merge commit.
You can use the rebase command to change your Git repository history commits.
 This is called interactiverebase, see ? for information about this feature.

36.2. Good practice for rebase

You should avoid using the Git rebase operation for changes which have been published in other Git repositories. The Git rebase operation creates new commit objects, this may confuse other developers using the existing commit objects.
Assume that a user has a local feature branch and wants to push it to a branch on the remote repository. However, the branch has evolved and therefore pushing is not possible. Now it is good practice to fetch the latest state of the branch from the remote repository. Afterwards you rebase the local feature branch onto the remote tracking branch. This avoids an unnecessary merge commit. This rebasing of a local feature branch is also useful to incorporate the latest changes from remote into the local development, even if the user does not want to push right away.
Rebasing and amending commits is safe as long as you do not push any of the 
changes involved in the rebase. For example, when you cloned a repository and
 worked in this local repository. Rebasing is a great way to keep the history clean
 before contributing back your modifications.
In case you want to rewrite history for changes you have shared with others you 
need to use the -fparameter in your git push command and subsequently your 
colleagues have to use fetch -f to fetch the rewritten commits.
# using forced push
git push -f

36.3. Example for a rebase

The following demonstrates how to perform a rebase operation.
# create new branch
git checkout -b rebasetest

# create a new file and put it under revision control
touch rebase1.txt
git add . && git commit -m "work in branch"

# do changes in master
git checkout master

# make some changes and commit into testing
echo "rebase this to rebasetest later" > rebasefile.txt
git add rebasefile.txt
git commit -m "create new file"

# rebase the rebasetest onto master
git checkout rebasetest
git rebase master

# now you can fast forward your branch onto master
git checkout master
git merge rebasetest

GIT: Editing history with the interactive rebase

Git allows you to edit your commit history with a functionality called interactive rebase. For example, you can combine several commits into one commit, reorder or skip commits and edit the commit message.
This is useful as it allows the user to rewrite some commit history (cleaning it up) before pushing the changes to a remote repository.
Interactive rebase allows you to quickly edit a series of commits using the following actions:
Table 3. Interactive rebase actions
ActionDescription
pick
includes the selected commit, moving pick entries enables reordering of commits
skip
removes a commit
edit
amends the commit
squash
combines the changes of the commit with the previous commit and combines their commit messages
fixup
squashes the changes of a commit into the previous commit discarding the squashed commit’s message
reword
similar to pick but allows modifying the commit message
The setup for the rebase is called the rebase plan. Based on this plan, the actual interactive rebase can be executed.
It is safe to use interactive rebase as long as the commits have not been pushed 
to another repository. As the interactive rebase creates new commit objects, 
other developers might be confused if you rebase already published changes.

37.1. Example: Interactive rebase

The following commands create several commits which will be used for the interactive rebase.
# create a new file
touch rebase.txt

# add it to git
git add . && git commit -m "add rebase.txt  to staging area"

# do some silly changes and commit
echo "content" >> rebase.txt
git add . && git commit -m "add content"
echo " more content" >> rebase.txt
git add . && git commit -m "just testing"
echo " more content" >> rebase.txt
git add . && git commit -m "woops"
echo " more content" >> rebase.txt
git add . && git commit -m "yes"
echo " more content" >> rebase.txt
git add . && git commit -m "add more content"
echo " more content" >> rebase.txt
git add . && git commit -m "creation of important configuration file"

# check the git log message
git log
We want to combine the last seven commits. You can do this interactively via the following command.
git rebase -i HEAD~7
This command opens your editor of choice and lets you configure the rebase operation by defining which commits to picksquash or fixup.
The following listing shows an example of the selection. We pick the last commit, squash 5 commits and fix the sixth commit. The listing uses the long format of the commands (for example fixup instead of the short form f ) for better readability.
pick 7c6472e rebase.txt added to index
fixup 4f73e68 added content
fixup bc9ec3f just testing
fixup 701cbb5 ups
fixup 910f38b yes
fixup 31d447d added more content
squash e08d5c3 creation of important configuration file

# Rebase 06e7464..e08d5c3 onto 06e7464
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.