How to have git-push permanently skip commits from a detailed branch?

Viewed 165

For one repo I'm using a simple script to regularly commit changes to a file to keep a rather detailed and non-logical history that I'd like to keep for my own statistical purposes (i.e. I know about git rebase, but this non-logical history is something I want to keep anyway). Currently I'm just committing to a separate branch autocommit and use

git checkout master
git merge --squash autocommit
git commit
git checkout autocommit
git merge --ff-only master

for "proper" commits to keep the master branch tidy while maintaining a relationship to the autocommit-branch. So I have a history such as

| *   95e4189 Merge branch 'main' into autocommit
| |\
| |/
|/|
* | 040386a <= created via git merge --squash autocommit
| * 72bc5a5 autocommit
| * 9aaf5a6 autocommit
| * ea758c0 autocommit
| * 7ff1de8 autocommit

But what I really want is to git merge autocommit --edit into master to properly link to the history. However, I don't want to git push anything from the autocommit branch (or any commit with said message, if that is easier to manage). However I guess that will basically corrupt the pushed repo since parts of the commit history will be inaccessible. So my question is either:

  • How can I do this anyway? I.e. a kind of git push --skip autocommit
  • What should I do instead? The squash-commit version doesn't seem optimal

For visualization, I currently have:

   A1 -> A2 -> A3--=> A3' -> A4 -> ...    [autocommit]
  /            ↓' /
M1-----------> M2 -----> ...              [master]

Where ↓' denotes a git merge --squash which means A3 is not a parent of M2 but only M1 is, and A3' just merges back M2 to better keep track of the connection. What I think I want is just

M1--------------=> M2 ---> ....                             [master]
  \            /
  A1 -> A2 -> A3 -> A4 (or maybe A3' first merging M2 back) [autocommit]

but none of the commits A1 etc. from the autocommit branch ever getting pushed.

2 Answers

To understand what might be possible, it's useful to understand a bit about how git works. Fundamentally, git builds up its model of version control in layers:

  1. A content-addressed database, where arbitrary blobs of data can be looked up based on a hash of their content. These blobs are immutable, in the sense that changing any of the content would result in a new hash, so would never update the existing item.
  2. A directed acyclic graph, where blobs describe the state of the repository, some metadata (date, committer, message, etc) and zero or more parents. The parents are addressed by their immutable hash, and are in turn part of the content hashed for the new commit.
  3. Pointers to particular commits to give them more user-friendly names. We call some of these "branches", but they just point to one commit; from there, git can walk backwards along the parent references to find all commits "on" that branch.

From this, we can see a few things that aren't possible:

  • Having your local copy record two parents for a commit, but github showing only one parent for the same commit - a different set of parents would give a different hash, so nothing would recognise it as the same commit.
  • Having both parents recorded, but not pushing the commits on the autocommit branch - git doesn't know the names of branches in a merge it finds in history, it just sees all parent commits as part of the history it's tracing backwards.
  • Pushing all the commits accessible on one branch except for those also accessible on a different branch. This still isn't a strong enough definition, because eventually you'll reach commits in the history from before you created the autocommit branch, and again git won't know where in the history that happens.
  • Even if you could define the list of commits you don't want to push, tools are not going to be very happy finding a reference to a parent commit that apparently doesn't exist, and will probably assume you're repository is corrupt.

I can only really think of two options:

  • Record a normal merge from your autocommit branch, and live with the automated commits showing in your history.
  • Squash the commits as you're doing now, but create your own tools for recording which automated commits you squashed, and allowing you to look them up, make sure none get missed, etc

IMSoP's answer covers most of the possibilities, but there's one more possibility provided that your "detailed" and "non-detailed" branches always completely synchronize the source code at the "update non-detailed branch" point, and that is to do this:

o----------A----------B   <-- master
 \         ⋮\         ⋮\
  D1--D2--D3-M1--D4--D5-M2   <-- detailed

Here, you do your work on your detailed branch. When you decide to make a new master commit, you literally copy the files from a detailed-branch commit (such as D3) into a new commit on master, as represented by the vertical ellipsis . Then you use git merge to combine this new commit from master into detailed, as merge commit M1. Git now believes/knows that the merge base of master and detailed is commit A.

The point of this merge isn't really to perform a merge, or even to record a merge, but rather to record, in the commit graph, which commits from detailed were used to create commits in master. In other words, this is more for your reference than for Git's. But this also lets us use git merge --squash to create commits A, B, and so on. We can create commit A like this:

git checkout master
git merge --squash detailed

Since there are no changes from o to o (o is the merge base of this merge), Git "combines" these no-changes with the changes since o on detailed. The resulting non-merge (squash) commit is commit A. But immediately after making commit A, we now run:

git checkout detailed
git merge master

Git now compares commit o vs A to see what they changed, and o vs D3 to see what we changed. These changes exactly match (of course) so merge commit M1 has a snapshot that matches both commits D3 and A.

We now make more of the usual detailed-commits on detailed, then repeat the above process to create commits B and M2. The merge base this time is commit A. The end result is that we're in the same position as we were when we made commit A, but the next time we do all this, the merge base will be commit B.

Note that it's possible to create commits A and B without running git merge --squash, and it's possible to create merges M1 and M2 without running git merge: we know the desired tree for each commit so we can simply use git commit-tree to make these commits. But git commit-tree is not a user-facing command and is a bit hard to use. You could write your own Git command to do this, that makes sure it's all being done correctly—that there were no commits directly to master for instance—but if you do use git merge --squash, commits made directly to master actually Just Work. Here's what one of those might look like, expressed as a commit whose hash ID is C:

o----------A----------B-----C----D   <-- master
 \         ⋮\         ⋮\         ⋮\
  D1--D2--D3-M1--D4--D5-M2--D6--D7-M3   <-- detailed

Here, perhaps commit C was an urgent bug fix. Rather than making it in the detailed branch, we made it directly on a new branch, tested it, and merged it to master (so perhaps it's a little merge bubble rather than a direct commit—the end result is all the same no matter how it's built). The git merge --squash action combines the change in C with those from B to D7, and the merge down from D to M3 brings in the change from commit C so that M3 and D match, source-code-snapshot-wise.

Related