Git Introduction

Git stores data as snapshots of the project over time. Everything in Git is referred to by a SHA-1 hash checksum. Git has three main states of your files:

  • Modified (file changed but not committed to the database yet).
  • Staged (file marked as modified in its current version to go into your next commit snapshot).
  • Committed (file is safely stored in your local database).

Git setup

When you customize git environment, git stores configuration variables in three places:

  1. System variables set with --system option are stored in /etc/gitconfig file.
  2. User variables set with --global option are stored in~/.gitconfig file.
  3. Repository variables set with --local option (default) are stored in .git/config file (relative to your specific repository).

View Configuration

View all of your settings:

git config --list --show-origin

Check specific key's value in git config:

git config user.name

Configure User

Set up your username and email address:

git config --global user.name "John Doe"
git config --global user.email [email protected]

Configure Editor

Configure your default text editor:

git config --global core.editor nano

Configure Default Branch Name

Set main as the default branch name:

git config --global init.defaultBranch main

Getting Help

Get help while using Git:

git help <verb>
git <verb> help
man git-<verb>
git <verb> -h

Git Basics

Initializing a Repository

Initialize Git in the current directory:

git init

Begin tracking files and do an initial commit:

git add *.c
git add LICENSE
git commit -m 'Initial commit'

Clone an existing repository:

git clone https://github.com/libgit2/libgit2

Clone a repository and specify the new directory name for it:

git clone https://github.com/libgit2/libgit2 mylibgit

Staging Modified Files

Check the status of your files:

git status

Add a file to the staging area:

git add README

Get a short output of the status command:

git status -s
git status --short

The first column indicates the status of the staging area, and the right column indicates the status of the working tree. Untracked files are marked with ??, new files added to the staging area have an A, and modified files have an M next to them.

Ignoring Files

Ignore files that Git should not add or show as being untracked by creating a .gitignore file in the repository directory:

# ignore all .a files
*.a

# but do track lib.a, even though you're ignoring .a files above
!lib.a

# only ignore the TODO file in the current directory, not subdir/TODO
/TODO

# ignore all files in any directory named build
build/

# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt

# ignore all .pdf files in the doc/ directory and any of its subdirectories
doc/**/*.pdf

For more examples view a collection of .gitignore templates.

The rules for the patterns in .gitignore file:

  • Blank lines of lines starting with # are ignored.
  • Standard glob patterns work and are applied recursively.
  • Avoid recursivity by starting patterns with a forward slash /.
  • Specify a directory by ending patterns with a forward slash /.
  • Negate a pattern with an exclamation point !.

Viewing Staged and Unstaged Changes

Compare what is in your working directory with what is in your staging area:

git diff

Compare changes that you've staged with your last commit:

git diff --staged
git diff --cached

View options for external diff viewing program on your system:

git difftool --tool-help

Committing Changes

Commit the staged changes:

git commit

Commit and add the diff of your change in the commit message:

git commit -v

Type your commit message inline:

git commit -m "Story 123: fix user signup form validation"

Best Practice for Commit Messages

A good commit message consists of three parts:

  1. Commit Title (First Line)
    • Keep it short (50-72 characters).
    • Use imperative mood (as if giving a command).
    • Start with a concise summary of the change.
  2. Commit Description (Optional, Multi-line)
    • Explain why the change was made, not just what changed.
    • Provide context if it's not obvious from the code.
    • If fixing a bug, mention the issue number: Fixes #123
    • Use bullet points for clarity in complex changes.
  3. Footer (Optional)
    • Reference relevant tickets, issues, or breaking changes.

Example of a single-line commit (small changes):

fix: Correct typo in error message

Example of a multi-line commit (more details):

feat: Add user authentication using JWT

- Implement JWT-based authentication for API access
- Add login and logout routes
- Store tokens securely in HTTP-only cookies
- Fixes #45

Example of a commit with a breaking change:

refactor: Update user table schema

- Remove `username` field, use `email` as the unique identifier
- Migrate old data to reflect changes
- Update related authentication methods

BREAKING CHANGE: Existing usernames will no longer work as login credentials.

