The Setup
I have a lot of computers. Sometimes I'll start a project on one computer and
get interrupted, then later find myself wanting to work on that same project,
right where I left off, on another computer. Sometimes I'm not ready to
publish my work, and I might still want to rebase it a few times (because if
you're not arbitrarily rewriting history all the time you're not really using
Git) so I don't want to push to @{upstream}
(which is how you say "upstream"
in Git).
I would like to be able to use Git to synchronize this work in progress between multiple computers. I would like to be able to easily automate this synchronization so that I don’t have to remember any special steps to sync up; one of the most focused times that I have available to get coding and writing work done is when I’m disconnected from the Internet, on a cross-country train or a plane trip, or sitting near a beach, for 5-10 hours. It is very frustrating to realize only once I’m settled in and unable to fetch the code, that I don’t actually have it on my laptop because I was last doing something on my desktop. I would particularly like to be able to that offline time to resolve tricky merge conflicts; if there are two versions of a feature, I want to have them both available locally while I'm disconnected.
Completely Fake, Made-Up History That Is A Lie
As everyone knows, Git is a centralized version control system created by the popular website GitHub as a command-line client for its "forking" HTML API. Alternate central Git authorities have been created by other startups following in the wave following GitHub's success, such as BitBucket and GitLab.
It may surprise many younger developers to know that when GitHub first created Git, it was originally intended to be a distributed version control system, where it was possible to share code with no particular central authority!
Although the feature has been carefully hidden from the casual user, with a bit of trickery you can enable re-enable it!
Technique 0: Understanding What's Going On
It's a bit confusing to have to actually set up multiple computers to test these things, so one useful thing to understand is that, to Git, the place you can fetch revisions from and push revisions to is a repository. Normally these are identified by URLs which identify hosts on the Internet, but you can also just indicate a path name on your computer. So for example, we can simulate a "network" of three computers with three clones of a repository like this:
1 2 3 4 5 6 7 8 |
|
This creates three separate repositories. But since they're not clones of each other, none of them have any remotes, and none of them can push or pull from each other. So how do we teach them about each other?
1 2 3 4 5 6 7 8 9 |
|
Now, you can go into a
and type git fetch --all
and it will fetch from b
and c
, and similarly for git fetch --all
in b
and c
.
To turn this into a practical multiple-machine scenario, rather than specifying
a path like ../b
, you would specify an SSH URL as your remote URL, and turn
SSH on on each of your machines ("Remote Login" in the Sharing preference pane
on the mac, if you aren't familiar with doing that on a mac).
So, for example, if you have a home desktop tweedledee
and a work laptop
tweedledum
, you can do something like this:
1 2 3 4 5 6 7 8 |
|
I don't know the names of the hosts on your network. So, in order to make it possible for you to follow along exactly, I'll use the repositories that I set up above, with path-based remotes, in the following examples.
Technique 1 (Simple): Always Only Fetch, Then Merge
Git repositories are pretty boring without any commits, so let's create a commit:
1 2 3 4 5 6 7 |
|
Now on our "computers" b
and c
, we can easily retrieve this commit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
If we make a change on b
, we can easily pull it into a
as well.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
This technique is quite workable, except for one minor problem.
The Minor Problem
Let's say you're sitting on your laptop and your battery is about to die. You want to push some changes from your laptop to your desktop. Your SSH key, however, is plugged in to your laptop. So you just figure you'll push from your laptop to your desktop. Unfortunately, if you try, you'll see something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
While you're reading this, you fail your will save and become too bored to look at a computer any more.
Too late, your battery is dead! Hopefully you didn't lose any work.
In other words: sometimes it's nice to be able to push
changes as well.
Technique 1.1: The Manual Workaround
The problem that you're facing here is that b
has its master
branch
checked out, and is therefore rejecting changes to that branch. Your
commits have actually all been "uploaded" to b
, and are present in that
repository, but there is no branch pointing to them. Doing either of those
configuration things that Git warns you about in order to force it to allow it
is a bad idea, though; if your working tree and your index and your your
commits don't agree with each other, you're just asking for trouble.
Git is confusing enough as it is.
In order work around this, you can just push your changes in master
on a
to
a diferent branch on b
, and then merge it later, like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
This works just fine, you just always have to manually remember which branches you want to push, and where, and where you're pushing from.
Technique 2: Push To Reverse Pull To Remote Remotes
The astute reader will have noticed at this point that git already has a way
of tracking "other places that changes came from", they're called remotes! And
in fact b
already has a remote called a
pointing at ../a
. Once you're
sitting in front of your b
computer again, wouldn't you rather just have
those changes already in the a
remote, instead of in some branch you have to
specifically look for?
What if you could just push your branches from a
into that remote? Well,
friend, I'm here today to tell you you can.
First, head back over to a
...
1 |
|
And now, all you need is this entirely straightforward and obvious command:
1 |
|
and now, when you git push b
from a
, you will push those branches into b's
"a" remote, as if you had done git fetch a
while in b
.
1 2 3 4 |
|
So, if we make more changes:
1 2 3 4 |
|
we can push them to b
...
1 2 3 4 5 6 |
|
and when we return to b
...
1 |
|
there's nothing to fetch, it's all been pre-fetched already, so
1 |
|
produces no output.
But there is some stuff to merge, so if we took b
on a plane with us:
1 2 3 4 5 |
|
we can merge those changes in whenever we please!
More importantly, unlike the manual-syncing solution, this allows us to push
multiple branches on a
to b
without worrying about conflicts, since the a
remote on b
will always only be updated to reflect the present state of a
and should therefore never have conflicts (and if it does, it's because you
rewrote history and you should be able to force push with no particular
repercussions).
Generalizing
Here's a shell function which takes 2 parameters, "here" and "there". "here" is the name of the current repository - meaning, the name of the remote in the other repository that refers to this one - and "there" is the name of the remote which refers to another repository.
1 2 3 4 5 6 |
|
In the above example, we could have used this shell function like so:
1 2 |
|
I now use this all the time when I check out a repository on multiple machines for the first time; I can then always easily push my code to whatever machine I’m going to be using next.
I really hope at least one person out there has equally bizarre usage patterns of version control systems and finds this post useful. Let me know!
Acknowledgements
Thanks very much to Tom Prince for the information that lead to this post being worth sharing, and Jenn Schiffer for teaching me that it is OK to write jokes sometimes, except about javascript which is very serious and should not be made fun of ever.