71

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.

Joe White
  • 94,807
  • 60
  • 220
  • 330

8 Answers8

42

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
Craig P. Motlin
  • 26,452
  • 17
  • 99
  • 126
39

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!)

Community
  • 1
  • 1
Cascabel
  • 479,068
  • 72
  • 370
  • 318
  • 2
    Excellent explanation of the reasons, and excellent alternative. Thanks! – Joe White Feb 07 '11 at 17:12
  • 1
    Downvoted because it’s clearly possible as shown by http://stackoverflow.com/a/12481546/99057 – Dave Abrahams Aug 08 '13 at 00:18
  • 2
    @DaveAbrahams I thought it was fairly clear that I meant it's impossible *without a copy of the work tree* which Petr's answer creates. But vote how you like. – Cascabel Aug 08 '13 at 00:43
  • Oh, we’d have never got the script posted by Petr if it weren’t for this answer! Maybe the answer should just start with something more affirmative. (upvoted now) – Dave Abrahams Aug 08 '13 at 00:50
  • However the last solution suggest pushing to a non-bare repo and that's denied by default... – iveqy Aug 08 '13 at 01:07
  • The quote from Jakub Narębski, where does that come from? I'm trying to link to it. –  Aug 29 '13 at 02:01
  • Ridiculous this isn't possible, in Mercurial, it's just: `hg rebase -s SHA1 -d SHA2` with `s` for source & `d` for destination. This works no matter what is checked out. – jaques-sam Jan 28 '19 at 15:12
14

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 :)

Petr Viktorin
  • 65,510
  • 9
  • 81
  • 81
6

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.

dpwr
  • 2,732
  • 1
  • 23
  • 38
3

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.
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • I like this method due to not having to create another working tree. Rather than looking manually for the base commit, you can do `git merge-base master OtherBranch` I also needed to drop the hat from the cherry-pick. – jgawrych May 31 '18 at 16:27
1

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

Snowbear
  • 16,924
  • 3
  • 43
  • 67
0

Craig- P. Motlin's answer suggests the use of worktree.

This is now more robust since Git 2.41 (Q2 2023) makes sure a few subcommands have been taught to stop users from working on a branch that is being used in another worktree linked to the same repository.

rebase: refuse to switch to a branch already checked out elsewhere

Signed-off-by: Rubén Justo

In b5cabb4 ("rebase: refuse to switch to branch already checked out elsewhere", 2020-02-23, Git v2.26.0-rc0 -- merge) we add a condition to prevent a rebase operation involving a switch to a branch that is already checked out in another worktree.

A bug has recently been fixed that caused this to not work as expected.

Let's add a test to notice if this changes in the future.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
-1

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`

iveqy
  • 19,951
  • 1
  • 15
  • 20
  • -1: There are plenty of good reasons to do want to do this, for example, wanting to rebase multiple branches you are working on (but not at the moment) on top of an updated master branch. And the caveat about HEAD matching work tree is a nontrivial landmine. – Cascabel Aug 08 '13 at 01:20
  • That's the exact case I'm talking about. If you don't work on them (at the moment) then don't rebase. Rebase when you're working on them. (Sorry that I wasn't clear enough). And yes it's a landmine, that's why I mentioned it... – iveqy Aug 08 '13 at 01:24
  • Maybe you want to share the rebased version with someone, maybe you want to (as I said) do several at once right now so you don't have a rebase to do every time you switch tasks, maybe it's some other reason. I've certainly had workflows where I'd rebase ten branches at once after pulling to master; the OP clearly has a need for it too. – Cascabel Aug 08 '13 at 01:26
  • And that's why I gave the solution to the problem that won't alter his mtimes and doesn't require an other repository... Pushing to a non-bare repo will also have the same problem with HEAD doesn't match the worktree (or branch doesn't match the worktree), so it's even worse. – iveqy Aug 08 '13 at 01:34
  • Pushing is for getting the changes back into the original repo; the secondary repository is just clone rebase, no shenanigans, no danger at all. If you can clarify exactly how to set up to do what you're talking about (how do you make a second work tree, how/where do you have to make sure HEAD is right, etc) I'd happily remove my downvote. – Cascabel Aug 08 '13 at 01:37
  • 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. `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` – iveqy Aug 08 '13 at 02:13
  • Put important information in your answer, not buried in comments! – Cascabel Aug 08 '13 at 02:44