- Published on
Get With Git
We didn't user version control at work. Here's how I taught my team Git.
- Authors
- Name
- Nate Cornell
- @natecornell
So we've been transitioning away from CVS and appending '.old' to files as our two forms of version control. I've been leading the charge to use git whenever possible. I love it, and have been using it on my own stuff ever since going through Michael Hartl's Rails Tutorial.
I wrote this for the guys at work and have been circulating it as a training doc. There's a few sections left to do, but it's pretty good, and I've gotten good feedback from it.
Table of Contents
This will be a quick intro to using Git for your projects @ R&B. This includes implementing Git on a new project, setting up a remote repo for the project, and setting up a new instance on your local dev environment. We'll also explore committing and pushing to the repo, as well as simple branching and merging techniques. If you want to skim, hit the section titles and monospaced parts to get the juiciest bits. If you just need a cheat sheet, skip to the last page. ;)
Git your project going
So you just started a new project and you want to protect it with version control. This is a wise choice, as it allows you to quickly undo changes, keep in sync with what the other Devs are doing, as well as push changes to the production site without having to manually diff them or run a scary rsync with your fingers crossed. Let's get started.
The first thing you'll need to do is install Git. Google that, it's easy enough. Come back once you've finished.
Done? Good. Next change to the folder where your project lives and type:
$ git init
This will create a '.git' folder in your project, where your local version of the git repo will live.
Next create a file called '.gitignore'. Inside the file, list each of the files or folders you don't want committed to the git repo. Here's an example:
includes/config.inc
media/images
*.old
*.swp
syncPasswords.txt
logs/*
nakedpictures/*.jpg
Now that you've declared which files you want git to ignore, you can start adding files for git to track. To do so, run this command:
$ git add .
This will add everything in the current folder to your local git repo. Way better than CVS's crappy one at a time deal. We're not done yet, however, none of these changes have actually been committed. To commit these files, run this most auspicious command:
$ git commit -am “Initial commit.”
You'll see a flurry of activity, after which, your git repo will have been filled with all the files in your project that you didn't tell it to ignore. Since you'll be using this command all the time, let's deconstruct it a bit. 'git commit' is the action to perform. It just tells git to make a new commit based on the arguments that come next.
'-am' Means “commit all files in this project with the following commit message:”
The part in quotes is simply the commit message, which is just to help you identify your commits. This command could also be written as:
$ git commit --all --message=”Initial commit.”
… and feel free to do it that way if it makes more sense to you, but personally I can't stand all the extra typing. :)
You can also commit files one at a time if you like, just leave off the '-a' and add the filename:
$ git commit -m “updated menu items.” index.php
Production Copies
For a production server, it's often undesirable to have your entire .git folder tree in the webroot, where an attacker could potentially reach it. In this case, the git init process is slightly different.
First create a folder in /var/git to store the .git files:
$ sudo mkdir -P /var/git/myProject.git
Next, instead of running 'git init', indicate the separate folder while performing the initialization:
$ git init --separate-git-dir /var/git/myProject.git
If you are cloning an existing project, the syntax is very similar:
$ git clone ssh://servername:/var/git/myProject –-separate-git- dir=/var/git/myProject.git
This process puts all the version control files in the folder you indicate, and leaves only a file named '.git' in the project folder, to instruct git where to find the files. No more security hole!
Git a repo
So you've got your version control system all set up on your personal dev environment and that's all fine and dandy for you, but what if you want to share your workload? In this case, you'd need to set up a remote repository that will track changes for everyone.
First ssh in to 'servername' and change to the '/var/git/' directory. Create a new folder for your project.
$ cd /var/git
$ sudo mkdir myProject.git
(The naming convention for repository folders is to use the name of the project with “.git” appended.
Once the folder has been created and you can write to it, change to the folder and initialize a “bare” git repository:
$ cd myProject.git
$ git --bare init
Change the ownership to rbgit and the webedit group:
$ cd /var/git
$ sudo chown -R rbgit.webedit myProject.git
This concludes the server-side setup of the git repo; next you just need to point your dev repo to it as a remote.
On your dev machine, change to the folder where your project lives and add the remote repo like so:
$ git remote add origin ssh://servername:/var/git/myProject.git
What this command does is set up a new remote repository with the name “origin” in your local git configuration for the project. The “ssh://...” section just tells it who to log in as, on which server, on which port, and in which folder the repo lives. If you get an 'access denied' or similar error, make sure your public ssh key gets added to the 'rbgit' user's authorized_keys2 file. Ask an admin for help with that.
The next step is to actually push your committed data to the remote repo. This is accomplished via the following command:
$ git push origin master
This is another command you'll be using all the time, so let's take a look at it a little more closely. 'git push' is the basic command for pushing all your local commits to another repo, whether it be somewhere else on your dev environment, or a remote server. “origin” is the name that we gave the remote, so we identify it here as the target of our push. “master” indicates that we're pushing the “trunk” of the git repo. Later we'll get into pushing branches, but for now just pushing the “master” is good enough.
So now that your remote is set up, remember to run that “push” command whenever you make a commit to make sure everyone else can also work with the latest version as well. We'll into that in more detail in section 5.
Git your own copy
So that's all fine and great if you're the one who started this project, but what if you didn't? What if someone else started a git repo, and now you're expected to pull it down and start hacking on it as well? That's when it's time to check their repo out.
Within your dev environment, change to a folder that can serve as the parent for the project. Next, download the project to a new folder with the following command:
$ git clone ssh://servername:/var/git/something.git
Once again, make sure you use the actual name of the project repo (the creator should know that). This will create a copy of the project as a subfolder of the current folder, using the folder name of the original project. If you want to put it in a different folder name, specify the new folder name at the end of the command.
Git committed
So the biggest issue from this point on is staying current. Unlike CVS, committing changes and adding files is something that only happens in your local environment. That being the case, you should commit often. In fact, once you get into working with branches committing changes is necessary in order to avoid polluting the master with your mad science experiments.
To commit a single file, just do:
$ git commit myfile.php -m “Fixed a stupid bug.”
'git commit' is the commit action, 'myfile.php' is the file to commit, and '-m' indicates that the next argument is the commit message. This is the most basic kind of commit, and simply adds the current version of the file indicated to your local git repository for this project. You can also do this for multiple files:
$ git commit file1.php file2.php file3.php -m “Updated some junk.”
This command is essentially identical to the last one, but it will commit the latest version of each file specified to the local git repo.
You can also perform massive, bulk commits. Let's say you've been working on a cool new feature that spans several files in different files, as well as a few new ones to boot? Being the wizened git master that you are, after coming down from your productivity high you decide to see how far from the git repo you've come since starting this project. You run:
$ git status
...and it says:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: myProject/classes/StuffDoer.php
modified: myProject/index.php
modified: myProject/js/functions.js
modified: myProject/css/styles.css
Untracked files:
(use "git add <file>..." to include in what will be committed)
myProject/Classes/ThingSayer.php
myProject/js/superfluousAnimations.js
myProject/css/prettyThings.css
Oh man. That's going to take a lot of commands to add the new files and commit everything, right? Wrong. Git's got you covered. To get yourself and all your precious changes into the safe, loving arms of your local repo, just run:
$ git add .
$ git commit -am “Added that sweet feature everyone wanted. Rocked it.”
Done. Time to get paid. This is probable the most efficient way to keep your repo in tip-top shape. Be sure run 'git status' first so you know exactly what's being added, that way you have a chance to add things to the '.gitignore' first.
What if you like the efficiency, but you have a file that's not ready? Or perhaps your 'config.inc' has some special settings that shouldn't be committed? You can temporarily exclude a file from commits by running:
$ git update-index --assume-unchanged notReadyFile.php
This command performs the 'update-index' action, which git uses for performing changes directly to the repo's index of tracked files. '--assume-unchanged' indicates that the file specified should be ignored when checking against the repo version. This protects the repo from the funky changes you're hiding in that file by telling git “don't even bother checking”.
SCENE EXT. DEV SERVER - MOS REPOSITORY The developers are stopped on a crowded street by several combat-hardened CommitTroopers who look over their files for changes. A Trooper questions Luke.
COMMITTROOPER How long have you had these files?
LUKE DEVELOPER About three or four days.
GIT KENOBI We're committing them if you want them.
COMMITTROOPER Let me see that config.inc. Luke becomes very nervous as he fumbles to make a few last-minute changes to the file so it will match the production version while Git Kenobi speaks to the Trooper in a controlled voice.
GIT KENOBI You don't need to see his config.inc.
COMMITTROOPER We don't need to see his config.inc.
GIT KENOBI These are not the files you're looking for.
COMMITTROOPER These are not the files we're looking for.
GIT KENOBI He can commit these changes, and go about his business.
COMMITTROOPER You can commit these changes and go about your business.
GIT KENOBI (to Luke) Move along.
COMMITTROOPER Move along. Move along.
You see what happened there? It's like Jedi magic!!
So committing is pretty cool. Batch commits, and committing locally only are pleasant when compared to the do-or-die approach of CVS. But what happens when you're finally ready to push your changes up to that central repo where all that nice collaboration with your other developer buddies happens? That's where “push” comes in.
Git back to your origins
So eventually you've gotta get your changes out there. You've got to get it to the production server. You've got to get it backed up. You need everyone else working on this project using your new version of the PEAR DB wrapper before they get in too deep to come back.
To sync your local repo changes to the master repo (the one we named 'origin', back in #2), just run:
$ git push origin master
The 'push' action is how git moves things from your local repo out to another git repository. In the above command, 'origin' is the name of the central repository we created on ahrn-mailer2 (remember the 'remote add' command?), and 'master' is the branch that we want to push (the trunk, in this case).
It's really that simple.
So what if you want to make sure that your repo is up to date? Surely you aren't the only one committing code changes, or else you'd probably be getting paid a lot more. How do you get the changes other folks are committing to the central repo. That my fuzzy searching friend is a job for 'pull'.
If you are simply curious, and want to know what the 'origin' repo has been up to these past few weeks, just run this simple and non-destructive command:
$ git fetch origin
Basically all fetch does is grab the index and file references from the remote you specify ('origin' in this case) and add them to your local index for that repo. This helps you compare then contents of your local repository with that of the remote without having to wait while it contacts the other server every time.
Now if you run 'git status' you'll probably see something like this:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
Or this:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working directory clean
Cool, looks like you're up to date! You might have some changes to send that direction, but there's nothing in that distant repo you don't already have. Commit and push with confidence!
But what if you see this?:
$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working directory clean
Or even this?:
$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commit each, respectively.
(use "git pull" to merge the remote branch into yours)
nothing to commit, working directory clean
In the first situation, the next thing you gotta do is a pull. The command for pulling changes from a remote looks like this:
$ git pull origin master
Its identical to the “push” command, except for the 'pull' part. We push when we have changes going out, and pull when they're coming in. Otherwise the command is the same. Let's hear it for consistency! Less weird things to remember is a Good Thing. In the first scenario, a 'fast-forward' will occur, and you'll get the latest changes without anything weird happening to your local files.
Now the only gotcha here is in our second scenario, where you have changes, the remote has changes, and for all we know they're the same lines in the same file. In most cases that won't be the case, and you'll just be “fast-forwarded” like in the first case. Sometimes, however, there will be conflicts.
Git conflict resolution
So you might have conflict with the remote repo. It's important to express your feelings. This is a safe place for both of you, and what we're going to do here is create an open space for conflict resolution, without judgement or attacks.
So in the scenario that led us here, we fetched the remote, got the status, and discovered that both have commits waiting. That doesn't necessarily mean there are conflicts, but it is probably better to find out before we pull, and definitely before we push. Let's start by just getting a little more info about the situation. A good command for that is:
$ git diff master origin/master
As our M.O. predicts, we're gonna examine this command. 'git diff' is the action, and it basically just means we're asking for the differences between any changed versions of files. 'master' is the local branch we want to compare, and 'origin/master' is the repo/branch to compare against. '--name-only' tells git we only want the names of the files, otherwise it could be a flood of output! You will probably get something like this:
$ git diff master origin/master --name-only
somefile.php
anotherfile.js
Now that you know some of the offenders, you can take a closer look at each of them by doing a 'git diff' on each of them like so:
$ git diff master origin/master somefile.php
This will give you some output (in nice Christmas colors!) like this:
$ git diff master origin/master anotherfile.js
diff --git a/anotherfile.js b/anotherfile.js
index eac5efe..e28fc9c 100644
--- a/anotherfile.js
+++ b/anotherfile.js
@@ -1,3 +1,3 @@
// Function for finding best fruit
-function bestFruit() { alert('Apples are the best!'); }
+function bestFruit() { alert('Oranges FTW!'); }
weGotTheFuncGottaHaveThatFunc
All that bolding and coloring make this process a lot more fun than it usually is, don't you think? Anyways, we can see the offending line compared against our own, we can call up the other dev and try to convince him how much better oranges really are.
The next step is to run:
$ git pull origin master
Which will result in a scary-looking message:
$ git pull origin master
From /Users/devdude/workspace/junk/collabProject
* branch master -> FETCH_HEAD
Auto-merging anotherfile.js
CONFLICT (content): Merge conflict in anotherfile.js
Automatic merge failed; fix conflicts and then commit the result.
This just means that git merged the conflicting lines from the repo into your local version, and left your repo in an 'unmerged' state. This means that you are not yet finished with the merge that began with your “pull” request. If you edit the file, you'll see something like the following:
<<<<<<< HEAD
function bestFruit() { alert('Apples are the best!'); }
=======
function bestFruit() { alert('Oranges FTW!'); }
>>>>>>> 77887e60ddd5244533ad30c76f01ba94459efeef
Simply remove all the lines with <<<
, ===
, and >>>
, and fix the code conflict. Once that's done, you must re-add the file:
$ git add anotherfile.js
...and then commit your harmonious version of the file:
$ git commit -am “Solved the fruit problem.”
Then be sure to push the new version up to the repo (I wish I could see the look on what's-his-name's face next time he tries to get fruit):
$ git push origin master
Bam! Problem solved. Conflicts are no big deal in git land; and part of that is because of how easy it is to keep everyone in sync in the first place. Speaking of keeping in sync, what happens when two developers need to work on the same file, and want to keep committing and pushing, but don't want to step on each other's toes? That's when branches come in handy.
Pro Tip:
Instead of diffing, merging, editing, and adding a conflicted file, do it all at once! First set your favorite editor for diffing and merging:
$ git config --global diff.tool vimdiff
$ git config --global merge.tool vimdiff
(my fave is vimdiff, but p4merge is a nice GUI tool - run 'git config help' to see a list of tools) ...then pull the master:
$ git pull origin master # (will complain)
then view the diffs and edit your merged local version all in one shot! :
$ git mergetool
Once finished editing, saving the file will automatically add it back to your repo and complete the merge! All you gotta do is commit the file and push!
*Note: It will leave behind an unedited copy with “.orig” taped on the end of the filename. You can delete this or add it to your .gitignore file if you wanna play it safe.