Git

Introduction

Git (a distributed version control system) is complex and it’s easy to forget how to exactly do this or that. This page is where I note the Git commands and configuration items I came across. Git commands usually have many options, which are not all documented here, far from it. The true Git documentation gives all the details.

Installation

On a Debian GNU/Linux system, install Git (as root) with:

apt-get install git # As root.

Configuration

Minimal post-install configuration

After installing Git, user name and e-mail address should be configured:

git config --global user.name "My Name"
git config --global user.email "my.id@example.com"

You should probably also configure the action of the git push (without argument) command. Value simple may be appropriate in most cases:

git config --global push.default simple

You can see your Git configuration with:

git config --list

Creating aliases

Create aliases with commands like:

git config --global alias.ci commit # Creates alias "ci" for command
                                    # "commit".

git config --global \
    alias.g 'log --pretty=oneline --abbrev-commit' # Creates alias "g" for
                                                   # command "log" with
                                                   # options for compact
                                                   # output.

Splitting the configuration file

All the git config --global commands mentioned above actually create entries (“config directives”) in file ~/.gitconfig. You may want to store some entries in one or more separate files. Create an [include] section in your ~/.gitconfig file for that. Travis Jeffery gives more details.

Local configuration

Configuration entries can be created in the repository local configuration (file .git/config) by using the --local option instead of the --global option in the git config commands. Repository local configuration can be used to define smudge and clean filters (see Maintaining a difference between working and committed trees).

Working with a separate repository

This command:

git init --separate-git-dir path/to/separate_git_dir.git

creates an empty Git repository like git init but does not create a .git repository in the current directory. It creates path/to/separate_git_dir.git instead (plus a .git file in the current folder containing the path to the actual repository). The same command moves the repository to the specified location if it already exists.

The --git-dir option can be used in any Git command to specify the path to the repository. Useful for cases where the working directory does not contain any .git directory or file (and this can happen if the working directory is an artifact of a build process and is cleaned out and regenerated by, say, a make clean html command (case of a Sphinx HTML project)). Example:

git --git-dir=path/to/separate_git_dir.git status

Working from outside the working directory

The -C switch can be used in any Git command to specify the path to the working directory. Example:

git -C path/to/working/directory status

The -C switch and the --separate-git-dir or --git-dir options can be combined.

The following command initializes a repository whose working directory is in the build/html subdirectory and the separate repository is .git_build_html in the current directory:

git -C build/html init --separate-git-dir ../../.git_build_html

The following command is a git status command applied to a repository whose working directory is in the build/html subdirectory and the separate repository is .git_build_html in the current directory:

git -C build/html --git-dir ../../.git_build_html status

Cloning an existing repository

Clone a repository with:

git clone repository_url

Force the name of the cloned repository by providing the name as a supplementary argument:

git clone repository_url cloned_repository_name

It is also possible to clone and check out a specific branch:

git clone -b branch_name repository_url

You can also clone without checking out anything:

git clone -n repository_url

Staging changes

git add -A stages all changes (including new files and file removals). git add . is equivalent to git add -A (except with Git version 1.x (file removals not staged)).

git add --ignore-removal does not stage file removals.

git add -u does not stage new files.

Use the -p switch to stage only parts of the changes made to a file (interactive command):

git add -p path/to/file

The following commands stage the removal of a file:

git rm path/to/file

git rm --cached path/to/file # Does not remove the file from the working
                             # directory.

git status shows the staged files (among other things).

Showing changes

Show the difference between what is staged (or what is in the last commit if no change is staged) and the working tree with:

git diff

git diff -- path/to/files # Shows changes for the specified files only.

Show the difference between the last commit of branch “branch_name” and the working tree with:

git diff branch_name

git diff branch_name -- path/to/files # Shows changes for the specified files
                                      # only.

Assuming at least one of the path is outside the working tree, the following command shows the difference between the two files:

git diff path/to/file other/path/to/file

Show the difference between what is staged and the last commit with:

git diff --staged

git diff --staged -- path/to/file # Shows changes for the specified files
                                  # only.

Show the difference between a particular commit and the working tree with commands like:

git diff 42b9c3b

git diff 42b9c3b -- path/to/files # Shows changes for the specified files
                                  # only.

Show the difference between two particular commits with commands like:

git diff 42b9c3b a92c02a

