0

I'm trying to figure out a workflow for a shared repository that multiple people will push to. The goal is to give people the ability to commit locally as often as they want to, retaining information on when they created and merged branches, while keeping a clean history on the branch they eventually push to.

With the man page for git-merge stating that merge

...will replay the changes made on the topic branch since it diverged from master (i.e., E) until its current commit (C) on top of master, and record the result in a new commit...

what I THOUGHT you could do is the following:

  1. Clone repository, need to change something in branch master
  2. Create a local branch branch, do all the work in say 5 commits
  3. Merge the local branch back into master with the --no-ff switch, forcing the creation of a merge commit (which contains the changes of all merged commits).
  4. Push that merge commit master

The local history I get when I do this looks like this:

* cada35b - Tue, 26 Feb 2019 08:55:45 +0100 (21 minutes ago) (HEAD -> master, origin/master) | 6 - dev * 8391544 - Tue, 26 Feb 2019 08:55:44 +0100 (21 minutes ago) | 5 - dev * 4381abd - Tue, 26 Feb 2019 08:55:41 +0100 (21 minutes ago) | 4 - dev * 40e21b1 - Tue, 26 Feb 2019 08:54:49 +0100 (22 minutes ago) |\ #3254 Important Feature - Merge branch 'branch' - dev <-- Merge commit | * 4f595e8 - Tue, 26 Feb 2019 08:54:38 +0100 (22 minutes ago) (branch) | | 3.3 - dev | * ea05ba7 - Tue, 26 Feb 2019 08:54:36 +0100 (22 minutes ago) | | 3.2 - dev | * d779583 - Tue, 26 Feb 2019 08:54:34 +0100 (22 minutes ago) |/ 3.1 - dev * fab5a25 - Tue, 26 Feb 2019 08:54:20 +0100 (22 minutes ago) | 3 - dev * b6ddac3 - Tue, 26 Feb 2019 08:54:19 +0100 (23 minutes ago) | 2 - dev * 0abafad - Tue, 26 Feb 2019 08:54:18 +0100 (23 minutes ago) 1 - dev 

This is correct and what I want. What I would have expected (and need) on the remote is a history that looks like this:

* cada35b - Tue, 26 Feb 2019 08:55:45 +0100 (7 minutes ago) (HEAD -> master, origin/master, origin/HEAD) | 6 - dev * 8391544 - Tue, 26 Feb 2019 08:55:44 +0100 (7 minutes ago) | 5 - dev * 4381abd - Tue, 26 Feb 2019 08:55:41 +0100 (7 minutes ago) | 4 - dev * 40e21b1 - Tue, 26 Feb 2019 08:54:49 +0100 (8 minutes ago) | #3254 Important Feature - Merge branch 'branch' - dev <-- Merge commit * fab5a25 - Tue, 26 Feb 2019 08:54:20 +0100 (9 minutes ago) | 3 - dev * b6ddac3 - Tue, 26 Feb 2019 08:54:19 +0100 (9 minutes ago) | 2 - dev * 0abafad - Tue, 26 Feb 2019 08:54:18 +0100 (9 minutes ago) 1 - dev 

However, what I actually get is all the commits I made locally, just without the branch pointer for the branch I didn't push:

* cada35b - Tue, 26 Feb 2019 08:55:45 +0100 (7 minutes ago) (HEAD -> master, origin/master, origin/HEAD) | 6 - dev * 8391544 - Tue, 26 Feb 2019 08:55:44 +0100 (7 minutes ago) | 5 - dev * 4381abd - Tue, 26 Feb 2019 08:55:41 +0100 (7 minutes ago) | 4 - dev * 40e21b1 - Tue, 26 Feb 2019 08:54:49 +0100 (8 minutes ago) |\ #3254 Important Feature - Merge branch 'branch' - dev <-- Merge commit | * 4f595e8 - Tue, 26 Feb 2019 08:54:38 +0100 (8 minutes ago) | | 3.3 - dev | * ea05ba7 - Tue, 26 Feb 2019 08:54:36 +0100 (8 minutes ago) | | 3.2 - dev | * d779583 - Tue, 26 Feb 2019 08:54:34 +0100 (8 minutes ago) |/ 3.1 - dev * fab5a25 - Tue, 26 Feb 2019 08:54:20 +0100 (9 minutes ago) | 3 - dev * b6ddac3 - Tue, 26 Feb 2019 08:54:19 +0100 (9 minutes ago) | 2 - dev * 0abafad - Tue, 26 Feb 2019 08:54:18 +0100 (9 minutes ago) 1 - dev 

