Can I rebase a Git branch without modifying my working copy?

GitRebase

Git Problem Overview


Suppose I have my "master" branch checked out. I've committed some production changes to "master", and now I want to rebase my "experimental" branch onto the latest master. But, I want to do this without modifying any files in my working copy. Essentially, I want all the magic to happen inside the .git directory, without touching the working copy.

If not for the "don't modify my working copy" requirement, this would just be a matter of doing:

# current branch is master
git checkout experimental
git rebase master
git checkout master

My real problem is that this modifies timestamps in my working copy, even though I'm ending by checking out the exact same content I started with. As soon as I run "git checkout experimental", any files that contain changes in the experimental branch will get their mtime set to the current time -- and so will any files that were changed in master since the last time I rebased experimental. Because the mtimes have changed, things like build tools get the idea that there's work they need to do again, even though, by the time I'm done, the files' contents haven't actually changed. (In my case, it's that if a project file's timestamp changes, Visual Studio thinks it needs to spend a lot of time unloading and reloading the project.) I want to avoid that.

Is there a way to do all of the above in one step, without ever modifying anything in the working copy (assuming there are no conflicts during the rebase)?

(If there are conflicts, my preference would be to show the error and then abort the entire operation, without ever modifying any timestamps. But that's just my preference, not a hard requirement -- I don't know what all is possible.)

Of course I can write a script to capture the mtimes, run git, and then reset the mtimes; but it seems likely that Git would already have a way to do things like rebase without bothering the working copy, since the rebase is really about the deltas, not the files' actual contents.

Git Solutions


Solution 1 - Git

Since git 2.5, an even better solution is to use a second worktree.

> A git repository can support multiple working trees, allowing you to check out more than one branch at a time.

$ git worktree add ../second-copy experimental
$ cd ../second-copy/
$ git rebase master experimental

And that's it. Afterwards, you can rm -rf second-copy if you want, or keep it for more rebases in the future.

$ git rebase master experimental

Solution 2 - Git

> Is there a way to do all of the above in one step, without ever modifying anything in the working copy?

This is unfortunately impossible (without creating a modifiable copy of the working copy - see also Petr's answer), because git performs all merge-y operations (real merges, cherry-picks, rebases, patch application) on the work tree. This is mentioned several times before, for example in one of the knowledgeable Jakub Narębski's answers:

> There is no way that merge (or rebase) can work without touching the working directory (and index), as there can be merge conflicts that have to be resolved using working directory (and/or index).

Yes, it's a design decision, but it's a pretty understandable one - it'd be a bit of a chore to build up all the structure necessary to attempt a merge in memory, then as soon as it hits a conflict, dump everything into the work tree, when instead you could simply do it in the work tree in the first place. (I'm not a git developer; don't take this as absolute complete truth. There could be other reasons.)

My suggestion, rather than writing a script to do all that mtime manipulation, would be simply to clone the repository, perform the rebase in the clone, then push it back into your original repository:

git clone project project-for-rebase
cd project-for-rebase
git branch experimental origin/experimental
git rebase master experimental
git push origin experimental

That of course assumes that experimental isn't checked out in your original repo. If it is, instead of the push, you'd do something like git fetch ../project-for-rebase experimental; git reset --hard FETCH_HEAD or more readable, git remote add for-rebase ../project-for-rebase; git fetch for-rebase; git reset --hard for-rebase/experimental. That will naturally touch whatever files differ between the original and rebased experimental branches, but that's definitely correct behavior. (This wasn't the example you gave, of course, but I want these instructions to be general!)

Solution 3 - Git

I've created a small script to do this on linux. It's based on Jefromi's answer and a few additions (mainly, setting up alternates so the object database isn't copied, and only pulling the needed branches). Some of you may find it useful: https://github.com/encukou/bin/blob/master/oot-rebase

If it doesn't do quite what you like, pull requests are welcome :)

Solution 4 - Git

> Update, since git 2.5 this answer is superseded by the in-built > mechanism "worktree" which is basically the same. See above answer: > https://stackoverflow.com/a/12481546/1499102

Similar to creating a clone of your repository, I find that it's much tidier to make use of multiple workdirs to do things like this. Also a clone will use a lot of disk space, this will use almost none.

https://github.com/git/git/blob/master/contrib/workdir/git-new-workdir

You create a new workdir like this:

git-new-workdir project-dir new-workdir branch

Then you can treat that as if it was a clone except that fetches and commits in your original workdir will be reflected here (although not on the working branch without recheckout). The only exception to this is if you have submodules as those are done separately for each workdir by default. Frankly I've never looked in to that because I try and avoid submodules.

So basically just:

cd new-workdir
git checkout experimental
git rebase master

Not exactly a single command, but pretty simple.

An advantage of this approach (like the clone approach) over the stash approach below is that if you have code currently executing (or otherwise being used by some processes) from your working directory, it isn't interrupted.


The other option which isn't mentioned here is to do it in your current working directory, but stash your changes so that you can then instantly restore your working directory state.

# current branch is master (with changes to working state)
git stash -u
git checkout experimental
git rebase master
git checkout master
git stash pop

Make sure to use stash -u if you have any new files as otherwise they will not be stashed. Again, not one step, but pretty clean and simple.

Solution 5 - Git

As others have said, it is not possible to rebase a branch without touching the working directory (even the suggested alternatives such as creating a new clone or worktree cannot change this fact; these alternatives do indeed not touch your current working directory, but only by creating a new worktree).

