Reputation:
I want to sync some configuration files between two computers, computer A and computer B. computer A has an ssh login on computer B, but not the other way around.
When I have ssh connections both ways I usually do this: run fetch, and then merge remote/upstream/master, from either repo. That way I can let things diverge and only have to merge when I have the time.
When I only have a one way connection I can get changes from B to A as usual by running in A: git fetch B, and then git merge remotes/B/master. To get changes from A to B I try to run from A: git push B. But I get error message error: refusing to update checked out branch: refs/heads/master
. This is because I'm trying to push to a checked out branch and thus update HEAD (as I understand it). But I don't want to update HEAD, I only want to changes from A available in B as remotes/A/master. So I would like a push option where push from A to B does not try to update B's head or master. Which is the same thing as fetch but in reverse.
Another way of putting it: pull is fetch + merge, and you can split them up. But push is what plus merge?
Upvotes: 0
Views: 1035
Reputation: 490078
(Remember that there are two Gits involved here. You're calling one A. From machine A you can ssh into machine B, so on machine A, in the repository there, you use the remote name B
to talk about "the other Git". Meanwhile on machine B you use remote name A
to talk about "the other Git".)
Use git push B master:refs/remotes/A/master
to create or update an actual remote-tracking name on A
. Use git push B +master:refs/remotes/A/master
to force-overwrite the remote-tracking name on A
(or the same with --force
and without the plus sign).
You can use git push B master:A/master
to make or update a regular (local branch, not remote-tracking name) A/master
on A
. As before you can add --force
or a plus sign to this refspec to overwrite.
If you use remotes/A/master
, without the refs/
part, this creates a local branch named remotes/A/master
, which I think is a bad idea. On machine A
, run git branch
and git branch -r
to see the distinction between branch names and remote-tracking names; or use git for-each-ref
to dump them all out in their full-name forms.
First, let's note that git push
does not merge. Neither does git fetch
, for that matter. The refusing to update message has nothing to do with merges! Your diagnosis here is at least partly correct (depending on what you mean by diverge—you did not say from what ):
... fails because it would make worktree and index diverge
In fact, the index and work-tree would remain unchanged, and that's the problem.
All you need to do here is git push
to some name that is not the currently-checked-out branch name.
When you run git push
, you direct your Git to call up another Git, usually via https (port 443) or ssh (port 22) to some IP address. That other Git is a repository, with its own branches. If it's a --bare
repository, it has no work-tree and no one can be doing any work in it, so it will generally accept pushes. If not, it has a work-tree, indexed by its index, and someone could be doing work in it.
Consider what a commit is. Each commit has a unique hash ID, which is in effect that commit's real, true name. Each commit stores a snapshot of all files, and each commit stores some metadata about that snapshot: who made it (name and email address), when (time-stamp), and why (log message). It also stores the hash ID—the true name—of its immediate parent commit, or, for merge commits, all of its parent commits.
Branch names, in a Git repository, serve to locate the last commit in a chain of commits:
... <-F <-G <-H <--branch
The branch name stores the hash ID of commit H
. H
itself stores the hash ID of its predecessor G
, which stores another ID F
, and so on back through history.
When you run git push
, your Git calls up some other Git. Your Git then offers their Git some commit hash ID(s). They either do have this commit—hash IDs are universal across all Gits, via the magic of cryptographic checksums—or they don't. If they don't have the commit, they need it (and all of its files) and maybe its parent commit too (and all of that commit's files) and maybe the parent's parent, and so on. Essentially, they need any commits that you have that they don't. So your Git offers them commits until their Git says: "OK, I have that one and all the earlier ones too, you can stop now."
Then your Git sends those commits (and their files). That's where you see the counting and compressing and writing objects messages. Eventually your Git has sent them everything they need. If they already had the commits, this phase sends no data at all, but no matter what, in the end, they have the commits.
Now your Git does the last part of git push
: it asks, politely (push without --force
), or commands (git push --force
), that the other Git set some of its branch name(s) to remember the commit(s) you just sent them, or didn't send them if they already had them.
Let's say you send them new commit I
whose parent is H
:
...--F--G--H <-- branch
\
I
They now have I
, but now you ask them: Set your name branch
to remember commit I
instead of H
, OK?
That's not OK, because someone has branch
checked out! For all their Git knows, that someone is busy editing files and fiddling with the index and/or work-tree and plans to make their own commit J
in a nanosecond or two:
...--F--G--H--J <-- branch
\
I
which would leave I
all weirded out, or:
...--F--G--H--I--J <-- branch
which, because git push
doesn't do any merging, would make commit J
effectively undo commit I
.
So their Git (which is really yours, you're just standing on the wrong side of the Internet while you do this) says No, you can't make my name branch
remember commit J
, it's busy remembering commit H
while my user works!
But you can ask them to set, say, merge-me-into-branch
, a name that doesn't exist right now. For their Git, that's a new name:
...--F--G--H <-- branch
\
I <-- merge-me-into-branch
So all you need to do is run:
git push <remote-or-URL> branch:merge-me-into-branch
to create new name merge-me-into-branch
in "their" Git to remember new commit I
.
Upvotes: 2
Reputation: 165586
tl;dr You need a 3rd bare repository for repoA and repoB (and repoC and repoD...) to coordinate with. Any Git service will provide you with one. Or you can use computer B since you can login to it. Or you can stick the common repository on Dropbox, though that's not as simple as it sounds.
You're swimming upstream against two problems. First, as you've discovered, you can't push to a branch that's checked out. If you could it would lead to much confusion. A checked out branch implies somebody might be actively working on it or using it. While someone is working on the checkout HEAD
would move out from beneath them and their working copy would be out of sync.
Try to bring them into sync and they'll get another big surprise as suddenly their working copy changes. For example, let's say you left some edits on B and then tried to push from A to B. What should Git do?
pull is fetch + merge, and you can split them up. But push is what plus merge?
push
does not merge, it will only fast forward. push
is basically a fetch
in reverse, it sends the missing objects. But then it will only fast forward.
Let's say you did some work on repoA and pushed.
repoA A - B - C - D - E [master]
repoB A - B - C [master]
repoA will send D and E. And then repoB will slide master
forward to E. No merge required.
What if you did work on both repoA and repoB?
repoA A - B - C - D - E [master]
repoB A - B - C - F - G [master]
When repoA tries to push Git will notice that the branches have "diverged". Their common ancestor, C, is not at the tip of repoB/master. This requires a merge. Merges often require human intervention, but there's no human on the repoB side. Git doesn't allow it because it makes things even more confusing.
Instead, rebase your changes on top of B. Then push. This makes you do the work of stitching the two branches together, and because we've rebased instead of merged it will be a simple fast-forward.
Again, starting with our divergent repositories.
repoA A - B - C - D - E [master]
repoB A - B - C - F - G [master]
If repoA
runs git pull --rebase
(I recommend setting pull.rebse = preserve
so git pull
rebases and preserved branches by default, it results in a much cleaner history than regular merge pulls) then it will fetch F and G.
F - G [repoB/master]
/
repoA A - B - C - D - E [master]
repoB A - B - C - F - G [master]
And then pretend D and E were written on top of G all along.
D1 - E1 [master]
/
F - G [repoB/master]
/
repoA A - B - C
repoB A - B - C - F - G [master]
Now when repoA
(force) pushes to repoB
it's a simple fast forward.
That's the workflow I recommend for any project. However, this doesn't solve the problem of repoB
being checked out. To solve this you need a 3rd bare repository that they both push and pull to.
D - E [master]
/
repoA A - B - C [common/master]
repoB A - B - C [common/master]
\
F - G [master]
common A - B - C [master]
repoA
and repoB
both use common
as their remote. Both repoA
and repoB
push and pull their changes to and from the bare repo common
. common
is not checked out. It provides a static location for repoA
and repoB
to share their work.
For example, repoA can push to common.
[common/master]
repoA A - B - C - D - E [master]
repoB A - B - C [common/master]
\
F - G [master]
common A - B - C - D - E [master]
Then repoB can git pull --rebase
to get an update from common
and replay its own changes on top of that.
[common/master]
repoA A - B - C - D - E [master]
[common/master]
repoB A - B - C - D - E - F1 - G1 [master]
common A - B - C - D - E - F1 - G1 [master]
repoB
can then (force) push its changes to common.
And then next time repoB
pulls from common
it will get the updates and everybody is in sync.
[common/master]
repoA A - B - C - D - E - F1 - G1 [master]
[common/master]
repoB A - B - C - D - E - F1 - G1 [master]
common A - B - C - D - E - F1 - G1 [master]
common
can be on any number of services which will provide you with a Git repository. Or it could be on computer B since you can log into it.
Upvotes: 1