git command for making one branch like another
Asked Answered
S

9

94

I'm trying to take a branch with changes and bring it back to be identical to the upstream it diverged from. The changes are both local and have been pushed to github, so neither git reset or git rebase are really viable, since they change history, which is a bad thing with a branch that's already been pushed.

I've also tried git merge with various strategies but none of them undo the local changes, i.e. if I'd added a file, a merge might bring other files back in line, but I'll still have that file that the upstream doesn't have.

I could just create a new branch off the upstream, but i'd really like a merge that in terms of revision history applies all the changes to take my branch and make it identical to the upstream again, so that I can safely push that change without clobbering history. Is there such a command or series of commands?

Scottscotti answered 6/2, 2011 at 5:33 Comment(5)
If you don't care about preserving the changes, why not just delete and recreate the branch? "The history of the project" needn't be sacred. Git is a tool to help developers communicate. If these changes don't help that, throw them away.Susa
+100 @Susa - especially if the changes were already merged in.Taproom
I care about preserving the history both because it's published for collaboration and I might want to go back to it. Why bother using revision control if you only keep the latest?Scottscotti
This is a subjective argument, but to me, the purpose of a VCS is not to record every minutia of the history of the project, but only to record changes to the content (commits), to allow you to manipulate the tree/history based on those commits (branching, merging, rebasing, resetting, etc), and allow you to view reports based on the history (diffs, logs, blame, etc). git is the "stupid content tracker" - I view it as a tool to manage source code, not a time machine.Taproom
As you said, it's subjective. I care about being able to review abandoned approaches, and being able to see what decisions were made at some time in the past. And I care that my decision to abandon something doesn't destroy the merge points others might be pointing at.Scottscotti
J
129

You could merge your upstream branch to your dev branch, with a custom merge driver, "keepTheirs":
See "git merge -s theirs” needed — but I know it doesn't exist".
In your case, only one .gitattributes would be required, and a keepTheirs script like:

mv -f $3 $2
exit 0

git merge --strategy=theirs Simulation #1

Shows as a merge, with upstream as the first parent.

Jefromi mentions (in the comments) the merge -s ours, by merging your work on the upstream (or on a temp branch starting from upstream), and then fast-forwarding your branch to the result of that merge:

git checkout -b tmp origin/upstream
git merge -s ours downstream         # ignoring all changes from downstream
git checkout downstream
git merge tmp                        # fast-forward to tmp HEAD
git branch -D tmp                    # deleting tmp

This has the benefit of recording the upstream ancestor as the first parent, so that the merge means "absorb this out-of-date topic branch" rather than "destroy this topic branch and replace it with upstream".

Update 2023: for instance, if you want main to reflect exactly what dev is:

git switch -c tmp dev
git merge -s ours main   # ignoring all changes from main
git switch main
git merge tmp            # fast-forward to tmp HEAD, which is dev
git branch -D tmp        # deleting tmp

(Edit 2011):

This workflow has been reported in this blog post by the OP:

Why do I want this again?

As long as my repo had nothing to do with the public version, this was all fine, but since now I'd want the ability to collorate on WIP with other team members and outside contributors, I want to make sure that my public branches are reliable for others to branch off and pull from, i.e. no more rebase and reset on things I've pushed to the remote backup, since it's now on GitHub and public.

So that leaves me with how I should proceed.
99% of the time, my copy will go into the upstream master, so I want to work my master and push into upstream most of the time.
But every once in a while, what I have in wip will get invalidated by what goes into upstream, and I will abandon some part of my wip.
At that point, I want to bring my master back in sync with upstream, but not destroy any commit points on my publicly pushed master. I.e. I want a merge with upstream that ends up with the changeset that make my copy identical to upstream.
And that's what git merge --strategy=theirs should do.


git merge --strategy=theirs Simulation #2

Shows as a merge, with ours as the first parent.

(proposed by jcwenger)

git checkout -b tmp upstream
git merge -s ours thebranch         # ignoring all changes from downstream
git checkout downstream
git merge --squash tmp               # apply changes from tmp, but not as merge.
git rev-parse upstream > .git/MERGE_HEAD #record upstream 2nd merge head
git commit -m "rebaselined thebranch from upstream" # make the commit.
git branch -D tmp                    # deleting tmp

