5

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!

David Hull
  • 1,255
  • 1
  • 14
  • 17
  • Do you want two branches that look like the `A` branch, or do you have to change the `B` branch? You could do `git checkout A ; git checkout -b C` to create a 3rd branch that is identical to `A`, leaving `B` as a third branch. – simont Apr 03 '13 at 23:49
  • @simont: I want to keep the B ("second") branch (because in the real world it's tracking a remote branch). – David Hull Apr 04 '13 at 00:10
  • See also: https://stackoverflow.com/questions/18436680/how-to-reset-develop-branch-to-master/18436724 – kolypto Oct 22 '20 at 22:56

5 Answers5

2

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.

kan
  • 28,279
  • 7
  • 71
  • 101
  • It appears that `git revert` creates a separate new commit for each commit being reverted; is that correct? Also, what happens if the history on the "second" branch is more complicated than my example above (that it, it may contain merges of its own)? – David Hull Apr 04 '13 at 00:39
  • Yes, it does... You could do `git revert --no-edit master..second` for mass revert. If you don't want such polluted history, maybe you could squash all reverts by interactive rebase before the merge. Or just use the `theirs` strategy above. – kan Apr 04 '13 at 00:43
2

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).
Mark Lodato
  • 50,015
  • 5
  • 41
  • 32
1

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.
Zombo
  • 1
  • 62
  • 391
  • 407
  • 1
    It is not that he wants - the `second` branch will have content unchanged, ignoring all `master` changes. He wants to have `second` == `master`. – kan Apr 04 '13 at 00:09
  • Have you tried? I have. No, it could not work, you are merging into `master`, he wants `second` changed. The `ours` strategy just creates a merge commit which marks the branch merged, but current branch content is unchanged. So, it gives that `git diff master master^1` is nothing, but `git diff second master` shows a difference. – kan Apr 04 '13 at 00:30
  • "current branch content" == tree which is pointed by a branch checked out most recently. – kan Apr 04 '13 at 00:40
1

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.

Tino
  • 9,583
  • 5
  • 55
  • 60
  • Variant 2 worked, but results in merge conflicts when I merge other feature branches from master – kolypto Oct 22 '20 at 22:44
  • See also: https://stackoverflow.com/questions/18436680/how-to-reset-develop-branch-to-master/18436724 – kolypto Oct 22 '20 at 22:56
  • @kolypto This probably is caused by B2 having B1 as parent and not A2. What is the merge base `git` chooses? Perhaps switching branches around could solve the problem? However the latter looses the natural `HEAD^` of B2, as this then refers to A2 instead of B1 as expected. Example: `git checkout -b tmp master; git fake-merge develop; branch -f develop tmp; git checkout develop; git branch -D tmp`. FYI [`git fake-merge`](https://stackoverflow.com/a/48567212/490291) is an [alias](https://github.com/hilbix/gitstart/blob/c7b1adec0d49a2cfa707c14acd204997b4e7c4cf/aliases.sh#L498) at my side. – Tino Oct 25 '20 at 18:33
0

Sounds like you just want branch second to start from A2. That is simple:

git branch -f second <commit-for-A2>

The -f flag means "Reset branch to commit". No reason to add a commit to unwind/reset the second branch.

[edit] If you can't simply reset, then do a revert of B1 (while on second) and then merge A2 from master onto second. Like this:

ebg@ebg(168)$ git log --graph --oneline --all
* ad6fb9d B1/b:beets;d:dandelion
| * 64c9f45 A2/c:carrot
|/  
* 07a4ceb A1/a:apple;b:banana
ebg@ebg(169)$ ls
a       b       d
ebg@ebg(170)$ git revert HEAD
[second 2b1de0c] Revert "B1/b:beets;d:dandelion"
 2 files changed, 1 insertion(+), 2 deletions(-)
 delete mode 100644 d
ebg@ebg(171)$ ls
a       b
ebg@ebg(172)$ git merge master
Merge made by the 'recursive' strategy.
 c |    1 +
 1 file changed, 1 insertion(+)
 create mode 100644 c
ebg@ebg(173)$ ls
a       b       c
ebg@ebg(174)$ git log --graph --oneline --all
*   0ffd24e Merge branch 'master' into second
|\  
| * 64c9f45 A2/c:carrot
* | 2b1de0c Revert "B1/b:beets;d:dandelion"
* | ad6fb9d B1/b:beets;d:dandelion
|/  
* 07a4ceb A1/a:apple;b:banana
GoZoner
  • 67,920
  • 20
  • 95
  • 145
  • Unfortunately, when I simplified my real problem to ask the question I left out the fact that the "second" branch is actually a remote tracking branch, so the B1 state has in effect been pushed to a remote server. I can't just reset the branch arbitrarily. – David Hull Apr 04 '13 at 03:10
  • Updated and used your exact inputs. But, used `revert` – GoZoner Apr 04 '13 at 03:36