You’re reading Ry’s Git Tutorial |
Tips & Tricks
This module presents a broad survey of useful Git utilities. We’ll take a step back from the theoretical aspects of Git and focus on common tasks like preparing a project for release and backing up a repository. While working through this module, your goal shouldn’t be to master all of these miscellaneous tools, but rather to understand why they were created and when they might come in handy.
If you’ve been following along from the previous module, you already have everything you need. Otherwise, download the zipped Git repositories from the above link, uncompress them, and you’re good to go.
Archive The Repository
First, let’s export our repository into a ZIP archive. Run the
following command in your local copy of my-git-repo
.
git
archive
master
--format
=zip
--output
=../website-12-10-2012.zip
Or, for Unix users that would prefer a tarball:
git
archive
master
--format
=tar
--output
=../website-12-10-2012.tar
This takes the current master
branch and places all of its
files into a ZIP archive (or a tarball), omitting the .git
directory. Removing the .git
directory removes all version control
information, and you’re left with a single snapshot of your project.
You can send the resulting archive to a client for review, even if they don’t have Git installed on their machine. This is also an easy way to create Git-independent backups of important revisions, which is always a good idea.
Bundle the Repository
Similar to the git archive
command, git bundle
turns a repository into a single file. However, in this case, the file retains
the versioning information of the entire project. Try running the following
command.
git
bundle
create
../repo.bundle
master
It’s like we just pushed our master
branch to a remote,
except it’s contained in a file instead of a remote repository. We can
even clone it using the same git clone
command:
cd
..
git
clone
repo.bundle
repo-copy
-b
master
cd
repo-copy
git
log
cd
../my-git-repo
The log output should show you the entire history of our master
branch, and repo.bundle
is also the origin
remote for
the new repository. This is the exact behavior we encountered when cloning a
“normal” Git repository.
Bundles are a great way to backup entire Git repositories (not just an
isolated snapshot like git archive
). They also let you share
changes without a network connection. For example, if you didn’t want to
configure the SSH accounts for a private Git server, you could bundle up the
repository, put it on a jump drive, and walk it over to your co-worker’s
computer. Of course, this could become a bit tiresome for active projects.
We won’t be needing the repo.bundle
file and
repo-copy
folder, so go ahead and delete them now.
Ignore a File
Remember that Git doesn’t automatically track files because we
don’t want to record generated files like C binaries or compiled Python
modules. But, seeing these files under the “Untracked files” list
in git status
can get confusing for large projects, so Git lets us
ignore content using a special text file called .gitignore
. Each
file or directory stored in .gitignore
will be invisible to
Git.
Let’s see how this works by creating a file called
notes.txt
to store some personal (private) comments about the
project. Add some text to it and save it, then run the following.
git
status
As expected, this will show notes.txt
in the “Untracked
files” section. Next, create a file called .gitignore
in the
my-git-repo
folder and add the following text to it. Windows users
can create a file that starts with a period by executing the touch
.gitignore
command in Git Bash (you should also make sure hidden files
are visible in your file browser).
notes
.txt
Run another git status
and you’ll see that the notes file
no longer appears under “Untracked files”, but
.gitignore
does. This is a common file for Git-based projects, so
let’s add it to the repository.
git
add
.gitignore
git
commit
-m
"Add .gitignore file"
git
status
You can also specify entire directories in .gitignore
or use
the *
wildcard to ignore files with a particular extension. For
example, the following is a typical .gitignore
file for a simple C
project. It tells Git to overlook all .o
, .out
, and
.exe
files in the repository.
*.o *.out *.exe
Stash Uncommitted Changes
Next, we’ll take a brief look at stashing, which
conveniently “stashes” away uncommitted changes. Open up
style.css
and change the h1
element to:
h1
{
font-size
:
32px
;
font-family
:
"Times New Roman"
,
serif
;
}
Now let’s say we had to make an emergency fix to our project. We
don’t want to commit an unfinished feature, and we also don’t want
to lose our current CSS addition. The solution is to temporarily remove these
changes with the git stash
command:
git
status
git
stash
git
status
Before the stash, style.css
was listed as “Changed by not
updated.” The git stash
command hid these changes, giving us
a clean working directory. We’re now able to switch to a new hotfix
branch to make our important updates—without having to commit a
meaningless snapshot just to save our current state.
Let’s pretend we’ve completed our emergency update and we’re ready to continue working on our CSS changes. We can retrieve our stashed content with the following command.
git
stash
apply
The style.css
file now looks the same as it did before the
stash, and we can continue development as if we were never interrupted. Whereas
patches represent a committed snapshot, a stash represents a set of
uncommitted changes. It takes the uncommitted modifications, stores
them internally, then does a git reset --hard
to give us a clean
working directory. This also means that stashes can be applied to any
branch, not just the one from which it was created.
In addition to temporarily storing uncommitted changes, this makes stashing
a simple way to transfer modifications between branches. So, for example, if
you ever found yourself developing on the wrong branch, you could stash all
your changes, checkout the correct branch, then run a git stash
apply
.
Let’s undo these CSS updates before moving on.
git
reset
--hard
Hook into Git’s Internals
Arguably, Git’s most useful configuration options are its
hooks. A hook is a script that Git executes every time a
particular event occurs in a repository. In this section, we’ll take a
hands-on look at Git hooks by automatically publishing our website every time
someone pushes to the central-repo.git
repository.
In the central-repo.git
directory, open the hooks
directory and rename the file post-update.sample
to
post-update
. After removing the .sample
extension,
this script will be executed whenever any branch gets pushed to
central-repo.git
. Replace the default contents of
post-update
with the following.
#!/bin/sh
# Output a friendly message
echo
"Publishing master branch!"
>&2
# Remove the old `my-website` directory (if necessary)
rm
-rf
../my-website
# Create a new `my-website` directory
mkdir
../my-website
# Archive the `master` branch
git
archive
master
--format
=tar
--output
=../my-website.tar
# Uncompress the archive into the `my-website` directory
tar
-xf
../my-website.tar
-C
../my-website
exit
0
While shell scripts are outside the scope of this tutorial, the majority of
commands in the above code listing should be familiar to you. In short, this
new post-update
script creates an archive of the
master
branch, then exports it into a directory called
my-website
. This is our “live” website.
We can see the script in action by pushing a branch to the
central-repo.git
repository.
git
push
../central-repo.git
master
After the central repository receives the new master
branch,
our post-update
script is executed. You should see the
Publishing master branch!
message echoed from the script, along
with a new my-website
folder in the same directory as
my-git-repo
. You can open index.html
in a web browser
to verify that it contains all the files from our master
branch,
and you can also see the intermediate .tar
archive produced by the
hook.
This is a simple, unoptimized example, but Git hooks are infinitely
versatile. Each of the .sample
scripts in the hooks
directory represents a different event that you can listen for, and each of
them can do anything from automatically creating and publishing releases to
enforcing a commit policy, making sure a project compiles, and of course,
publishing websites (that means no more clunky FTP uploads). Hooks are even
configured on a per-repository basis, which means you can run different scripts
in your local repository than your central repository.
For a detailed description of the available hooks, please consult the official Git documentation.
View Diffs Between Commits
Up until now, we’ve been using git log --stat
to view the
changes introduced by new commits. However, this doesn’t show us which
lines have been changed in any given file. For this level of detail, we need
the git diff
command. Let’s take a look at the updates from
the Add a pink block of color
commit:
git
diff
HEAD~2..HEAD~1
This will output the diff between the Add a pink block of color
commit (HEAD~1
) and the one before it (HEAD~2
):
index 828dd1a..2713b10 100644
--- a/pink.html
+++ b/pink.html
@@ -4,10 +4,17 @@
<title>The Pink Page</title> <link rel="stylesheet" href="style.css" /> <meta charset="utf-8" />+ <style>
+ div {
+ width: 300px;
+ height: 50px;
+ }
+ </style>
</head> <body> <h1 style="color: #F0F">The Pink Page</h1> <p>Only <span style="color: #F0F">real men</span> wear pink!</p>+ <div style="background-color: #F0F"></div>
<p><a href="index.html">Return to home page</a></p> </body>
This diff looks nearly identical to the patches we created in the previous
module, and it shows exactly what was added to get from HEAD~2
to
HEAD~1
. The git diff
command is incredibly useful for
pinpointing contributions from other developers. For example, we could have
used the following to view the differences between John’s
pink-page
branch and our master
before merging it
into the project in Distributed Workflows.
git
diff
master..john/pink-page
This flexible command can also generate a detailed view of our uncommitted
changes. Open up blue.html
, make any kind of change, and save the
file. Then, run git diff
without any arguments:
git
diff
And, just in case we forgot what was added to the staging area, we can use
the --cached
flag to generate a diff between the staged snapshot
and the most recent commit:
git
add
blue.html
git
diff
--cached
A plain old git diff
won’t output anything after
blue.html
is added to the staging area, but the changes are now
visible through the ‑‑cached
flag. These are the three
main configurations of the git diff
command.
Reset and Checkout Files
We’ve used git reset
and git checkout
many
times throughout this tutorial; however, we’ve only seen them work with
branches/commits. You can also reset and check out individual files, which
slightly alters the behavior of both commands.
The git reset
we’re accustomed to moves the current
branch to a new commit and optionally updates the working directory to match.
But when we pass a file path, git reset
updates the staging
area to match the given commit instead of the working directory, and it
doesn’t move the current branch pointer. This means we can remove
blue.html
from the staged snapshot with the following command.
git
reset
HEAD
blue.html
git
status
This makes the blue.html
in the staging area match the version
stored in HEAD
, but it leaves the working directory and current
branch alone. The result is a staging area that matches the most recent commit
and a working directory that contains the modified blue.html
file.
In other words, this type of git reset
can be used to unstage a
file:
Let’s take this one step further with git checkout
. The
git checkout
we’ve been using updates the working directory
and switches branches. If we add a file path to git
checkout
, it narrows its focus to only the specified file and does
not update the branch pointer. This means that we can “check
out” the most recent version of blue.html
with the following
command.
git
checkout
HEAD
blue.html
git
status
Our blue.html
file now looks exactly like the version stored in
HEAD
, and we thus have a clean working directory. Passing a file
path to git checkout
reverts that file to the specified
commit.
To summarize the file-path behavior of git reset
and git
checkout
, both take a committed snapshot as an reference point and make
a file in the staging area or the working directory match that reference,
respectively.
Aliases and Other Configurations
Typing git checkout
every time you wanted to see a new branch
over the last ten modules has been a bit verbose. Fortunately, Git lets you
create aliases, which are shortcuts to other commands.
Let’s create a few aliases for our most common commands:
git
config
--global
alias.co
checkout
git
config
--global
alias.ci
commit
git
config
--global
alias.br
branch
Now, we can use git co
instead of git checkout
,
git ci
for committing, and git br
for listing
branches. We can even use git br <branch-name>
for creating
a new branch.
Git stores these aliases in a global config file, similar to the local
config file we looked at in Mary’s repository
(marys-repo/.git/config
). By default, global configurations reside
in ~/.gitconfig
, where the ~
character represents
your home directory. This file should resemble the following.
[user] name = Ryan email = ryan.example@rypress.com [alias] co = checkout ci = commit br = branch
Of course, your settings should reflect the name and email you entered in The Basics. As you can see, all of our new aliases
are also stored in .gitconfig
. Let’s add a few more useful
configurations by modifying this file directly. Append the following to
.gitconfig
.
[color] status = always [core] editor = gvim
This makes sure Git colorizes the output of git status
and that
it uses the gVim text editor for creating commit messages. To use a different
editor, simply change gvim
to the command that opens your editor.
For example, Emacs users would use emacs
, and Notepad users would
use notepad.exe
.
Git includes a long list of configuration options, all of which can be found
in the official
manual. Note that storing your global configurations in a plaintext file
makes it incredibly easy to transfer your settings to a new Git installation:
just copy ~/.gitconfig
onto your new machine.
Conclusion
In this module, we learned how to export snapshots, backup repositories, ignore files, stash temporary changes, hook into Git’s internals, generate diffs, reset individual files, and create shorter aliases for common commands. While it’s impossible to cover all of Git’s supporting features in a hands-on guide such as this, I hope that you now have a clearer picture of Git’s numerous capabilities.
With all of these convenient features, it’s easy to get so caught up in designing the perfect workflow that you lose sight of Git’s underlying purpose. As you add new commands to your repertoire, remember that Git should always make it easier to develop a software project—never harder. If you ever find that Git is causing more harm than good, don’t be scared to drop some of the advanced features and go back to the basics.
The final module will go a long way towards helping you realize the full potential of Git’s version control model. We’ll explore Git’s internal database by manually inspecting and creating snapshots. Equipped with this low-level knowledge, you’ll be more than ready to venture out into the reality of Git-based project management.
Quick Reference
git archive <branch-name> --format=zip --output=<file>
- Export a single snapshot to a ZIP archive called
<file>
. git bundle create <file> <branch-name>
- Export an entire branch, complete with history, to the specified file.
git clone repo.bundle <repo-dir> -b <branch-name>
- Re-create a project from a bundled repository and checkout
<branch‑name>
. git stash
- Temporarily stash changes to create a clean working directory.
git stash apply
- Re-apply stashed changes to the working directory.
git diff <commit-id>..<commit-id>
- View the difference between two commits.
git diff
- View the difference between the working directory and the staging area.
git diff --cached
- View the difference between the staging area and the most recent commit.
git reset HEAD <file>
- Unstage a file, but don’t alter the working directory or move the current branch.
git checkout <commit-id> <file>
- Revert an individual file to match the specified commit without switching branches.
git config --global alias.<alias-name> <git-command>
- Create a shortcut for a command and store it in the global configuration file.