Using prefixes helps categorize commits systematically:

Prefix Meaning
feat Introduces a new feature
fix Fixes a bug
docs Updates documentation only
style Formatting, whitespace, linting (no logic changes)
refactor Code refactoring (no functional changes)
perf Performance improvements
test Adds or updates tests
chore Maintenance tasks (build scripts, dependencies)

More information: Git Guidelines, A Note About Git Commit Messages, Git project.

Skipping the Staging Area

Skip the git add part and automatically stage every file that is already tracked before doing the commit:

git commit -a -m 'Add new benchmarks'

Removing Files

Remove a file from your staging area and also from your working directory:

git rm README

Remove a file that you have already modified or added to staging area:

git rm -f README

Remove a file from your staging area but keep it in your working tree:

git rm --cached README

Remove files using glob patterns (note the use of backslash \ to prevent shell's filename expansion):

git rm log/\*.log

Moving Files

Rename a file in Git:

git mv file_from file_to

The above command is equivalent to:

mv file_from file_to
git rm file_from
git add file_to

Viewing the Commit History

List the commits in reverse chronological order:

git log

List the last two commits with patch output (differences) introduced in each commit:

git log -p -2
git log --patch -2

List commits with abbreviated stats:

git log --stat

List commits with one line output:

git log --pretty=oneline

Specify the format explicitly:

git log --pretty=format:"%h - %an, %ar : %s"

Useful specifiers for git log --pretty=format:

Specifier Description of Output
%H Commit hash
%h Abbreviated commit hash
%T Tree hash
%T Tree hash
%t Abbreviated tree hash
%P Parent hashes
%p Abbreviated parent hashes
%an Author name
%ae Author email
%ad Author date (format respects the --date=option)
%ar Author date, relative
%cn Committer name
%ce Committer email
%cd Committer date
%cr Committer date, relative
%s Subject

Add ASCII graph while showing branch and merge history:

git log --pretty=format:"%h %s" --graph

Common options to git log:

Option Description
-p Show the patch introduced with each commit
--stat Show statistics for files modified in each commit
--shortstat Display only the changed/insertions/deletions line from the --stat command
--name-only Show the list of files modified after the commit information
--name-status Show the list of files affected with added/modified/deleted information as well
--abbrev-commit Show only the first few characters of the SHA-1 checksum instead of all 40
--relative-date Display the date in a relative format (for example, “2 weeks ago”) instead of using the full date format
--graph Display an ASCII graph of the branch and merge history beside the log output
--pretty Show commits in an alternate format. Option values include oneline, short, full, fuller, and format (where you specify your own format)
--oneline Shorthand for --pretty=oneline --abbrev-commit used together

View a specific commit:

git show commit_hash

Display only those commits that are on the latter branch and that are not on the first branch:

git log --no-merges issue54..origin/master

Limiting Log Output

List the last n commits:

git log -10

List the commits made in the last two weeks:

git log --since=2.weeks

List the commits matching --author and --grep criteria:

git log --author="John Doe" --grep="fix"

List the commits that match the output of all --grep patterns:

git log --grep="fix" --grep="issues" --all-match

List all commits that changed the number of occurrences of a given string:

git log -s function_name

List commits that introduced a change to files specified by a path:

git log -- path/to/file

Options to limit the output of git log:

Option Description
-<n> Show only the last n commits
--since, --after Limit the commits to those made after the specified date
--until, --before Limit the commits to those made before the specified date
--author Only show commits in which the author entry matches the specified string
--committer Only show commits in which the committer entry matches the specified string
--grep Only show commits with a commit message containing the string
-S Only show commits adding or removing code matching the string
--no-merges Prevent the display of merge commits in log history

Undoing Things

Modify the last commit with additional changes from your current staging area and a new commit message:

git commit --amend

Unstage a staged file (either of the following):

git reset HEAD file_name
git restore --staged file_name

Revert a file back to the last staged or committed version (either of the following):

git checkout -- file_name
git restore file_name

Working with Remotes

List remote servers you have configured:

git remote -v

Add a new remote to Git repository:

git remote add remote_name https://github.com/user_name/repository_name

Get data from a remote (but not merge it yet):

git fetch remote_name

Fetch data from the server and automatically merge:

git pull

Set default behavior of git pull to fast-forward if possible, else create a merge commit:

git config --global pull.rebase "false"

Set default behavior of git pull to fast-forward if possible, else rebase:

git config --global pull.rebase "true"

Push your work on the master branch to your origin server:

git push origin master

Inspect a remote:

git remote show origin

Rename a remote:

git remote rename old_name new_name

Remove a remote:

git remote remove remote_name

Tagging

List the existing tags:

git tag

Filter existing tags:

git tag -l "v1.8.2*"

Create an annotated tag:

git tag -a v0.1.0 -m "Release version 0.1.0"

Create a lightweight tag

git tag v0.1.1

Show the tag data:

git show v0.1.0

Create a tag for a specified commit hash:

git tag -a v0.0.1 ca82a6d

Share a tag with a remote:

git push origin v0.1.0

Push all the tags to a remote:

git push origin --tags

Push only annotated tags to a remote:

git push origin -- follow-tags

Delete a tag locally:

git tag -d v0.1.0

Delete tag on a remote:

git push origin --delete tag_name

Check out a tag to view the version of files a tag is pointing to:

git checkout v0.2.0

Check out a tag and create a new branch:

git checkout -b v0.2.0

Git Aliases

Set up Git aliases:

git config --global alias.unstage 'restore --staged'
git config --global alias.last 'log -1 -p HEAD'
git config --global alias.ls 'log --graph --decorate --oneline'
git config --global alias.la 'log --graph --pretty=format:"%h - %an, %ar : %s%d"'
git config --global alias.visual '!gitk'

Check alias definitions:

git config --global --list

Remove Git alias:

git config --global --unset alias.alias_name

Git Branching

A branch in Git is a lightweight movable pointer to one of the commits. The default branch name in Git is master. As you start making commits, the pointer refers to the last commit that you've made while you are checked out in that branch.

Create a new branch:

git branch branch_name

Switch to an existing branch (either of the following commands):

git checkout branch_name
git switch branch_name

When you checkout to another branch, Git moves the HEAD pointer to refer to that branch_name and replaces files in your working directory to the snapshot the branch points to.

Show commit history for the desired branch:

git log branch_name

Show commit history for all of the branches:

git log --all

Create a new branch and switch to it at the same time (either of the following):

git checkout -b branch_name
git switch -c branch_name
git switch --create branch_name

Return to previously checked out branch:

git switch -

Delete a branch:

git branch -d branch_name

Merging

Merge the finished work from iss23 branch into the master branch:

git switch master
git merge iss23

When there are conflicts in the merge, first check which files are unmerged:

git status

Then open those files listed under unmerged paths section and resolve those conflicts manually:

<<<<<<< HEAD
<div>contact us: [email protected]</div>
=======
<footer>
    Please contact us at [email protected]
</footer>
>>>>>>> iss23

Alternatively, use a graphical tool to resolve merge issues:

git mergetool

Set up Meld to be the default diff and merge tool:

git config --global diff.tool meld
git config --global merge.tool meld
git config --global difftool.prompt false
git config --global mergetool.prompt false
git config --global mergetool.keepBackup false

Then stage and commit the changes to finalize the merge:

git commit -a

After merge is done, close the issue in your issue-tracking system and delete the iss23 branch :

git branch -d iss23

Branch Management

List all local branches:

git branch

See the last commit on each branch:

git branch -v

List branches that are already merged into the current branch:

git branch --merged

Branches on this list without the * in front of them are ready to be deleted because you have already merged them into the current branch.

List all the branches that contain work you have not yet merged into the current branch:

git branch --no-merged

Because such branches contain work that isn't merged in yet, trying to delete it will fail.

You can specify the branch name or commit as an argument to ask about the merge state of it:

git branch --merged branch_name
git branch --no-merged branch_name

Rename a branch locally:

git branch --move old_branch_name new_branch_name

Push the branch name change to a remote:

git push --set-upstream origin new_branch_name

List all local and remote branches:

git branch -all

Delete the old branch from a remote:

git push origin --delete old_branch_name

Show branch name in Ubuntu command prompt by modifying the file ~/.bashrc with the following content:

# Add git branch if its present to PS1

parse_git_branch() {
    git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'
}

if [ "$color_prompt" = yes ]; then
    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[01;31m\]$(parse_git_branch)\[\033[00m\]\$ '
else
    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w$(parse_git_branch)\$ '
fi

Push a local branch to the remote origin with the same branch name:

git push origin local_branch_name

Push a local branch to the remote origin that is named differently:

git push origin local_branch_name:remote_branch_name

Tracking Branches

When someone creates a new branch and pushes it to a remote, fetching data from the remote does not create a new local branch. Tracking branches are local branches that have a direct relationship to a remote branch, such that when you run git pull, Git knows which server to fetch and which branch to merge in. You can create a tracking branch by running:

git fetch origin
git switch -c branch_name --track origin/branch_name

If the local branch name is the same as the remote one, you can use a shortcut:

git switch -t origin/branch_name

If the branch name you are trying to switch (a) doesn't exist and (b) exactly matches a name on only one remote, Git will create a tracking branch for you automatically:

git switch branch_name

Change the upstream branch for the local branch that you are tracking:

git branch -u origin/branch_name local_branch_name

List branches with information about tracking branches:

git branch -vv

Fetch from all remotes and list up-to-date information about branches:

git fetch --all; git branch -vv

Branch Naming Best Practice

  1. When working on a new feature, you can name the branch with a prefix like feature/ followed by a short description of the feature or the user story ID:
    • feature/user-authentication
    • feature/us1234-add-login-page
  2. For bug fixes, use a prefix like bugfix/ or fix/ followed by a brief description or the bug ID:
    • bugfix/fix-login-error fix/bug5678-crash-on-startup
  3. For urgent fixes that need to be applied to the production code, use a prefix like hotfix/:
    • hotfix/critical-security-patch
  4. When preparing for a release, you might use a prefix like release/ followed by the version number:
    • release/1.0.0
  5. Use hyphens to separate words in branch names for better readability.
  6. While being descriptive, try to keep branch names concise to avoid overly long names.
  7. Stick to lowercase letters to avoid case sensitivity issues.

Git On the Server

Multi-User Setup

Export an existing repository into a new bare repository:

git clone --bare my_project my_project.git

Put the bare repository on the server:

scp -r my_project.git [email protected]:/srv/git

Other users can clone the repository if they have SSH access:

git clone [email protected]:/srv/git/my_project.git

Let Git add group write permissions to the created bare repository:

ssh [email protected]
cd /srv/git/my_project.git
git init --bare --shared

Single User Setup

On a user's machine, generate a new SSH key if you don't have one:

ssh-keygen -t ed25519 -C "[email protected]"

Start ssh-agent in the background to prevent having to enter the password each time:

eval "$(ssh-agent -s)"

Add SSH private key to the ssh-agent:

ssh-add ~/.ssh/id_ed25519

On a remote machine, create a git user account and a .ssh directory for that user:

sudo adduser git
su git
cd
mkdir .ssh && chmod 700 .ssh
touch .ssh/authorized_keys && chmod 600 .ssh/authorized_keys

Append user's public key:

cat /tmp/id_ed25519.johndoe.pub >> ~/.ssh/authorized_keys

Set up an empty bare repository:

cd /srv/git
mkdir project.git
cd project.git
git init --bare

On a user's machine, initialize a new repository:

cd myproject
git init
git add .
git commit -m 'Initial commit'
git remote add origin [email protected]:/srv/git/project.git
git push origin master

Restrict the shell of the git user account to only Git-related activities:

cat /etc/shells   # see if git-shell is already in there. If not...
which git-shell   # make sure git-shell is installed on your system.
sudo -e /etc/shells  # and add the path to git-shell from last command

Change git user's shell:

sudo chsh git -s $(which git-shell)

Prevent SSH port forwarding by prepending the authorized_keys file with the following options:

no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty

The result should look like this:

no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa
AAAAB3NzaC1yc2EAAAADAQABAAABAQDEwENNMomTboYI+LJieaAY16qiXiH3wuvENhBG...

You can learn more how to further customize git-shell:

git help shell

Distributed Git

Commit Guidelines

Confirm for whitespace errors before your commit:

git diff --check

Make each commit a logically separate unit. Split your work into at least one commit per issue, with a useful message per commit. If some of the changes modify the same file, use git add --patch to partially stage files.

Workflow

Create a new branch and push it to a remote while tracking it:

git switch -c featureA
vim lib/simplegit.rb
git commit -am 'Add limit to log function'
git push -u origin featureA

Create a new branch based on the origin/master branch:

git fetch origin
git checkout -b featureB origin/master
vim lib/simplegit.rb
git commit -am 'Add ls-files'

Push your work from local featureB branch to the remote origin/featureBee branch while tracking the local featureB branch with the remote origin/featureBee branch:

git fetch origin
git merge origin/featureBee
git push -u origin featureB:featureBee

Inspect the work from a collaborator and merge it into your local branch:

git fetch origin
git log featureA..origin/featureA
git checkout featureA
git merge origin/featureA

Make some changes in featureA branch and push the work to the remote:

git commit -am 'Add small tweak to merged content'
git push

Git Tools

Revision Selection

Locate a specific commit you are interested in and refer to it by a partial hash that begins with at least four characters:

git log
git show 1c00

To examine the last commit object on a branch, simple refer to the branch name:

git log
git show topic1

Identify which specific SHA-1 a branch points to:

git rev-parse topic1

Every time your branch is updated, Git stores that information in a temporary history (for a few months), which you can view:

git reflog

View the third prior value of the HEAD of your repository:

git show HEAD@{3}

Show where your master branch was yesterday:

git show master@{yesterday}

View the reflog information formatted like the git log output:

git log -g master

Show a parent of a commit by using a ^ caret:

git show HEAD^

Show the first parent (from the branch you were on when you merged) and the second parent (from the branch that was merged) of a merge commit:

git show ac8b8d3^
git show ac8b8d3^2

Show the first parent of the first parent commit:

git show HEAD~2

Show the second parent of a third-degree ancestor:

git show 12f6403~3^2

Show all commits in experiment branch that are not in master branch:

git log master..experiment

Show all commits that you are about to push to a remote:

git log origin/master..HEAD

If you leave off HEAD, Git will assume HEAD:

git log origin/master..

Show all commits that are reachable from refA or refB, but not from refC:

git log refA refB ^refC

All the following commands are equivalent:

git log refA..refB
git log ^refA refB
git log refB --not refA

Show all commits that are reachable by either of two references but not by both of them with side indication:

git log --left-right master...experiment

Interactive Staging

Enter an interactive shell mode:

git add -i

To start the interactive add mode to do the partial-file staging you can also use either:

git add -p
git add --patch

Start the interactive mode for partially resetting files:

git reset --patch

Partially checking out files:

git checkout --patch

Partially stashing parts of files:

git stash save --patch

Stashing and Cleaning

Push a new stash onto your stack:

git stash push

List the stored stashes:

git stash list

Apply previously pushed stash:

git stash apply

Apply a specific stash:

git stash apply stash@{2}

Apply the stash and stage the changes:

git stash apply --index

Remove a specific stash:

git stash drop stash@{0}

Apply the stash and remove it from your stack:

git stash pop

Save all your work in the stash (including staged), but also leave the staged work in the index:

git stash --keep-index

By default, git stash will stash only modified and staged tracked files. If you specify --include-untracked or -u, Git will include untracked files in the stash:

git stash -u

Stash your work interactively:

git stash --patch

Create a branch from a stash:

git stash branch new_branch_name

Save all the files, including untracked and ignored files in a stash stack:

git stash --all

Remove all untracked files that are not ignored and subdirectories that become empty as a result:

git clean -f -d

Dry run of the previous command with either:

git clean -f -d --dry-run
git clean -f -d -n

Remove all ignored files too:

git clean -d -x

Remove files in an interactive mode:

git clean -x -i

Signing Your Work