git diff 42b9c3b a92c02a -- path/to/files # Shows changes for the specified
                                          # files only.

Committing

The following commands commit the staged changes to the repository:

git commit                                # Opens a text editor for commit
                                          # message edition.

git commit -m "Commit message"            # Takes the commit message from the
                                          # command line.

git commit -F path/to/commit/message/file # Reads the commit message from a
                                          # file.

With the -a switch, all the changes (except file addition) are staged before committing:

git commit -a

A commit that has not been already pushed to a remote can be amended, that is you can stage changes and then create a commit that contains the changes already committed and the new changes. This new commit replaces the previous commit. Use the --amend option to create the new commit:

git commit --amend

Viewing the commit log

Show the commit log with:

git log

The log command is extremely configurable. I have aliases for those variants:

git log --pretty=oneline --abbrev-commit # Compact output.

git log --graph --oneline --all          # Compact graphical representation.

You can limit the number of commits shown. Example with a limit set to 4:

git log -4

You can also add a “diffstat”:

git log --stat

Working with remote repositories

Configure a remote named “origin” with:

git remote add origin remote_repository_url

Check the configured remotes with:

git remote -v

Push the commits in the “master” branch to “origin” with:

git push origin master

The following command downloads changes from “origin” (but does not affect the history of the local repository):

git fetch origin

The following command downloads changes from “origin” for branch “master” and merges the changes into the local repository:

git pull origin master

Working with branches

git status shows the current branch (among other things).

Switch to branch named “branch_name” with:

git checkout branch_name

git checkout -b branch_name # Creates the branch named "branch_name".

Rebase current branch on the latest commit of branch “master” with:

git rebase master

Merge the branch named “branch_name” into the current branch with one of the following commands:

git merge --no-ff branch_name # Creates a merge commit.

git merge branch_name         # Does not create a merge commit when the merge
                              # resolves as fast-forward.

It is possible to merge all changes on the branch named “branch_name” into the current branch without keeping the commit history:

git merge --squash branch_name # A "git commit" command is needed after that
                               # to actually create a merge commit.

Delete the local branch named “branch_name” with one of the following commands:

git branch -d branch_name # Does not delete the branch if it's not fully
                          # merged.

git branch -D branch_name # Deletes the branch even if it's not fully merged.

Rename the local branch named “old_name” to “new_name”:

git branch -m old_name new_name

Stashing changes

Store the current state of the working tree and the index in the stash stack and go back to a clean working tree with one of the following commands:

git stash push
git stash                       # Equivalent to "git stash push".
git stash push -m "Description" # Provides a descriptive message.

If you don’t want to revert the staged changes, use the --keep-index option:

git stash push --keep-index

Each git stash push command creates a new entry in the stash stack.

List the stash entries with:

git stash list

Inspect a stash entry with a command like one of the following:

git stash show stash@{0}
git stash show -p stash@{0} # Produces a patch-like output.

Remove an entry from the stash stack and apply the changes to the working tree with a command like:

git stash pop stash@{0}
git stash pop           # Equivalent to "git stash pop stash@{0}"

You can also remove an entry from the stash stack without applying the changes to the working tree:

git stash drop stash@{0}
git stash drop           # Equivalent to "git stash drop stash@{0}"

Use the --index option to also reapply the staging:

git stash pop --index

Finding text patterns in the indexed files (or in any directory tree)

Use commands like the following ones to search text patterns:

git grep <reg_exp>            # Search regular expression <reg_exp> in
                              # indexed file.

git grep -i <reg_exp>         # Case insensitive search.

git grep --no-index <reg_exp> # Useful to search in a directory which is not
                              # a Git repository.

Find the last modification date and author of any line of a file

Use this command to see the last modification date and author of any line of a file:

git blame path/to/file

Reverting the index and working directory to a previous commit

Revert the index and working directory to the last, penultimate, etc… commit with commands like:

git reset --hard HEAD^
git reset --hard HEAD^^
git reset --hard HEAD^^^

Use with care, changes to the working directory are discarded.

Maintaining a difference between working and committed trees

In some cases, you want a particular file content in your working tree, that you don’t want to commit.

For example, this page you are currently reading is part of a Sphinx project. The page you’re reading is the result of Sphinx processing some source files and generating HTML output. On project creation, Sphinx writes a Makefile and you just have to issue a make html command to generate the HTML output. The html argument is mandatory because the Makefile is so that make (without argument) does not generate the HTML output (it just outputs a help message).

