I have two repos - repo 1 and repo 2, and repo 1 has two branches - a master branch and branch 1. I want to be able to mirror repo 2's master branch to branch 1 only, and leave repo 1's master branch as is. Is there a way to do that? Is there also a way to mirror a specific branch to the master of another repo (i.e. mirror repo 1's branch 1 to repo 2)?
I've tried following your typical git clone --mirror URL , and then git push --mirror URL to repo 1. However, this seems to only push the mirror to the master branch of repo 1. I've also seen some people reference possibly doing a git push branch1 --mirror URL, but for this, I get a fatal: --mirror can't be combined with refspecs .
In some of the other questions similar to this, the solution hasn't necessarily been to mirror to and mirror from a specific branch. Unfortunately I'm facing this particular use case, and can't mirror to and from the master, but need to do it from a particular branch.
You can do anything you want here. The real trick is to figure out what you want, first. After that, it's just a matter of refspecs, which I'll get to in a moment. Given what you've said above, though, you don't want any of the --mirror
options.
First, remember that Git is really all about commits. Commits are uniquely identified by their hash IDs. These hash IDs are universal: every Git everywhere agrees that some particular commit has that one hash ID. No other Git anywhere can ever use that same hash ID for a different commit, and every Git everywhere must, if it has that commit, use that hash ID.
In other words, hash IDs have a real, concrete meaning. It's the hash IDs that matter. They are the currency of Git-to-Git exchange.
Branch names, by contrast, are specific to one Git repository or another. Your master
can be completely unrelated to my master
, even if our two Git repositories are to exchange commits. I have my Git call up your Git; your Git tells me that your master
is commit a123456...
; I get that commit from you, and it's still a123456...
, but the name I have my Git use is something else entirely, such as origin/master
or dinh/master
or yourbranch
or whatever I want to call it. If you say a123456...
and I say a123456...
we can tell we both have that commit. Your name, if any, and my name, need not agree, and with git fetch
, they typically don't agree.
The git push
command isn't quite symmetrical, but it works pretty similarly: I have my Git call up your Git, offer you some commit(s) by raw hash ID (and your Git and mine agree on those as usual). Then I ask (regular push) or command (force-push) your Git to set one or more of your names to identify some specific commits, which I name by their universal, agreed-on hash IDs.
My Git can see your names when I run git fetch
to your repository. The default for git fetch
is that I have my Git copy your names, renaming them: that's why you'll typically have an origin/master
in your Git, after you have your Git copy things from another Git you're calling origin
. Meanwhile, I have my Git tell your Git some name to use when I run git push
from my repository. For convenience, I'll usually tell your Git my branch name when I git push
. That's why you typically push your master
to origin's master
: there's no default renaming happening here.
If hash IDs are how Git finds commits—and they are—then why, exactly, do we have branch names at all? Well, consider some actual hash IDs in an actual Git repository:
83232e38648b51abbcbdb56c94632b6906cc85a6
aa8c8d914e4ae709e4fd025f359594f62653d9e5
061ed420ec2dc97e2a922a6f02992869089cefb3
These are three commits, in the order they were made (newest first). Can you remember any of these numbers? (Maybe—but I wouldn't want to.) Git needs to remember all of them, but Git is set up so that commit 83232e...
itself remembers the number aa8c8d...
, and aa8c8d...
remembers the number 061ed4...
. So we only need to remember that big ugly 83232e...
one. We could write it down; but it might be nicer if we had Git write it down for us.
We can do this with a branch name, a tag name, or any other such name. The name will hold 83232e...
and now we only need to remember the name. These names are, collectively, what Git calls refs or references. Branch names are names that start with refs/heads/
, tag names start with refs/tags/
, and remote-tracking names start with refs/remotes/
and continue with the name of the remote (e.g., refs/remotes/origin/
). After the last slash, you have the branch, tag, or remote-tracking name itself. Git tends to hide the prefix, but it shows up now and then, especially when using full references.
So, we have references—branch names, tag names, remote-tracking names, and so on—that remember one hash ID. From that one hash ID, Git finds the remaining hash IDs. The only thing special about branch names is that we can use git checkout
to get "on" the branch—git status
will say on branch master
—and then, once we do that, we can make a new commit. The new commit we make will remember 83232e...
for us, and Git will automatically stuff the new commit's hash ID—which will be new and unique, different from every other commit ever—into master
, which now automatically remembers the latest commit.
Hence, branch names just automatically designate the last commit in the branch. From that commit, which Git finds by its hash ID—as found under the name—Git finds the previous hash ID, which gets Git to the commit, which gets another previous hash ID, and so on. The result is the history: the series of commits that end at the designated tip, whose hash ID is stored in the branch name.
When git fetch
and git push
exchange commits, they do so by hash ID, but they also see, and sometimes copy, the names. This is where refspecs come in.
A refspec is essentially a pair of references separated by a colon :
character, e.g.:
refs/heads/master:refs/remotes/origin/master
You can put any reference on either side. Here we have master
on the left—refs/heads/
marking it as a branch name—and origin/master
on the right as a remote-tracking name. The name on the left is the source and the name on the right is the destination. This is the kind of refspec that git fetch
uses, because the source reference is a branch name and the destination one is a remote-tracking name. This tells your Git: Using their master
branch, get any commits they have that I don't, and remember the one their master
denotes, using my origin/master
.
With git push
, you might write git push origin master:master
. Your Git will translate this to refs/heads/master:refs/heads/master
—expanding the full name in place—and thus take any commits you have that they don't, send them over, and then ask them to set their master
branch to the last commit in that chain. Or you could git push origin master:bren
, telling your Git to take your master
—the source on the left side—and send any commits you have that they don't, but then ask them to set their branch bren
.
You can—and in scripts at least, probably should—spell out the full names, complete with refs/heads/
at the front. This makes sure that if for some reason someone accidentally creates a tag named master
, there's no ambiguity: you mean the branch refs/heads/master
, not the tag refs/tags/master
.
Any refspec can be preceded with a single plus-sign +
character. Doing so tells Git: Do this operation even if you would normally object to it. That is, it sets the --force
flag for this one particular reference update. Git would normally object if changing a branch name would lose some commits, for instance. That is, if the branch name currently says "commit a123456" which says "from here, go back one step to fedcba9", and you ask them to set their name to "fedcba9", they won't be able to find "a123456" any more—the directional links in commits point only backwards, to older commits, never forwards to newer ones. (For more about this, see Think Like (a) Git.)
--mirror
optionsYou can, and git fetch
normally does, use the special *
metacharacter to match all branches:
+refs/heads/*:refs/remotes/origin/*
This fetch-oriented refspec says: Take all of their branch names—everything under refs/heads/
—and get all the commits and such and then forcibly update all of my remote-tracking names in refs/remotes/origin/
to match. That's the normal refspec that git fetch
uses to talk to a remote named origin
. That way, you get all their branches, as your remote-tracking names.
You can, however, use git clone --mirror
to change this refspec to:
+refs/*:refs/*
This one says Take all their references, regardless of what kind of names they are, and overwrite all my references with whatever hash IDs are in each of theirs. This means every git fetch
replaces all your branch and tag names. Any commits you have that they don't, you have now lost. Any commits they have that you didn't, you now have—and your clone is now a mirror of theirs.
The git push --mirror
command means that you'll have your Git command theirs (as with --force
or a plus sign prefix) to:
This completely wipes out all of their branch, tag, and other names, replacing them with yours. (Of course, you'll first send any of your commits and other objects they'll need to complete this action.) That does depend on them obeying the forceful operations, but that is the usual default setup.
The main thing to remember here is that in Git documentation, the word mirror means take everything from the other: one Git repository is never authoritative for anything, and the other is always authoritative for everything. That's clearly not what you want! You only want one repository to be authoritative for one branch.
You can do this in either direction: you can git fetch repoA +refs/heads/theirs:refs/heads/mine
to replace your branch mine
with whatever's the latest on theirs
, and you can git push repoB +refs/heads/mine:refs/heads/theirs
to replace their branch theirs
with whatever's the latest on mine
. Except for where you run the command, and the direction of the data flow, these are almost symmetric: the only real difference is that with git push
, they can refuse (through a pre-receive, update, or post-receive hook).