Suppose I've multiple remotes for a single repository. Most of the time I use one git account for the development purpose, and when I'm finished, I push the final version to another remote. Now, how can I hide my commit history of the first remote from the second one?
1 Answers
I'll show you how to do what you asked for, then tell you why this is a bad idea. :-)
History, in any Git repository, is simply the set of commits in that repository, as found by the set of names in that repository. This finding process works backwards, because Git always works backwards. We'll see more on this in a moment.
Background
Remember that each commit has a unique hash ID. This is, in effect, the true name of a commit. To view a commit with git log
, you must somehow clue Git in to the commit's hash ID. Git can then retrieve that commit from the repository database, provided it's in the database in the first place.
Every commit has a full snapshot of all of your files—that's the main data in the commit—plus some metadata: information about the commit itself, such as who made it, when (date-and-time-stamp), and why (the log message). Most of this metadata is just stuff Git is to show you with git log
. But one crucial piece of information in the metadata, needed by Git itself, is in here too. Every commit has a list of the raw hash IDs of its parent commits. Most commits have just one entry in this list, as they have a single parent.
This hash ID means that if we somehow find some starting commit hash H
, we can get the commit itself and show it, and use it to find its parent (earlier) commit. Let's call that commit G
. We say that commit H
points to commit G
:
... G <-H
But G also points to an earlier commit—let's call it F
—like this:
... F <-G <-H
and of course F
points backwards too:
... <-F <-G <-H
So all we really need is a way to tell Git: the last commit's hash ID is _____ (fill in the blank with a hash ID).
This is what a branch name is and does: it provides the last commit's hash ID. The branch name points to a commit, just as each commit points to an earlier commit. This way, we don't have to remember big ugly hash IDs that humans can't deal with. We only have to remember branch names. The names remember the big ugly hash IDs:
... <-F <-G <-H <-- master
... when I'm finished [making new commits] ...
Let's look at the process of making a new commit. Let's make a new branch name, feature
for instance, right now. The branch name has to point to some existing commit—that's the rules in Git: a branch name points to some commit. Of the ...--F--G--H
series, the obvious one to use is ... the last one:
...--F--G--H <-- feature (HEAD), master
We need a way to remember which branch name we're using, so I've attached the special name HEAD
to the new name feature
. This is what we get if we do:
git checkout -b feature master
We're still working with commit H
, but now we're on branch feature
, as git status
will say. The special name HEAD
is attached to feature
now, instead of master
.
When we make a new commit, it gets a new, never-used-where-else-before, never-to-be-used-anywhere-else-ever-again commit hash I
. New commit I
points back to existing commit H
:
...--F--G--H <-- master
\
I <-- feature (HEAD)
Repeat a few times and you have this:
...--F--G--H <-- master
\
I--J--K <-- feature (HEAD)
Eventually, you're done making commits. You can now git push
to some remote, such as origin
. The way this works is that your Git calls up another Git—the one at the URL stored under the remote name, origin
—and offers them some commit(s) by hash ID.
They look in their repository to see if they have that hash ID. If you offer them commit K
, they won't have it. That forces your Git to offer them commit J
too, because J
is K
's parent, and that, too, is part of the rules of Git. They won't have that, so your Git will offer I
, and they won't have that so your Git will offer H
. Here, they might well have H
! Let's say they do. This lets your Git stop offering hash IDs.
Now your Git has to package up the new-to-them commits, I-J-K
, and send them. You'll see messages about counting and compressing here, and then your Git sends the commits across.
The git push
now enters its final phase: it sends them a polite request: If it's OK, please set your branch name ______ to point to commit K
. As long as this adds commits to one of their branches, without removing any commits from that branch, they are likely to obey this request. If it's a totally new branch name, they are even more likely to obey this request.
The end result is that now they have their branch name pointing to the last commit K
in the chain of commits. From K
, they'll find J
, then I
, and then H
, and so on. This is the history they have in their repository now.
What you want
... how can I hide my commit history of the first remote from the second one?
You can't, or, not exactly. You can, however, make new commit(s) that are a different history, and send those instead.
Suppose you, in your own repository, make a new and different branch name, using:
git checkout -b other-guy master
This gives you, in your Git repository, this series of names and commits:
...--F--G--H <-- master, other-guy (HEAD)
\
I--J--K <-- feature
Your current commit is now commit H
. Your current branch name is now other-guy
.
You can now make a new commit—with a totally new, never-seen-before hash ID, which we'll call L
—with whatever snapshot in it you like. Let's not worry about how you do that yet and just draw the result:
L <-- other-guy (HEAD)
/
...--F--G--H <-- master
\
I--J--K <-- feature
You can now use:
git push other-remote other-guy:feature
This has your Git call up the Git stored under the remote name other-remote
and offer them commit L
. They won't have it, so your Git will also offer commit H
. They might have that one—it's been going around for a while—so your Git can probably stop there, bundle up L
, and send it over.
Now your Git sends their Git a polite request of the form: If it's OK, please set or create your name feature
pointing to commit L
. If they accept, what they have in their repository is:
...--H--L <-- feature
(they probably have some other name, such as their master
, pointing to H
, we just didn't draw it here). So their commits in their repository are found by starting from their name feature
, which identifies commit L
. They'll show commit L
. Then they'll move back to L
's parent H
, and show H
, and so on.
Note how they never show I-J-K
. They can't, because they don't have them. They can, now or in the future, if they want and have access, get them from you and/or from any other Git you sent them to, or any Git that has Git-sex with the Git you sent them to and thereby picks them up, and so on; but right now, they aren't infected with commits I-J-K
.
(Gits in general really like to pick up new commits. Gits in general do not like to give up commits. It's very easy to spread commits around like infections.)
The easy way to make commit L
I promised to show you how to do what you want. There's an easy way to make commit L
after making I-J-K
, and that's to use git merge --squash
.
Given this:
...--F--G--H <-- master, other-guy (HEAD)
\
I--J--K <-- feature
you can run git merge --squash feature
and then git commit
. The git merge --squash
tells Git: Do everything you would for a real merge, but then stop without committing. When I make the commit, make it as a regular everyday single-parent commit, not a merge commit with its two parents.
Git now combines the difference from commit H
to commit H
—no change at all—with the difference from H
to K
, and applies all those changes to the snapshot in H
, resulting in the snapshot in K
. This snapshot isn't committed yet, but you run git commit
, fill in the commit message however you like, and now it is:
L <-- other-guy (HEAD)
/
...--F--G--H <-- master
\
I--J--K <-- feature
and you're ready to git push
commit L
and have someone else call it feature
.
Why you probably shouldn't do this
As soon as you've done this once, your next starting position in your own repository is this:
L <-- other-guy (HEAD)
/
...--F--G--H <-- master
\
I--J--K <-- feature
One of your two remotes has this same setup except that it lacks commit L
entirely. If you want to send commit L
there, you'll need to use some name other than feature
this time: their name feature
remembers commit K
. You can tell them to forcibly drop I-J-K
in favor of L
, but if you do that, you've already given up on doing what you asked for: now both other remotes only can find commit L
(at least, via their name feature
).
If you want to develop more stuff, you now have a problem: do you start from commit K
, or do you start from commit L
? If you start from L
, your own history for the new work you do, does not have the I-J-K
history. History, after all, is the set of commits as found by some name and working backwards.
So what you end up doing is one of two things:
- make a lot of histories you ignore (your own
feature
branches, that you abandon by starting the next one from commitL
in this case), or - start having to do
git merge --squash
when you did agit merge --squash
.
Let's see how the latter works:
git checkout feature
now results in:
L <-- other-guy
/
...--F--G--H <-- master
\
I--J--K <-- feature (HEAD)
We make more commits:
L <-- other-guy
/
...--F--G--H <-- master
\
I--J--K--M--N <-- feature (HEAD)
Now we go to squash-merge:
git checkout other-guy
git merge --squash feature
This merges work—as in, compares H
vs L
to find "our" changes, and H
vs N
to find "their" changes, and combines the changes. This often works OK ... but if anything we did in M-N
un-does something in I-J-K
, or touches the same lines as something we did in I-J-K
, we get a merge conflict.
There are ways to deal with this, and in the end we get:
L--O <-- other-guy
/
...--F--G--H <-- master
\
I--J--K--M--N <-- feature (HEAD)
where O
has the squashed result of combining M
and N
. You can now git push
the two different histories to two different places.
This really can work. It just gets to be painful over time. There's also that other "infection-like" problem: you've sent commits I-J-K-M-N
to some other Git. Chances are pretty good that those commits will get spread into more Git clones, and from there, will get to the clone you were trying to keep these secret from. Even if this doesn't happen, it's really easy to goof up on your own end, by doing git push other-guy feature
(though fortunately after the first round, this will normally get rejected as a "not a fast-forward" error).
In short, secrets—the hidden commits—generally just don't last once shared. There's usually a much simpler alternative. I don't know your motive for wanting all of this, though, so it's hard to say for sure.

- 448,244
- 59
- 642
- 775
-
Thanks a lot for your detailed explanation :) – md mamun May 07 '20 at 21:45
-
but then why if I `git merge --squash otherguy/branch` and then `commit -m my commit` I get the `otherguy` remote name next to the commit hash like so `commit fa5d2c467a6a08de77bd608f69f4a1c2d1e0586d6 (origin/develop, coordination/develop, develop)` – dnuske Sep 22 '22 at 01:03
-
@dnuske: `git merge --squash` makes an ordinary commit (i.e., a commit with one parent), with author and committer set as usual for any ordinary commit (i.e., based on your user.name and user.email unless overridden), on the current branch. If the current branch is `develop`, the new commit should at this point *only* be on `develop`. Once you `git push origin develop`, however, and they accept it, `origin/develop` in your repository will update, so you'll see that—or if you `git push coordination develop` you'll see *that*. If `origin` then reset their `develop`, [continued] – torek Sep 22 '22 at 04:15
-
If `origin` (or respectively `coordination`) reset *their* `develop` branch and you `git fetch` from that remote, that will update your local `origin/develop` (or `coordination/develop`) name, so that `git log` will now show all three names as identfying this new commit. Remember that `git log` simply shows the state of *your* repository; it's `git fetch` and `git push` that *update the state of your repository* according to what your Git sees happening in some other repository *at that time*. – torek Sep 22 '22 at 04:17
-
If you're seeing `origin/develop` and `coordination/develop` update *immediately* (without running `git push` or `git fetch`), then something else—it's not clear what—is running `git push` and/or `git fetch` and/or lying to your Git software. If you're using a GUI, perhaps the GUI is auto-pushing-and-fetching as a fancy instant synchronization feature. – torek Sep 22 '22 at 04:20