git merge --strategy=theirs Simulation #3

This blog post mentions:

git merge -s ours ref-to-be-merged
git diff --binary ref-to-be-merged | git apply -R --index
git commit -F .git/COMMIT_EDITMSG --amend

sometimes you do want to do this, and not because you have "crap" in your history, but perhaps because you want to change the baseline for development in a public repository where rebasing should be avoided.


git merge --strategy=theirs Simulation #4

(same blog post)

Alternatively, if you want to keep the local upstream branches fast-forwardable, a potential compromise is to work with the understanding that for sid/unstable, the upstream branch can from time to time be reset/rebased (based on events that are ultimately out of your control on the upstream project's side).
This isn't a big deal, and working with that assumption means that it's easy to keep the local upstream branch in a state where it only takes fast-forward updates.

git branch -m upstream-unstable upstream-unstable-save
git branch upstream-unstable upstream-remote/master
git merge -s ours upstream-unstable
git diff --binary ref-to-be-merged | git apply -R --index --exclude="debian/*"
git commit -F .git/COMMIT_EDITMSG --amend

git merge --strategy=theirs Simulation #5

(proposed by Barak A. Pearlmutter):

git checkout MINE
git merge --no-commit -s ours HERS
git rm -rf .
git checkout HERS -- .
git checkout MINE -- debian # or whatever, as appropriate
git gui # edit commit message & click commit button

git merge --strategy=theirs Simulation #6

(proposed by the same Michael Gebetsroither):

Michael Gebetsroither chimed in, claiming I was "cheating" ;) and gave another solution with lower-level plumbing commands:

(it wouldn't be git if it wouldn't be possible with git only commands, everything in git with diff/patch/apply isn't a real solution ;).

# get the contents of another branch
git read-tree -u --reset <ID>
# selectivly merge subdirectories
# e.g. superseed upstream source with that from another branch
git merge -s ours --no-commit other_upstream
git read-tree --reset -u other_upstream     # or use --prefix=foo/
git checkout HEAD -- debian/
git checkout HEAD -- .gitignore
git commit -m 'superseed upstream source' -a

git merge --strategy=theirs Simulation #7

The necessary steps can be described as:

  1. Replace your worktree with upstream
  2. Apply the changes to the index
  3. Add upstream as the second parent
  4. Commit

The command git read-tree overwrites the index with a different tree, accomplishing the second step, and has flags to update the work tree, accomplishing the first step. When committing, git uses the SHA1 in .git/MERGE_HEAD as the second parent, so we can populate this to create a merge commit. Therefore, this can be accomplished with:

git read-tree -u --reset upstream                 # update files and stage changes
git rev-parse upstream > .git/MERGE_HEAD          # setup merge commit
git commit -m "Merge branch 'upstream' into mine" # commit

Git 2.44 (Q1 2024) also proposes a custom merge driver approach (that I illustrate in "git merge -s theirs needed, but it does not exist").

Custom merge drivers need access to the names of the revisions they are working on, so that the merge conflict markers they introduce can refer to those revisions.
The conflict labels to be used for the common ancestor, local head and other head can be passed by using the placeholders '%S', '%X' and '%Y' respectively.

Joeljoela answered 6/2, 2011 at 8:3 Comment(7)
You can always just use ours instead of theirs: check out the other branch, merge yours into it, then fast-forward yours to the merge. git checkout upstream; git merge -s ours downstream; git checkout downstream; git merge upstream. (Use a temporary branch at upstream if necessary.) This has the benefit of recording the upstream ancestor as the first parent, so that the merge means "absorb this out-of-date topic branch" rather than "destroy this topic branch and replace it with upstream".Afire
@Jefromi: excellent point, as usual. I have included it in my answer.Joeljoela
Another option -- like git merge --strategy=theirs Simulation #1 -- except this one preserves the brange as the first merge parent: git checkout -b tmp origin/upstream git merge -s ours downstream # ignoring all changes from downstream git checkout downstream git merge --squash tmp # apply changes from tmp but not as merge. git rev-parse upstream > .git/MERGE_HEAD #record upstream as the second merge head git commit -m "rebaselined ours from upstream" # make the commit. git branch -D tmp # deleting tmpFloruit
Wow, who would have thought that --strategy=theirs could be implemented in so many ways. Now if it could just be in the next version of gitScottscotti
VonC and his knowledge is astounishing. He's like the JonSkeet of git. :)Malcom
Seems to be in Git now.Valuer
@Valuer which part of the answer "seems to be in Git now"? -s theirs (merge strategy) still doesn't exist. This isn't the same than a merge strategy option -X theirs (as in https://mcmap.net/q/12935/-getting-merged-code-quot-right-quot-with-git), which is for conflicts only.Joeljoela
T
13

It sounds to me like you just need to do:

$ git reset --hard origin/master

If there is no change to push upstream, and you simply want the upstream branch to be your current branch, this will do that. It is not harmful to do this locally but you will lose any local changes** that haven't been pushed to master.

** Actually the changes are still around if you have committed them locally, as the commits will still be in your git reflog, usually for at least 30 days.

Taproom answered 6/2, 2011 at 8:13 Comment(1)
this works if you need that change at any price , because it may change the history of the branch (you have to push with -f). that can be configured to be blocked, so basically it will work only for you own private repositories, in practice.Prakrit
K
13

You can do this rather easily now:

$ git fetch origin
$ git merge origin/master -s recursive -Xtheirs

This gets your local repo in-sync with the origin, and preserves the history.

Karyolymph answered 16/11, 2012 at 4:30 Comment(6)
git merge -s recursive -Xtheirs does not automatically merge binary files, so you end up in a conflict situation which you have to resolve manually. Workflows based on git merge -s ours does not suffer from this.Ambidextrous
This appears to create an empty commit.Lubricator
My apologies - it does in fact work, but git show on a merge commit only shows conflict resolutions, and there are no conflict resolutions if -Xtheirs are used, obviously.Lubricator
worked like a charm! in my situation i had checked out an old commit, was hence in a detached state, but had continued coding in that state and eventually wanted to bring exactly that code onto the branch that i had originally detached from (dev). i created a new branch (temp), commited everything, then checkout out to dev and did this: git merge temp -s recursive -XtheirsGun
This ALMOST worked for me. I merged branch2 into branch1, using -s recursive -Xtheirs. I'm paranoid so I kept a pre-merge copy of the repository in another folder, still on branch2, to compare the results. Afterwards, the newly merged branch1 was identical to branch2, except one file in the merged result had code that had been deleted in branch2. It's like the merge missed one commit. Does this merge strategy work for commits that simply remove text?Hubie
It should be noted, this doesn't do what the question asked for. It only takes upstream changes (theirs) in case of conflicts. But the question wanted to get rid of changes in the current branch, even if they don't conflict with anything. " ours This option forces conflicting hunks to be auto-resolved cleanly by favoring our version. ... theirs This is the opposite of ours; ... "Boak
T
9

Another simulation for git merge -s theirs ref-to-be-merged:

git merge --no-ff -s ours ref-to-be-merged         # enforce a merge commit; content is still wrong
git reset --hard HEAD^2; git reset --soft HEAD@{1} # fix the content
git commit --amend

An alternative to the double reset would be applying the reverse patch:

git diff --binary ref-to-be-merged | git apply -R --index
Theda answered 1/2, 2014 at 13:27 Comment(5)
Interesting use of the reverse patch. +1Joeljoela
The reset didn't work for me, I got "fatal: ambiguous argument 'HEAD2': unknown revision or path not in the working tree.". (Yes, I did type HEAD^2) The patch method did work.Pentarchy
@Stijn: Most likely you actually did not type ^ correctly. Sometimes other keystrokes like "ctrl-c" are displayed as "^C". - If you really typed the right "^" you found a serious bug in your git version.Theda
@Theda On Windows the ^ character is used for escaping, so in order to use it as a literal you have to escape it with itself. git reset --hard HEAD^^2; git reset --soft HEAD@{1}Charlinecharlock
git diff | git apply is brilliant, thanksHistrionics
W
4

There's also a way with little help of plumbing command - IMHO the most straightforward. Say you want to emulate "theirs" for 2 branches case:

head1=$(git show --pretty=format:"%H" -s foo)
head2=$(git show --pretty=format:"%H" -s bar)
tree=$(git show --pretty=format:"%T" -s bar)
newhead=$(git commit-tree $tree -p $head1 -p $head2 <<<"merge commit message")
git reset --hard $newhead

This merges arbitrary number of heads (2 in the example above) using tree of one of them (bar in the example above, providing 'theirs' tree), disregarding any diff/file issues (commit-tree is low level command, so it doesn't care about those). Note that head can be just 1 (so equivalent of cherry-pick with "theirs").

Note, that which parent head is specified first, can influence some stuff (see e.g. --first-parent of git-log command) - so keep that in mind.

Instead of git-show, anything else capable of outputting tree and commit hashes can be used - whatever one's is used to parsing (cat-file, rev-list, ...). You can follow everything with git commit --amend to interactively beautify commit message.

Widow answered 6/9, 2012 at 13:22 Comment(1)
This is the most straightforward in my eyes. You're using plumbing commands to say "Create a new commit object with this tree, this first parent, this second parent. Then point the head to this new commit.", which is exactly what we want "git merge -s theirs" to do. The drawback is that you need to save 4 different hashes to make this operation work.Waive
D
2

Heavy handed, but hell, what can possibly go wrong?

  • Check out the branch X you want to look like the Y
  • cp -r .git /tmp
  • Check out branch Y git checkout y
  • rm -rf .git && cp -r /tmp/.git .
  • Commit & push any difference
  • DONE.
Donny answered 6/3, 2017 at 12:40 Comment(1)
This is the most simple, brute force way to make two branches identical, assuming you don't care about maintaining a merge history.Cropland
M
1

change to the remote upstream branch and do a git merge with the merge strategy set to ours.

git checkout origin/master
git merge dev --strategy=ours
git commit ...
git push

All the history will still be present, but you'll have an extra merge commit. The important thing here is to start from the version you want to be at and merge ours with the branch github is actually at.

Melyndamem answered 6/2, 2011 at 5:41 Comment(6)
I need the opposite. That will take my branch and integrate it into the upstream, but leave the upstream head unchanged. But I need to take the upstream and integrate it into my branch leaving my head to look like the upstream. Basically something like --strategy=theirs, except the closest --strategy=recursive -X=theirs doesn't quite do that.Scottscotti
--strategy=theirs is just the opposite of --strategy=ours. You start from the opposite end (so start from github and merge the other way).Melyndamem
there is no --strategy=theirs, which is the problem. The closest is --strategy=recursive -X theirs which isn't quite the opposite, since it won't remove the extraneous local changes, if they don't conflict.Scottscotti
These two are opposites: git checkout dev; git merge origin/master --strategy=ours and git checkout origin/master; git merge dev --strategy=oursTaproom
@Arne: See my comment on VonC's answer. The presence of the ours strategy makes it completely possible to do a theirs strategy.Afire
@jefromi @Melyndamem thanks for clarifying how i can use ours to fake theirs. Was missing that the ours merge was a temp branch that i'd then have to merge back into my dev.Scottscotti
T
1

Use git reset BACKWARDS!

You can make a branch look like any other commit with git reset, but you have to do it in a round-about way.

To make a branch on commit <old> look like a commit <new>, you can do

git reset --hard <new>

in order to make <new> the contents of the working tree.

Then do

git reset --mixed <old> 

to change the branch back to the original commit but leaving working tree in the <new> state.

Then you can add and commit the changes, in order to make your branch exactly match the contents of the <new> commit.

It's counter-intuitive that to move from the <old> state to the <new> you need to do a git reset from <new> to <old>. However with the option --mixed the working tree is left at <new> and the branch pointer set to <old>, so that when the changes are committed the branch looks how we want.

Warning

Don't lose track of your commits, e.g. forget what <old> is when doing git reset --hard <new>.

Toomey answered 31/10, 2018 at 10:12 Comment(0)
M
1

I followed those roles:

Fetching, reset hard from the branch then recursive from theirs and then forced push to the branch

ON YOUR OWN RISK

git fetch
git reset --hard <branch>
git merge <branch> -s recursive -X theirs
git push -f <remote> <branch>
Magneto answered 12/2, 2019 at 10:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.