Pages

Friday 20 August 2010

The power of git - splitting one file into multiple commits


Another really handy thing with Git, which I do use regularly, is it's ability to split lots of changes to the same file into separate logical commits.

As far as I know, any of the other VCSs I've used in the past haven't supported this and the only sane way I know of doing so would be to copy the file away, and use vimdiff to copy the logical commits one-by-one, committing each in turn.

Thankfully, git makes this really easy with the git add command.

Set up an example repository
# Create a new git repository for this example
cd /tmp/
mkdir git-add-pi
cd git-add-pi/
git init

# Make our first commit
echo "Example line" > example-file
git add example-file
git commit -m "First part of the example file"

Make some changes to our example file
Make a change to the beginning of the file:
(echo "Some big sweeping change to the file"; cat example-file) > tmp; mv tmp example-file
and make a change to the end of the file:
echo "Another big sweeping change to the same file" >> example-file

The magic
We actually wanted to split that file into two logical commits. With subversion, that would be a pain, but with git it's really easy with git add --pi:

523 git-add-pi:master> git add -pi example-file
diff --git a/example-file b/example-file
index d503a0c..82d4c17 100644
--- a/example-file
+++ b/example-file
@@ -1 +1,3 @@
+Some big sweeping change to the file
 Example line
+Another big sweeping change to the same file
Stage this hunk [y/n/a/d/s/?]?

Because the lines are so close to one another, git hasn't automatically split them up. Specify s to split them:
Stage this hunk [y/n/a/d/s/?]? s
Split into 2 hunks.
@@ -1 +1,2 @@
+Some big sweeping change to the file
 Example line
Stage this hunk [y/n/a/d/j/J/?]?
Well, we want to commit this hunk, so hit y
Stage this hunk [y/n/a/d/j/J/?]? y
@@ -1 +2,2 @@
 Example line
+Another big sweeping change to the same file
Stage this hunk [y/n/a/d/K/?]? n
We didn't want this hunk as part of this logical commit, so choose n

Checking what we've done
We can double check what we've added so far:
524 git-add-pi:master> git diff --cached
diff --git a/example-file b/example-file
index d503a0c..8bca897 100644
--- a/example-file
+++ b/example-file
@@ -1 +1,2 @@
+Some big sweeping change to the file
 Example line
And commit as normal:
525 git-add-pi:master> git commit -m "First change"
Created commit ad01f32: First change
 1 files changed, 1 insertions(+), 0 deletions(-)

What about the rest of the file?
You can repeat the add -pi as much as you like, or since we only have one more line to commit:
526 git-add-pi:master> git add .
git commi527 git-add-pi:master> git commit -m "final commit"
Created commit 16722ed: final commit
 1 files changed, 1 insertions(+), 0 deletions(-)

Thursday 19 August 2010

The power of git - splitting a commit


Every blog has to have a customary first post - or at least, almost every blog seems to. Rather than mine just saying that I'm starting a new blog, and I'll try and keep it up to date and blah blah blah, I thought I'd actually post some content.

For the past 9 months or so, I've been fairly heavily involved in an open source project called Mahara. Mahara is an ePortfolio system and I'm sure that I'll post about it more in the near future so I won't go into lots of detail now. Needless to say though, the work I've been doing on it has involved a variety of customisations and any sane developer stores, manages and tracks their work using a version control system or VCS. The mahara project uses git.

Git is probably the most powerful version control system I've come across and enables you to manage your source and commits with minimal effort and confusion - it's probably the only VCS I've ever used which works for you rather than making you work around it. That said, some of the really powerful features of git take a bit of understanding.

Earlier today I was telling someone how you can use git in some really cool and interesting ways. One of the things I do frequently is to take a commit and split it into a series of logical commits and I thought that I'd share this.

Setting up a quick git repository
If you want to try and follow with my examples, I've pushed my sample repository to gitorious.org. You can skip the first part by cloning it with:
git clone git://gitorious.org/thamblings/git-split-commits.git

Say that you've created a quick git repository with a few files in it:
# Create a new git repository
cd /tmp
mkdir git-example
cd git-example/
git init

# Commit the first file
echo "First File" >> one 
git add one 
git commit -m "First file committed"

# Now add a few more commits/files
echo "Second File" >> two 
echo "Third File" >> three
git add .
git reset
git add two 
git commit -m "Second File"
git add three
git commit -m "Third File"

# Now add two files in the same commit
echo "Fourth File" >> four
echo "Fifth File" >> five
git add .
git commit -m "Fourth and Fifth Files"

# And another file
echo "Sixth File" >> six 
git add .
git commit -m "Sixth File"

# Let's see what we've done
git log

But we made a mistake...
But wait a second - files four and five should have been in different files. Let's split them out.

Let's rewrite history
First we'll check out a new branch, and then we'll do an interactive rebase to edit the commit:
git checkout -b fixcommits
git rebase -i HEAD~2

We should get something like:
pick 0d3abd7 Fourth and Fifth Files
pick feaeea8 Sixth File

We want to edit commit 0d3abd7, so change that to an edit - you can use 'e' instead:
edit 0d3abd7 Fourth and Fifth Files
pick feaeea8 Sixth File

That takes us to the point after the commit 'Fourth and Fifth Files' and before 'Sixth File'.
We can then use git reset the state of our current HEAD.
git reset HEAD~1

We can then re-add and re-commit each file:
git add four
git commit -m "Fourth File"
git add five
git commit -m "Fifth File"

We can then continue our rebase and we'll be left with our finished article:
git rebase --continue

Let's get things back to master
And if we're happy with what we have, we can check it back in to our master branch with another rebase
git checkout master
git rebase fixcommits

The same also works for other git additions. So you could add parts of a file with an interactive git add (git add -pi) for example.