Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

git: How to reset branch with contents of another branch?

Tags:

git

I cannot figure out how to update a branch to be identical to another branch. Here's an example:

git init test; cd test
echo apple >a; echo banana >b; git add a b; git commit -m 'A1/a:apple;b:banana'
echo carrot >c; git add c; git commit -m 'A2/c:carrot'
git checkout -b second HEAD^1
echo beets >b; echo dandelion >d; git add b d; git commit -m 'B1/b:beets;d:dandelion'

At this point my history looks like this:

A1-----A2     (master, contains a:apple, b:banana, c:carrot)
 \
  \----B1     (second, contains a:apple, b:beets, d:dandelion)

Now I'd like to add a commit to the "second" branch to make its contents match the "master" branch, that is:

A1-----A2
 \
  \----B1--?--B2     (desired contents a:apple, b:banana, c:carrot)

My question is, what git commands do I run to do this? The closest I've come up with is:

git checkout master .
git commit -m B2

However, while this resets b to banana and restores c, it does not remove the file d. I have not been able to come up with any variation of git reset that does what I want either. I don't think I want to do a version of git revert, because in my actual repository the history on the branches is more complicated than this example and the branches may contain merges.

Any help will be greatly appreciated!

like image 307
David Hull Avatar asked Apr 03 '13 23:04

David Hull


4 Answers

I assume current branch is second. If you unhappy with git reset --HARD master (because it will require force push if the second is published anywhere), you could undo B1 by git revert B1 (or just git revert master..second if you don't want to list all commits such as B1) and then git merge master.

Using @StevenPenny idea with ours strategy I came up with this solution:

git checkout master
git checkout -b transfer_second_to_master
git merge -s ours second
git checkout second
git merge transfer_second_to_master
git branch -d transfer_second_to_master

It is kind of emulation of theirs strategy, done as inversion of the ours stategy. And now git diff master second gives nothing, it means branches are identical.

like image 165
kan Avatar answered Nov 09 '22 22:11

kan


The simple way

You almost had the right answer. The solution is to first remove all of the files before checking out from master.

git checkout second
git rm -rf .
git checkout master .
git commit -m B2

This will create a new commit with a new message, author, and timestamp. If you all that to be the same, your best bet is...

Using git-reparent (shameless plug)

Download git-reparent, install it in your $PATH, and then

git checkout second
git branch parent
git reset --hard master
git reparent -p parent
git branch -d parent

Explanation:

  1. Switch to the branch we want to modify (second).
  2. Save a pointer (parent) to the current commit.
  3. Point the commit that we want to end up at (master).
  4. Modify the current branch (second) to have the contents of the current commit but have the given parents (parent).
  5. Delete our saved pointer (parent).
like image 43
Mark Lodato Avatar answered Nov 09 '22 23:11

Mark Lodato


This sounds like a job for git merge

git checkout master
git merge -s ours second

more info

This resolves any number of heads, but the resulting tree of the merge is always
that of the current branch head, effectively ignoring all changes from all other
branches. It is meant to be used to supersede old development history of side
branches.
like image 41
Zombo Avatar answered Nov 09 '22 22:11

Zombo


Before

A1-----A2     (master, contains a:apple, b:banana, c:carrot)
 \
  \----B1     (second, contains a:apple, b:beets, d:dandelion)
  • Current branch: second
  • git status: clean

Variant 1: no reference to master (very ugly)

git read-tree -m -u master
git commit -m 'B2: copy of A2'
git diff master

After:

A1-----A2      (master, contains a:apple, b:banana, c:carrot)
 \
  \----B1---B2 (second, contains a:apple, b:banana, c:carrot)
  • Current branch: second (ff)
  • git status: clean
  • diff status to master: clean

Confirm backing off works:

git reset --hard HEAD^
git clean -f
git branch -avv

Shows the before state again.

What does this variant do?

  • git read-tree reads the given other commit (here master) into INDEX
  • as option -u is given, it updates worktree accordingly
  • Then this is committed onto B1 as usual
  • However it does not add a reference to the "merged" tree.

Variant 2: show a merge reference to master (preferred!)

Sorry, I did not find out the correct way. Here is a workaround:

First: Do a dummy merge

git merge -s ours -m 'B2 dummy' master

Second: Fix the merge to the correct data

git read-tree -m -u master
git commit --amend -m 'B2: copy of A2'

Third: Check that it really matches master:

git diff master

After:

A1-------A2    (master, contains a:apple, b:banana, c:carrot)
 \         \
  \----B1---B2 (second, contains a:apple, b:banana, c:carrot)
  • Current branch: second (ff, merged, first parent: B1)
  • git status: clean
  • diff status to master: clean

Confirm backing off works:

git reset --hard HEAD^
git clean -f
git branch -avv

Shows the before state again, as it should.

How does this work:

  • git read-tree -m -u master resets the merge information, so we cannot start a merge, read the tree and then commit this merge afterwards, sadly.
  • However we want to use option -u, as this is crucial for updating the worktree. But -u requires -m or similar which throws us out of merge mode.
  • So we do a dummy merge before (and use strategy ours to prevent merge conflicts, which is exactly the wrong thing we want to happen).
  • Then we read in the state from master using git read-tree as we wanted
  • And now we amend the merge with the now, finally correct, index contents.
  • This keeps the original merge information.
  • Note that this is ff according to upstream.

What is the problem with this?

  • git commit is meant to be atomic. Either you have a correct state or not.
  • However this introduces a nonatomic situation, where a race condition (power outage) can put your local git repo in some unwanted state
  • This is bad in case this strategy shall be used in an automated setup, where, for example, after a release (B1) the current upstream (master) shall be used to restart to create the next release (B2 and later).
  • So variant 2 cannot be seen as the definitive last word to this. Yes, it works, somehow, but it is neiter nice, clean nor perfect.

Other Solutions to do it seem to be far too complicated. But perhaps I oversaw a bit?

  • Either use completely incomprehensible git-plumbing commands to create the correct commit-object yourself -- a no go for a solution which shall be simple to understand and be used.

  • Or do some very complex sub-branching and then fast-forward to the sub-branch, then delete it -- a double no-go, as this even introduces way more complicated things like need for complex cleanup if something breaks.

  • Or find out which commands are needed to update the worktree to the index (which re-invents the wheel as -u can already do it) after a git read-tree master, as this should happen before(!) the commit (as usual).

TODO

This has not yet been thoroughly tested with submodules. If they are added, deleted, replaced etc. and .gitmodules and .git/modules/ has been updated or blocks some things. Loooooong story.

like image 1
Tino Avatar answered Nov 09 '22 22:11

Tino