For some reasons, I want to be able to generate the HTML output with make (without argument). One way to achieve that is to add those 2 lines somewhere in the file (the leading blank in the second line is actually a tabulation character):

html: Makefile
      @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

(You can download the whole file.)

I think this change could surprise Sphinx users accustomed to the usual behaviour of the Sphinx Makefile, so I prefer to commit the file with the change commented out:

# html: Makefile
#     @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

A Git smudge / clean filter makes that possible. Just create a .gitattributes file with the following line, which indicates that file Makefile is to be filtered on checkout and on staging using (respectively) a smudge and a clean filter named “html_as_default_target”:

Makefile filter=html_as_default_target

There’s no point committing the .gitattributes in such a case, so I added it to the .gitignore file:

echo .gitattributes>>.gitignore

Another option is to add it to the .git/info/exclude file. It applies only to your local copy of the repository (unlike .gitignore which applies to every clone of the repository).

The last step is to define the smudge and clean filters. The filters are commands (typically involving the sed program) given as local configuration directives:

git config --local filter.html_as_default_target.smudge 'sed "s/^# *\(.*html[
:].*\)$/\1/"' git config --local filter.html_as_default_target.clean 'sed
"s/^\(.*html[ :].*\)$/# \1/"'

The smudge filter uncomments the lines containing “html ” or “html:” and the clean filter comments out those lines. They’re visible in the .git/config file.

Creating an archive of the latest commit (without any history)

The following commands create archives of the working directory in “tar” and “zip” formats:

git archive -o latest.tar HEAD
git archive -o latest.zip HEAD

Scripting

It is sometimes needed to automate a sequence of Git commands and write a script (a shell script for example). Scripting makes it possible to define hooks.

Git commands are divided into two categories:

  • Plumbing commands,
  • Porcelain commands.

Porcelain commands should be avoided in scripts. They are meant to be used by end-users (i.e. human beings, not programs) and produce a user-friendly output which may not be stable. And output format stability is highly desirable for commands used in scripts.

Plumbing commands provide stable, parser-friendly output and must be preferred over porcelain commands in scripts.

As things are never as simple as they seem, some porcelain commands are considered plumbing commands when used with the --porcelain option. git status is an example of that:

git status --porcelain

Here are a few Git commands that are useful for scripting:

git symbolic-ref --short HEAD             # Outputs the checked out branch.
git rev-parse --abbrev-ref HEAD           # Same output (but listed as
                                          # porcelain).

git for-each-ref \                        # Lists the local branches.
    --format='%(refname:short)' \
    refs/heads/
git rev-parse --abbrev-ref --branches     # Same output (but listed as
                                          # porcelain).

git diff-index --quiet HEAD               # Does not output anything.
                                          # Terminates with exit status 0
                                          # when working tree is clean, with
                                          # non zero exit status otherwise.

git show-ref --heads branch_name          # Provides the commit hash of the
                                          # head commit of branch
                                          # "branch_name".
git show-ref --heads --abbrev branch_name # Similar, but provides short
                                          # commit hash (7 first characters
                                          # of commit hash).

git merge-base --is-ancestor hash1 hash2  # Does not output anything.
                                          # Terminates with exit status 0
                                          # when commit with hash "hash1" is
                                          # an ancestor of commit with hash
                                          # "hash2" (and thus a fast forward
                                          # merge is possible from "hash1" to
                                          # "hash2"), with non zero exit
                                          # status otherwise.

Hooks

Assuming that:

  • You have a script “script-name” meant to be used as, say, a post-commit hook,
  • This script is located at the top level of the working tree,
  • The repository is in the standard .git subdirectory,
  • The current working directory is the top level of the working tree,

you can install the hook with:

ln -s ../../script-name .git/hooks/post-commit # Creates a symbolic link in
                                               # .git/hooks.

Of course the script must be executable:

chmod +x script-name

The Git hooks documentation lists the possible hooks.

One difficulty with Git hooks is that when the hook of a repository operates on another Git repository, the -C and --git-dir options may not be respected. One solution can be to omit those options and to set environment variables instead:

export GIT_WORK_TREE=...
export GIT_DIR=...

Also the GIT_INDEX_FILE environment variable must probably be unset:

unset GIT_INDEX_FILE

More details can be found at those locations: