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

If you use a git hosting service like GitHub, GitLab or Bitbucket, you may want Git to store your credentials for the service. One way to achieve that is to use the Git credential helper.

The following command causes Git to store the credentials you provide next time you issue a (for example) git push command, so that you won’t ever have to retype them:

git config --global credential.helper store

The credentials are stored in ~/.git-credentials. They are not encrypted, so check that only you have read permission on that file (if this is not the case, issue a chmod 600 ~/.git-credentials command).

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

Cloning to a new directory

Clone a repository to a new directory 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

Cloning in an existing directory

Sometimes you want to turn an existing directory into a clone of a Git repository. It is possible with a sequence of commands like:

cd dir/to/turn/into/a/clone          # Move to the directory.
git init                             # Create an empty Git repository.
git remote add origin repository_url # Configure the remote.
git pull origin master               # Pull master branch.

The git pull origin master command fails if it has to overwrite existing local files. If you really want a clone of the remote repository, remove the local files and run the git pull origin master command again.

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).

Note also that there is a dry run option for git add. This is the -n switch. The following command shows what would be staged but does not actually stage:

git add -n .

This comes especially handy when you want to ignore files and/or directories and you are not sure the .gitignore file is correct.

Unstaging changes

You can unstage a file that you have just mistakenly staged with a command like:

git reset -- path/to/file

Ignoring files and directories

Quiet often there are files and/or directories in the working directory that shouldn’t be tracked by the version control system. Such files and/or directories must be mentioned in file .gitignore or in file .git/info/exclude. .gitignore is tracked, .git/info/exclude is not. Of course, you can mention some of the files/directories to be ignored in .gitignore and the others in .git/info/exclude.

The official documentation provides information on the patterns that can be used in .gitignore.

Sometimes, you want to ignore everything except a few files. For example, a .gitignore file with the following content would cause the whole working directory to be ignored, except:

  • file .gitignore
  • file file_1;
  • file file_2;
  • file dir_a/subdir/file_3;
  • file dir_a/subdir/file_4.
  • all files and directories in directory dir_b with infinite depth.
/*
!.gitignore
!file_1
!file_2
dir_a/*
!dir_a
dir_a/subdir/*
!dir_a/subdir
!dir_a/subdir/file_3
!dir_a/subdir/file_4
!dir_b

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 <reg_exp> <subdir>   # Restrict search to subdirectory <subdir>.

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

git grep -untracked <reg_exp> # Search also untracked files.

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.

Note that the filters can be defined in external scripts. The clean filter above could be a file containing:

#!/bin/sh

sed “s/^(.*html[ :].*)$/# 1/” $1

Assuming that this file is named clean_filter is located in a subdirectory called filter of the working directory, the git config --local filter.html_as_default_target.clean should be (note the %f):

git config --local filter.html_as_default_target.clean 'filter/clean_filter %f'

Of course, the script must be executable:

chmod +x filter/clean_filter

One more thing that I’ve learned while working on a clean filter is that the sed program accepts multiple substitution commands, separated with semicolons. It can be very useful when you need to clean multiple lines in a file. Be careful, in some cases you may perform two substitutions at places where you want only one. Try for example:

printf "one\ntwo\nthree\n" | sed "s/one/two/; s/two/three/;"

I’m not sure what the most practical way to validate a clean filter is, but gitk can come in handy here. Commit, browse the commit with gitk and check that the clean filter has caused the expected changes. If not, fix the clean filter and amend the commit.

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

apt-get install gitk

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 (but
                                          # possibly with untracked files),
                                          # with non zero exit status
                                          # otherwise.

git status --porcelain                    # Outputs nothing if the working
                                          # directory is clean (and without
                                          # any untracked files), outputs
                                          # something if the working
                                          # directory is not clean and/or has
                                          # untracked files.

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: