31

I'm trying to have two branches with binary files in git - one "development" and one "stable". The development branch can have several changes of these files before I want to "release" them to the stable branch (and the stable branch has those files renamed, in case that's relevant).

I could do a normal merge, this works fine, but preserves too much history - when pulling the "stable" branch, all intermediate commits from the "development" branch are pulled as well (because they're parent commits). But we're talking about binary files that don't have any sensible merging strategy (except theirs/ours), so the actual history of the files on the development branch is useless. When I pull the "stable" branch, I get this:

          X-------------------G stable
         /                   /
    a---b---c---d---e---f---g development

Because G has a parent in the development branch, I get the whole history of the development branch (data objects for c, d, e, f and g) in my repository, which I'm not interested in (X is the same as b, with some file renaming applied).

So I tried to git merge --squash changes from the development branch to the stable branch. The first such merge and commit went OK, the results were as expected (nice change log in the commit message, no relation to the development branch):

          X-------------------G stable
         /                   
    a---b---c---d---e---f---g development

After I pull this squashed stable branch, I get this in my repository, which is what I want:

a---b---X---G

But the second merge failed (because git had no way to know how much I merged already and got confused).

  • Is it possible to somehow record the merge, without producing a "merge commit" with two parents?
  • Or, is it possible to tell git to merge only a certain "range" of revisions, like in SVN?
  • Or, is it possible to do a normal merge without having to download all refs from the other branch when pulling?
  • Or should I provide a custom merge driver for the files in question, that simply renames "their" version to "our", thereby resolving the conflicts? I'm still afraid that --squash will always try to merge the whole history, up to the common parent, solving only half of my problem.

Update: rebasing

If I understand rebasing correctly, I'll end up with this:

                              X stable
                             /
    a---b---c---d---e---f---g development

Which gets me all the data I'm not interested in (c, d, e, f) and as a bonus, I'll loose the information that b was a stable version in the branch.

Each development revision adds about 5MB to the repository size (and repacking the whole repo shrinks it only about 10%), the "stable" branch comes almost for free (the data is already there). I want pulling a single new revision from the stable branch to pull only the new 5MB, but instead updating from X to G downloads 25MB, because I'm somehow not able to say that I don't care about the contents of c, d, e and f.

Ken Williams
  • 22,756
  • 10
  • 85
  • 147
Kim Sullivan
  • 943
  • 2
  • 10
  • 15

5 Answers5

26

Starting with

      X stable
     /                   
a---b---c---d---e---f---g development

You can use the following steps to copy the last commit from your development branch to your stable branch:

git checkout development@{0}  # get working tree from "development", detach HEAD
git reset --soft stable  # reposition detached HEAD on "stable"
git commit  # enter the appropriate commit message
git branch temp  # create a temporary branch "temp" at HEAD
git checkout temp  # get on the new temporary branch
git branch -M stable  # rename "temp" to "stable"

so you end up with:

      X-------------------G stable
     /                   
a---b---c---d---e---f---g development

If you continue work on "development", e.g.,

      X-------------------G stable
     /                   
a---b---c---d---e---f---g---h---i---j development

you can repeat the same git commands as above and you will end up with:

      X-------------------G-----------J stable
     /                   
a---b---c---d---e---f---g---h---i---j development
pvillela
  • 553
  • 1
  • 6
  • 10
  • 1
    This looks great, but I don't understand exactly why it's needed to create a temp branch and then rename it. Is it because you're still detached after the commit? – Kim Sullivan Jan 05 '11 at 14:53
  • @kim-sullivan: the "exactly why": when you do the commit in the above example, it moves `HEAD` but not `stable` to point to the new commit -- the head is still "detached", and `stable` still points to the previous version (X, in your original example). Doing the branch and rename (the checkout is technically optional, though without it you'll need to supply to arguments to `git branch -M`, and you'll still be detached, which is likely non-desirable) causes stable to point to J (per the answer). – lindes Feb 16 '11 at 21:54
  • 4
    Also: You may find running `git log --graph --oneline --all --decorate` and `git status` at each point along this series of commands to be informative. And/or looking at the contents of various files in .git/, perhaps with `grep . .git/HEAD .git/refs/heads/*` – lindes Feb 16 '11 at 21:57
  • 2
    I'm maybe wrong but... resetting (`--soft`) to `stable` will undo the effects of `X`. The correct target is the common ancestor of `stable` and `development` (`git merge-base stable development`). – cYrus Oct 30 '11 at 19:22
  • Instead of creating a temporary branch, would it also be effective to `git branch -f stable` after you make your commit? (and then reattach your head to stable with `git checkout stable`) – Iron Savior Apr 15 '13 at 19:49
  • This looks really interesting - I have a similar [question](http://stackoverflow.com/questions/16424774/git-merge-squash-conflict-resolution-when-all-i-want-is-the-changes-from-the-b). Would the method you describe produce the correct results on [deleted files](http://stackoverflow.com/a/14343784/281545) ? More generally (and importantly) - _will the working tree be identical in `g` and `G`_ ? – Mr_and_Mrs_D May 08 '13 at 18:36
  • 1
    It appears the answer to my previous comment is yes – Mr_and_Mrs_D Jul 18 '13 at 20:31
  • 1
    @cYrus: [you are wrong](https://makandracards.com/makandra/11485-git-basics-checkout-vs-reset) apparently : "If you are in detached HEAD state, git reset does only move HEAD." - so HEAD is moved to stable but because `--soft` is issued the working tree/index stay as they were – Mr_and_Mrs_D Jul 18 '13 at 22:44
8

This isn't the right place to use merge --squash. One good place to use it is in a throwaway topic branch, which you're going to merge into your main branch and then get rid of. All the development done in the topic branch is shown as one commit in the main branch. In your situation, you should merge from the development branch normally, and then use git-rebase --interactive to squash the commits you want.

Is it possible to somehow record the merge, without producing a "merge commit" with two parents?

If I understand the question correctly, no. Absolutely not.

Is it possible to tell git to merge only a certain "range" of revisions, like in SVN?

Yes. git merge [commit hash]

Or, is it possible to do a normal merge without having to download all refs from the other branch when pulling?

See answer to previous question.

Or should I provide a custom merge driver for the files in question, that simply renames "their" version to "our", thereby resolving the conflicts? I'm still afraid that --squash will always try to merge the whole history, up to the common parent, solving only half of my problem.

No! Just don't use git merge --squash. This isn't the right place to use it!

Paul T. Rawkeen
  • 3,994
  • 3
  • 35
  • 51
artagnon
  • 3,609
  • 3
  • 23
  • 26
  • Thanks for your answer! I'm not sure that rebasing is what I need. I do need to record some history in the "stable" branch, so I can go back and forth between "releases". This is what I want: ` D -> S D | D | D -> S D | D -> S` But I only need the history from the S branch - I don't care about the history from the D branch. The problem I need to solve is how to pull the S branch without pulling all parent refs from the D branch (if I understand it correctly, rebasing gets rid of all previous S commits which is not what I want). ` D -> x D D D -> x D D -> S` – Kim Sullivan Sep 23 '09 at 10:23
  • Yes, I understand that you need to preserve the existing history from the S branch. No, rebasing doesn't get rid of all previous S commits; before merging development into stable, note the [commit hash] and then git rebase -i [commit hash] - then you'll effectively only be squashing the commits you just merged. Also, as far as releases are concerned, why not just tag specific commits as releases? – artagnon Sep 23 '09 at 11:43
  • I'm sorry I don't quite understand your D -> S D | D diagram too well. I hope my previous comment clarified everything. – artagnon Sep 23 '09 at 11:44
  • I didn't realize comments don't support formatting, I'll update my question. – Kim Sullivan Sep 23 '09 at 12:48
  • I hope the question makes more sense now. The problem is with repository size and slow connections. I was hoping there was a way to use a single git repository for deployment for both testing and stable binaries. But the more I think about this, the more I come to realize that Git just wasn't designed to do this. I'll either have to accept the high bandwidth cost associated with pulling both deployment and stable binaries, or use some other mechanism (maybe a centralised versioning system, or lots of shell scripts that "version" the binaries on a plain filesystem and deploy using plain copy). – Kim Sullivan Sep 23 '09 at 14:14
  • 1
    "But the more I think about this, the more I come to realize that Git just wasn't designed to do this" -- Exactly. A plausible solution: Remove S. Create discrete flat-file (tarball?) releases from tags in D. Git was never meant to handle binaries. – artagnon Sep 23 '09 at 14:48
4

you could use git rebase -i (interactive mode) to sqash all your changes, just change the lines to m or meld (update: for newer Git releases type s or squash)

you will than have a single commit which you can merge. if you want every step of yours reserved, you have to create another branch on your hacking branch before doing the rebase, this can be done with git branch small-dirty-changes-i-don’t-want-anybody-to-see

all this has to be done before attempting to merge; step by step:

# on branch "hacking": hack commit hack commit hack commit

# create a reference to your history
$ git branch dirty-history

# sqash your commits by setting all lines to "meld"
$ git rebase -i

# checkout master or the branch you want to merge to
$ git checkout master

# merge your squashed commit
$ git merge hacking
# or: $ git pull hacking
knittl
  • 246,190
  • 53
  • 318
  • 364
  • Hm, I probably could rebase the changes on the development branch ("dirty") once in a while (actually, before merging to stable and releasing), so I don't have all those intermediate binaries I don't care about much. The only important thing between stable releases is the hash and the commit messages... The hash is important for testing (which build was tested and which was not). I'll have to think about it some more. – Kim Sullivan Sep 24 '09 at 07:50
  • good answer, I guess "meld" nowadays means "squash" (your answer is from 2009!) – R Yoda Dec 03 '17 at 17:15
  • 1
    Yes, it's now `s`/`squash`. – knittl Dec 03 '17 at 17:21
2

The top voted answer effectively diffs the stable and the development branches and applies all changes missing in the stable as one commit. There is an alternative way to do this:

$ git checkout stable 
$ git diff stable development | git apply --index -   
$ git commit

Before you commit, you can review the staged files. If you want to abort the process after you applied the diff, you can just use basic commands to remove changes from index and working directory.

$ git reset --hard

You can repeat the process any number of times, since you are effectively just diff-ing the tips of both the branches each time you need to merge.

Note that there is assumption here that applies to the other answer as well: there are no commits in the stable branch other than the ones that came from the development branch. If the same line were changed independently in both branches, unlike a regular merge, this approach would not give you a conflict. Instead, it would just override the changes in the stable branch with the ones in development.

If you are concerned about this, each time you commit on stable, you can put the merged development commit hash range in the commit message. That way, you have some record, if you ever need to backtrack.

Community
  • 1
  • 1
Raghu Dodda
  • 1,505
  • 1
  • 21
  • 28
  • This is exactly what I wanted by combining this with a commit message containing the old hash as you suggested. The default "merge strategy" of "theirs" this provides is also perfect. I just want a safe public branch without history :) – Thomas Boby May 05 '18 at 17:11
0

I'm new to git, but it seems like --squash is what you want, you just need to handle the branch differently.

$ git merge --squash development
$ git branch -d development
$ git checkout -b development
# continue work on development branch

This would essentially truncate the history of the development branch, and the next merge would only be what you wanted. Looking at the man page it seems like git branch -f development might do the same thing as deleting and recreating the branch.

As I said, I'm pretty new to git, so I'd love some feedback on this idea.

Glenn Moss
  • 6,812
  • 6
  • 29
  • 23