Is there any way to only push the merge commit and have it contain all the changes that were made on the local branch? I am aware merge --squash and rebase -i exist, but with those you cannot see at what point something was merged back into a branch (merge --squash) or lose the local history entirely (rebase -i).

6
  • Which command did you use exactly? git log ...? Commented Feb 26, 2019 at 9:14
  • this is essentially the output of git log --all --graph excluding some formatting switches i use (which don't show up here anyway) Commented Feb 26, 2019 at 9:37
  • You cannot get different commit histories in local repo and on remote. If your local branch contains a merge commit, and you push that branch, the remote branch will also have a merge commit. Commented Feb 26, 2019 at 11:37
  • @Alderath That would be fine. What I don't want to see are the commits that don't belong to master, but those show up as well (see third listing). The branch I merged from does not exist on the remote, yet the commits that belong to it show up anyway. Commented Feb 26, 2019 at 12:21
  • That's because you merged them by doing git merge. That commit creates a merge commit with two parents. If you push your local branch which contains this two-parent-commit, your remote branch will inevitably also get the two-parent-commit. You could, as you mentioned, do ` git merge --squash`, this way, you'll be creating one commit which contains all changes made on the side branch, with only one parent (ie. you will be dropping the history of the side branch. You can choose one of these approaches, but you cannot have one of them on your local branch and the other on the remote branch. Commented Feb 26, 2019 at 12:44

1 Answer 1

3

The answer is no, you can't get what you want here and the reason is that Git is really all about commits, set in stone and saved forever,1 in exactly the form they had when you made them. This includes their commit graph linkages, because each commit links to its parent commits via their raw hash IDs. The push and fetch operations in Git transfer these commits and include all the commits needed to make the graph complete. The names—branch names especially—are largely unimportant and ephemeral, with one enormous exception.

The exception here is that in order to remember and find commits, Git needs the names. The names locate the end of a graph chain. From there, Git finds all the ancestor commits by following the graph.

Since a merge commit has two parents, and the graph must always be complete,2 whichever Git receives the merge commit also gets all of its ancestors, i.e., both "sides" of the merge. A commit that had only one parent would be a different commit, with a different hash ID.

That's the sort of thing that git merge --squash produces: a new, different commit with a different hash ID, but with the same tree—snapshot—that a real merge would produce. But the side effect of using git merge --squash is that the development branch—the name you used in your argument to git merge --squash—is now essentially dead. You don't have to kill it off, but you probably should. Note that this, too, is OK: if that's what you want—if you want to kill off the development branch and replace the five (or however many) commits with a single commit—then git merge --squash is probably the way to go.

There's no single right work-flow here, but one possibility is for your developers to not merge their work into the next-level-up branch, but rather to merge the next-level-up branch into their work. That is, I might begin with this:

...--o--o--*--...? <-- origin/mainline \ 1--2--3--4--5 <-- feature (HEAD) 

I started my feature branch from commit *, so my feature branch contains the five commits shown. Meanwhile mainline may have grown. So I run git fetch to update my origin/mainline and now I see:

...--o--o--*----o----o----A <-- origin/mainline \ 1--2--3--4--5 <-- feature (HEAD) 

At this point, I can, if I so choose, run git merge origin/mainline to create my new merge commit M1 on feature:

...--o--o--*----o----o----A <-- origin/mainline \ \ 1--2--3--4--5--M1 <-- feature (HEAD) 

The snapshot in M1 is the same as the snapshot I would have made, had I done git checkout mainline (creating my own mainline from origin/mainline) and then git merge feature and then git push origin mainline and then git checkout feature and git branch -D mainline. But if I had done that I'd now have:

...--o--o--*----o---o---A--M2 <-- origin/mainline \ / 1--2--3--4--5 <-- feature (HEAD) 

Note that M1 and M2 have the same snapshot, the same author and committer, and the same parent hash IDs, but probably different time-stamps. It's only the time-stamps that make M1 and M2 themselves different! If I were to somehow do these two operations in parallel, in two different Git repositories, at exactly the same time, M1 and M2 would have the same hash, and—since the graph is the same—the only difference is the set of labels. We can draw this situation more clearly by wobbling the graph nodes about a bit and just calling the merge M, and removing the two labels:

 o-----o-----A / \ ...--o--o--* M \ / 1--2--3--4--5 

If we paste the label mainline or origin/mainline onto commit A, and the label feature onto commit M, we have situation #1: I merged mainline into feature. If we paste the label mainline or origin/mainline onto commit M, and the label feature onto commit 5, we have situation #2: I merged feature into mainline.

In other words, both merges do exactly the same thing, except for the arrangement of the labels. So then, when I use git push to send the commit graph to the push-to repository at origin, it doesn't matter which way I did the merge. The only thing that matters is what label I ask the Git at origin to use to remember commit M: do I ask it to set its feature, or do I ask it to set its mainline, or do I make up a third name such as torek/feature?

In my repository, I have my branch names (feature, mainline if I choose to keep one, master if I choose to keep that, and so on). In your repository over on origin, you have your branch names. We both share commits, and my Git remembers your branch names—as of my last fetch or push—under my origin/whatever names.

The graph is shared. The labels are per-repository. And that's (mostly) all there is to a repository: the graph and some labels. It's up to whoever looks at the graph (and the labels) to guess what the intent is, and to work from there. So you have to define your work-flows around these basic facts. The git push command is going to send new commits that add to the graph, and then set some label(s) in the Git at origin.

If I do my merge into my feature I can then continue working like this:

...--o--o--*----o----o----A--o--o--o---B <-- origin/mainline \ \ \ 1--2--3--4--5--M1--6--7--8--M2 <-- feature (HEAD) 

My merge M2 is, perhaps, easier than if I had not made M1, because my merge base for creating M2 is commit A and my two tip commits are commits 8 and B. When I made M1 I had merge base * and branch tips 5 and A. If feature is ultimately going to be squash-merged into mainline, with feature killed off and commits 1-2-3-4-5-M1-6-7-8-M2 forgotten and eventually garbage-collected, this is a reasonable way to go about the process.


1Forever is too strong. Commits last while they can be reached. The concept of reachability is pretty central to Git; see Think Like (a) Git. Branch names—and tag names, and any other reference for that matter—serve to keep commits reachable. Deleting a branch name may release its commits for Git's garbage collector to come along and collect, if they're not protected via some other name and its commit ancestry.

2Git makes an exception to the completeness rules for shallow repositories, but they don't actually help you here. A shallow repository is one with one or more commits that do have some parent(s) with some hash IDs, but those parent commits are just missing. There's a file named .git/shallow that contains the hash IDs of the commits whose parents are missing. When Git is about to work with such a commit, instead of looking at its parent IDs, it notices that this commit is listed in .git/shallow, and for some purposes—such as git log—just pretends that the commit has no parents. So this lets you, for space and/or time reasons, cut off the history of a commit, but it's just cut off, like a pruned tree limb, and later you'll graft it back on in place and everything will heal up and the graph will be complete—in all repositories everywhere, since the commits are shared and this one didn't change.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.