Version control with Git: remote repositories

Previously, I wrote about Git usage for single-user single-location projects. However, where Git really shines is in managing a project when changes are made on multiple machines (whether by one person or by multiple people).

Unlike centralized version control systems and file synchronization software, Git and other distributed version control systems actually have good support for disconnected operation:

  • You can perform your commits locally and without talking to a central server. You can push all your changes to another location whenever you get a chance. (CVS and SVN don't support making commits in isolation, so people who work offline end up submitting huge patches.)
  • When you (or you and other people) perform independent changes in parallel on different machines, Git knows how to gracefully merge those changes the next time you synchronize.

However, I've found Git to be a lot easier to get started with than other VCS's with the same features. All you need is a git init to start managing a simple project in Git: like RCS (but unlike CVS and SVN), there's no need to create a separate repository and make a checkout of it. And yet, when your project matures, Git will happily (and with just a couple of additional commands) move that project to multiple machines or share it over the internet so you can take advantage of its distributed features.

One of my projects is used solely to manage my dotfiles. (I'm constantly tweaking my .emacs, .bashrc, etc.) I use Git to keep my dotfiles synchronized on all the computers I use. I'll discuss the basics of distributed operation for a generic project before talking about some of the wrinkles associated with using Git to manage your dotfiles.

Suppose my Git repository is in ~/testproj on the machine bigphil. To start working on that project on another machine (the equivalent of "svn checkout"), do:

git clone ssh://bigphil/~/testproj

You can also clone a repository from elsewhere on a local disk, or over HTTP:

git clone ~/testproj
git clone http://example.com/git/testproj

You now have the complete version history of the project, and Git can work completely independently of the original repository, if you'd like. You can add files, make your own commits, etc. on your new repository locally.

However, typically you will want to continue incorporating commits that are made to the source repository. You can do this with:

git pull

(When you cloned the repository, Git remembers the original location; git pull will retrieve updates from the same location.) This is the equivalent of "svn update".

Merging

When you pull from a remote repository and there have been changes on both your local copy and the remote copy since you last synchronized, Git needs to merge those changes. Usually, it will do this automatically and make a new commit which incorporates the changes from both the local and the remote repositories.

After a merge has occurred, if you look at the project history with gitk --all, you'll see a place where two history lines diverged (representing development in the local and remote repositories) and then were merged back together. (However, the output of git log flattens these nonlinearities.)

If the local and remote changes made modifications to the same pieces of code, Git may have trouble performing a merge. In this case, it will do its best, but it will leave conflict markers in the code and not commit the final result. You should fix up the conflicts and then commit the merge with git commit -a.

Pushing, and bare repositories

To take your local modifications and push them back to the original repository from which you made your clone, do this:

git push

To push to a different repository:

git push path/to/other/repo

However, if you push your local modifications to a regular repository, a person who is using that repository to do work may get confused because the state of the repository is changing right under him. So typically it's better to push to a bare repository, which is a repository without a working copy (essentially, what's in the .git subdirectory of a regular repository).

To initialize a bare repository:

git --bare init

Then use git push PATH to push to it. Bare repositories look different at the file level, but cloning from and pushing to them is otherwise the same.

Managing dotfiles with Git

To manage my dotfiles, I've made my home directory the root of a Git repo. I only add the files I'm interested in managing (.emacs, .bashrc, etc.), and Git ignores the rest of them. I push changes from that repo into a bare repository on one of my machines, and pull from that repository to get the latest versions.

The only complication is when I wish to bring my dotfiles to a new computer. Git does not allow you to clone a repo into an existing directory (as I would wish to do to clone my dotfiles into my home directory). However, things will work if you clone to a new directory, and then copy the contents of that directory (the .git subdirectory, and all the files of interest) back to your home directory:

git clone ssh://bigphil/~/projects/dotfiles
mv dotfiles/.[a-zA-Z]* ~

(Note that dotfiles/* doesn't work because * doesn't usually select dotfiles.)

Version control with Git

I've recently started using Git for version control of all my personal projects. It works so smoothly that I don't have any reservations about using version control. That means I commit small changes very often; as a result, I'm never afraid of leaving my projects in a wedged state, even if I'm making big changes.

This post only discusses Git basic usage. I'll write about its distributed features in future posts.

To start managing a directory with Git:

  1. Do the following to initialize the directory:

    git init

  2. If you have existing files you want to start managing, do

    git add .

    to add all the files in the directory, or

    git add FILE ...

    to add only particular files.

  3. Then do

    git commit

    Git prompts you for a log message and records your first commit. It will print a message of the following form:

    Created commit 1e169f6: Add new file.
     1 files changed, 15 insertions(+), 0 deletions(-)

    Congratulations!

Like SVN, Git store per-tree versions rather than per-file versions. However, instead of assigning version numbers like SVN does, Git assigns a unique hexadecimal identifier for each commit. Although these identifiers are long (40 characters), when you wish to refer to a particular commit, you only need to type as many characters are needed to make a unique prefix.

My typical workflow looks like this:

  1. Do some editing:

    emacs FILE ...

  2. Add files to Git's staging area:

    git add FILE ...

    Do this whether the files are new files you've added, or existing files you've modified.

  3. Commit the files in the staging area:

    git commit

    Again, Git prompts you for a log message and records a commit.

As a shortcut, git commit -a is equivalent to running git add with any modified files before running git commit. (It does not, however, pick up newly created files.)

The following commands are used to explore the project's history and current state:

  • git log shows recent commits.
  • git status shows which files are in the staging area, which files have been modified, and which newly created files are not managed by git.
  • git diff shows changes in your working copy.
  • git diff --cached shows changes in the staging area, that is, what will be committed when you do git commit.
  • git diff cd12..ab34 diffs the revisions cd12 and ab34.
  • git reset --hard restores your working copy state to that of the last commit.

The CVS version of GNU Emacs supports Git in VC, so it knows when your file is being managed by Git. When it is, you can use the following VC commands:

  • C-x v v commits the current file.
  • C-x v = shows a diff between your working copy and the last committed version (or between any two committed versions).
  • C-x v g displays an annotated version of the file showing, for each line, when that line was last modified, and a heat-map displaying older and newer code in different colors.

To install git on Ubuntu: type sudo apt-get install git-core.

Further reading: Everyday GIT with 20 commands or so, A tour of git.