For the special case where the branch that you want to update is to be rebased on the current working tree (or a parent thereof), it is possible to "rebase" the other branch without unnecessarily touching files.
This special case often happens if you are having a git workflow where you are working on many branches that are all branched from the main "master" branch (which is regularly updated to the remote master branch).

To illustrate, assume a Git repository with the following structure:

repo
- commitA
- commitB
- commitC <-- master <-- OtherBranch based on master
  - commitD <-- First commit in otherBranch
  - commitE <-- Second commit in OtherBranch
- commitD <-- Unrelated commit in current working tree

For the sake of the example, let's assume that "OtherBranch" is branched off "master", and that your current working tree is also based on "master". Your workflow typically starts with updating your local master branch with the remote version...

# Fetch commits from remote "origin" and update the master branch:

# If your current branch is identical to master
git pull origin master

# If your current branch has extra commits on top of master
git pull --rebase origin master

# If you don't want to touch your current branch
git fetch origin master:master

... and then you fiddle with the current branch and do some time-consuming compilations. Eventually, you decide that you want to work on OtherBranch. This OtherBranch should be rebased on master (preferably with minimal filesystem operations). The following section will show how.

Rebasing other branch (reference example - do NOT do this)

The following solution is the git way to do it:

git checkout OtherBranch
git rebase master    # or git rebase origin/master

The disadvantage of that is that the first command changes the dates of the current worktree, even though the files are going to be restored by the second command.

Rebasing other branch with minimal changes

To minimize the number of touched files, you need to check out to the new base branch and then apply all extra commits in OtherBranch on top of the base branch using git cherry-pick.

Before doing anything, you need to identify the commits in OtherBranch.

  • git log OtherBranch shows the commits on OtherBranch (mainly useful if you haven't changed OtherBranch yet)
  • git reflog shows the changes to branches in your local repository (useful if you have already updated branches and made a mistake).

In the current example, you will discover that the last commit on OtherBranch is commitE. You can see a list of commits before that with git log commitE (or if you want a shorter list, git log --oneline commitE). If you look through the list, you will see that the base commit is commitC.

Now you know that the base commit is commitC and the last commit is commitE, you can rebase OtherBranch (from its previous "master" to the new "master") as follows:

# Replace the old OtherBranch with "master" and switch to it.
git checkout -B OtherBranch master

# Cherry-pick commits starting from commitC and ending at commitE.
cherry-pick commitC^..commitE

Alternatively (if you want to successfully complete the "rebase" before replacing OtherBranch):

# Create new branch NewOtherBranch based off "master" and switch to it.
git checkout -b NewOtherBranch master

# Cherry-pick commits starting from commitC and ending at commitE.
cherry-pick commitC^..commitE

# Replace the old branch with the current branch (-M = --move --force)
git branch -M OtherBranch
Why does this work?

Rebasing branches in git requires one to switch the current branch to the branch that you want to update (OtherBranch).

With the git rebase workflow, the following happens:

  1. Switch to OtherBranch (potentially branched off a very old base branch).
  2. Rebase (internal step 1): Save commits that are not in the upstream branch.
  3. Rebase (internal step 2): Reset current branch to the (new) base branch.
  4. Rebase (internal step 3): Restore commits from step 2.

Step 1 and step 3 touch many files, but ultimately many of the touched files have not actually changed.

My method combines step 1 and 3 into step 3, and as a result the number of touched files is minimal. The only files that are touched are:

  • Files that were changed between the base branch and the current commit in the current working tree.

  • Files that are changed by the commits in the OtherBranch.

Solution 6 - Git

I would also love it, but this leaves no hope to me:

> If <branch> is specified, git rebase > will perform an automatic git checkout > before doing anything else. > Otherwise it remains on the current > branch.

http://git-scm.com/docs/git-rebase

Solution 7 - Git

So you want a rebase done on for a branch before you checkout that branch? I really can't see the reason for that, since if you don't checkout that branch you can't work on it. Why do you want to rebase a branch that you don't work on? Do the checkout, it will change your mtime and then do the rebase. The rebase will touch files that are changed and of course you need to rebuild them.

However, a simple way to solve this is to use an other worktree for the rebase. Just set the enviroment variable GIT_WORK_TREE to an other worktree. Just don't forget to have your HEAD match your worktree.

Depending on which branch he is at and what's pushed, a push to a non-bare repo can be dangerous. A much better solution is to fetch from the repo with the precious worktree instead. Example:

` orgbranch=$(git rev-parse HEAD)

mkdir /tmp/tmp_wd

cp -r !(.git) /tmp/tmp_wd

export GIT_WORK_TREE=/tmp/tmp_wd

git checkout branch1

git rebase master

git checkout $orgbranch

export GIT_WORK_TREE=

rm -rf /tmp/tmp_wd`

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJoe WhiteView Question on Stackoverflow
Solution 1 - GitCraig P. MotlinView Answer on Stackoverflow
Solution 2 - GitCascabelView Answer on Stackoverflow
Solution 3 - GitPetr ViktorinView Answer on Stackoverflow
Solution 4 - GitdpwrView Answer on Stackoverflow
Solution 5 - GitRob WView Answer on Stackoverflow
Solution 6 - GitSnowbearView Answer on Stackoverflow
Solution 7 - GitiveqyView Answer on Stackoverflow