Git Version Control Tutorial

Urs Roesch

What is git

Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.

Target audience

The course material is targeted at technically inclined person with a high Unix proficiency and an interest of peaking under the hood bug had very little or no exposure to Git yet.

Scope & Terminology

For most part of this tutorial we stay within the realm of porcelain.

Table 1. Terminology Summary
TermDescription

plumbing

Low-level toolkit commands.

porcelain

User-friendly front end commands.

Concepts

File storage

delta model
Figure 1. Delta based version control model
git storage
Figure 2. Git snapshots / file storage model

Location

Most git commands are performed locally. Compared to the CVCS (Centralizes Version Control Systems) such as Subversion, git is blazingly fast. Git always retains the whole repository with history locally. Only when sending changes to a remote site is network latency coming into play.

Integrity

The 40 character long hex encoded SHA1 checksums are popping up all over when working with git as they are used universally.

SHA1 hash example
d6a96ae3b442218a91512b9e1c57b9578b487a0b

Content

Git only works with files. Directories without a single file are not retained in git. Also files with the same content are only stored once and then referenced.

Stages

StageDescription

committed

The content of the file is safely stored in the repository.

staged

A modified file is marked for inclusion with the next commit.

modified

A file git is aware of has been modified but is not commited to the repository yet.

git stages
Figure 3. Git stages and workflow

Family tree

Understanding the relations between commits is essential for understanding more advanced topics like branching, rebasing and resetting among others. But it also helps with troubleshooting issues.
git family
Figure 4. Git commit relation

Repositories

The repository of every git project is stored in the root of the working directory under the .git directory. This provides a small overview of the content within the directory.

Content

git init tree
Figure 5. Listing of .git directory after initialization
For a more in depth description of the repository layout consult the gitrepository-layout manual page or read it online.

Objects

There are 4 types of git objects stored within the objects directory. Namely:

Commits, Trees, Blobs and Tags

git commit object
Figure 6. Graphic showing all 4 types of git objects

Getting help

Git is very well documented and comes with extensive help accessible from the command line. There are a few ways getting help with a git command.

Commands

To list all available porcelain commands git --help or git help is used.

Man pages

To show more help for the git init command one can either use man git-init, git help init or git init --help. They all open the Unix man page for the topic at hand.

Shell completion

git init --TabTab
--bare               --no-...             --no-template        --quiet
--separate-git-dir=  --shared             --template=

Websites

The comprehensive documentation is also available on the net via the official git website’s documentation section.

Module 1 - Configuration

Goals

  • Show configuration values.

  • Set contact information.

  • Differentiate system, global and local configurations

  • Useful configuration options.

With recent versions of git a base configuration for the user’s email and full name is required.

List git configuration

list

$ git config --list
core.repositoryformatversion=0 (1)
core.filemode=true (2)
core.bare=false (3)
core.logallrefupdates=true (4)

Set contact information

user.name & user.email

$ git config --global --add user.name "Urs Roesch" (1)
$ git config --global --add user.email "****@bun.ch" (2)
$ git config --list
user.email=****@bun.ch
user.name=Urs Roesch
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true

System, global and local?

File location

system

System wide configuration located usually under /etc/gitconfig.

global

User wide configuration located under ${HOME}/.gitconfig.

local

Repository only configuration under .git/config.

How does it work?

$ cat ~/.gitconfig
[user]
        email = ****@bun.ch
        name = Urs Roesch
$ cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
git config no overlap
Figure 7. Combining the global and local configuration with no overlap.
git config overlap
Figure 8. Combination with local overriding user.email.

Useful configuration options

These are basic configuration settings in git but they can make a large difference in the overall user experience.

core.editor

git config --global core.editor emacs (1)
1Now git will use Emacs every time interactive input is required regardless of the systems editor settings.

core.pager

git config --global core.pager '' (1)
1Switches off the pager altogheter.

color.ui

git config --global color.ui false (1)
1Switch off all colored output. The default is auto probing for terminal color support. A value of always send color to the terminal regardless of capabilities.

color.*

$ git config --list color.TabTab
color.advice
color.advice.hint
color.blame.highlightRecent
color.blame.repeatedLines
color.branch
...

credential.<url>

git config --local credential.https://git.bun.ch/repo/foobar.git urs.roesch (1)
1Sets the username of the URL https://git.bun.ch/repo/foobar.git to urs.roesch.

credential.helper

git config --global credential.helper 'cache --timeout=86400' (1)
1Caches the HTTPS credentials for 24 hours.

Summary

Module 2 - Local Repository

Goals

  • Create a new git repository.

  • Create and stage files.

  • Commit files.

  • View the status of the repository.

  • Display the log.

  • Excluding files from Git.

Create a local repository

As already mentioned under location for the most part Git operates on local storage. In order to create our first repository the only thing required is the git binary. Starting with a clean slate the only other requirement is an empty directory.

Git commands always start with git followed by a command, in this case init, then the arguments. To get more information about a git command use git help <command>.

git init

$ mkdir git-repo
$ cd git-repo
$ git init (1)
Initialized empty Git repository in .../git-repo/.git/
Want to know what exactly what git put into the .git directory have a look at the content of the initial tree

git add

$ echo "This is my first file in a git repo" > first-file.txt (1)
$ git status (2)
On branch master (3)

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        first-file.txt

nothing added to commit but untracked files present (use "git add" to track) (4)
$ git add . (5)
# git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage) (6)

        new file:   first-file.txt (7)

Commit files

With files in staging it is time to actually put them into the repository we take now a snapshot of the repository.

git commit

$ git commit -m "My first git commit" (1)
[master (root-commit) 307c1a0] My first git commit (2)
 1 file changed, 1 insertion(+) (3)
 create mode 100644 first-file.txt (4)
$ git status
On branch master
nothing to commit, working tree clean (5)

Inspecting log and objects

After commiting the first file one should know how to display the commit history or log. There is a multitude of options that can be used to customize the output. While showing every option is out of scope a few very useful ones are shown. Additionally the show command used for inspecting objects is briefly discussed.

git log

$ git log
commit 307c1a0a537758f3d4b6ecea98e9af2e5d0b7b88 (HEAD -> master) (1)
Author: Urs Roesch <****@bun.ch> (2)
Date:   Sun Aug 26 12:09:57 2018 +0200 (3)

    My first git commit (4)

With only one commit in the repository there is only a single entry shown. As the repository accumulates commits the number of log entries is also growing.

To show what files are part of the commit the --stat argument is used.

$ git log --stat
commit 307c1a0a537758f3d4b6ecea98e9af2e5d0b7b88 (HEAD -> master)
Author: Urs Roesch <urs++git@bun.ch>
Date:   Sun Aug 26 12:09:57 2018 +0200

    My first git commit

 first-file.txt | 1 + (1)
 1 file changed, 1 insertion(+) (2)

While not really useful at this stage in the project with many commits one can squeeze the log output into a single line.

$ git log --oneline
307c1a0 (HEAD -> master) My first git commit (1)

git show

$ git show
commit 307c1a0a537758f3d4b6ecea98e9af2e5d0b7b88 (HEAD -> master)
Author: Urs Roesch <urs++git@bun.ch>
Date:   Sun Aug 26 12:09:57 2018 +0200

    My first git commit

diff --git a/first-file.txt b/first-file.txt (1)
new file mode 100644
index 0000000..e7e0b37
--- /dev/null
+++ b/first-file.txt
@@ -0,0 +1 @@
+This is my first file in a git repo

The --stat switch does only display statistics and skips the diff output.

$ git show --stat (1)
commit 307c1a0a537758f3d4b6ecea98e9af2e5d0b7b88 (HEAD -> master)
Author: Urs Roesch <urs++git@bun.ch>
Date:   Sun Aug 26 12:09:57 2018 +0200

    My first git commit

 first-file.txt | 1 +
 1 file changed, 1 insertion(+)

Excluding files

Depending on the project there are files that should not be included in the repository as they can be reproduced by a build script. Or temporary editor file that try to sneak into the repository.

.gitignore

$ mkdir tmp
$ echo temporary > tmp/tmp.txt (1)
$ git status --short
?? tmp/ (2)
echo tmp > .gitignore (3)
git status --short
?? .gitignore (4)
While not strictly necessary it usually makes sense to include the .gitignore file in the repository.
$ git add .gitignore  (1)
$ git commit -m "Excluding files with .gitignore" (2)
[master 33d3825] Excluding files with .gitignore
 1 file changed, 1 insertion(+)
 create mode 100644 .gitignore
$ git log --oneline (3)
33d3825 (HEAD -> master) Excluding files with .gitignore
307c1a0 My first git commit

Summary

Module 3 - Remote repositories

Goals

  • Download or clone a remote repository.

  • Add content to the repository.

  • Learn about origin.

  • Push the changes.

  • Pull changes made outside of the working directory.

Create a bare repo

Creating a bare repo is an advanced topic but for the purpose of working with a local shared bare repository it is included. To distinguish bare repositories from local ones the naming convention is to suffix it with .git.

git init

$ git init --shared --bare remote-repo.git (1) (2)
Initialized empty shared Git repository in .../remote-repo.git/

Working with remote repository

To create a local copy of a remote or shared repository the clone command is used.

git clone

$ git clone remote-repo.git (1) (2)
Cloning into 'remote-repo'...
warning: You appear to have cloned an empty repository.
done.

origin

$ git config --list
user.email=urs++git@bun.ch
user.name=Urs Roesch
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
remote.origin.url=.../remote-repo.git (1)
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/* (2)
branch.master.remote=origin (3)
branch.master.merge=refs/heads/master (4)
A clear understanding of how this mechanism works is key to comprehend how git works with shared repositories over the network.
git remote tree
Figure 9. Remote branches in a .git directory highlighted in color.

git push

$ echo 'First remote repository content!' > first_file.txt (1)
$ git add first_file.txt (2)
$ git commit -m 'Initial commit' (3)
[master (root-commit) 45d5290] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 first_file.txt
$ git log --oneline
45d5290 (HEAD -> master) Initial commit
This is the same as for the local repository. As all the actions are done locally there is no difference if you work with a remote repository.

With the commit in place the changes have to be pushed to the remote repository. This is accomplished with git push.

$ git push
Counting objects: 3, done. (1)
Writing objects: 100% (3/3), 250 bytes | 250.00 KiB/s, done. (2)
Total 3 (delta 0), reused 0 (delta 0)
To .../remote-repo.git (3)
 * [new branch]      master -> master (4)

git fetch

$ git fetch
remote: Counting objects: 3, done. (1)
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From .../remote-repo
   45d5290..885f6c7  master     -> origin/master (2)
$ ls
first_file.txt (1)
$ git log --oneline
45d5290 (HEAD -> master) Initial commit (2)
$ git log origin/master (1)
commit 885f6c751abb430cb48c0903fcd82a2d35a77d25 (origin/master) (2)
Author: Urs Roesch <urs++git@bun.ch>
Date:   Thu Sep 6 06:57:50 2018 +0200

    Second commit

commit 45d52900b73a5bd461cbaef2652b9d1ed8220b3b (HEAD -> master) (3)
Author: Urs Roesch <urs++git@bun.ch>
Date:   Thu Sep 6 06:37:46 2018 +0200

    Initial commit

git merge

To bring the changed from the remote master branch into the working directory the merge command is used.

$ git merge origin/master (1)
Updating 45d5290..885f6c7
Fast-forward
 second_file.txt | 1 + (2)
 1 file changed, 1 insertion(+)
 create mode 100644 second_file.txt
$ git log
commit 885f6c751abb430cb48c0903fcd82a2d35a77d25 (HEAD -> master, origin/master) (3)
Author: Urs Roesch <urs++git@bun.ch>
Date:   Thu Sep 6 06:57:50 2018 +0200

    Second commit

commit 45d52900b73a5bd461cbaef2652b9d1ed8220b3b
Author: Urs Roesch <urs++git@bun.ch>
Date:   Thu Sep 6 06:37:46 2018 +0200

    Initial commit

git pull

$ git log origin/master
commit 45d52900b73a5bd461cbaef2652b9d1ed8220b3b (HEAD -> master, origin/master)
Author: Urs Roesch <urs++git@bun.ch>
Date:   Thu Sep 6 06:37:46 2018 +0200

    Initial commit
$ git pull
remote: Counting objects: 3, done. (1)
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From .../remote-repo
   45d5290..885f6c7  master     -> origin/master
Updating 45d5290..885f6c7
Fast-forward (2)
 second_file.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 second_file.txt

Summary

Module 4 - Visualizing and tracking changes

Goals

  • Use of the diff command.

  • Use blame to find who’s done what change.

  • Find strings in the tree with grep.

Display changed content

When working with a VCS one of the most important daily tasks is to evaluate changes made during the course of time. Git provides a variety of tools to get the job done.

git diff

$ echo 'second line in first file' >> first_file.txt (1)
$ cat first_file.txt (2)
First remote repository content!
second line in first file
$ git diff (3)
diff --git a/first_file.txt b/first_file.txt
index 697ffc4..f79ba9f 100644
--- a/first_file.txt
+++ b/first_file.txt
@@ -1 +1,2 @@
 First remote repository content!
+second line in first file

As mentioned basic invocation of git diff only tracks changes in the working tree. To see changes of staged files more options are required.

$ git add first_file.txt (1)
$ git diff (2)
$ git diff --cached (3)
diff --git a/first_file.txt b/first_file.txt
index 697ffc4..f79ba9f 100644
--- a/first_file.txt
+++ b/first_file.txt
@@ -1 +1,2 @@
 First remote repository content!
+second line in first file

Spinning this thread a bit further showing differences in a already commited file requires a reference to a commit.

$ git commit -m "Second line in first file" (1)
[master 3fb25c5] Second line in first file
 1 file changed, 1 insertion(+)
$
urs@automatix:~/var/work/git-tutorial/remote-repo$ git diff HEAD~1 (2)
diff --git a/first_file.txt b/first_file.txt
index 697ffc4..f79ba9f 100644
--- a/first_file.txt
+++ b/first_file.txt
@@ -1 +1,2 @@
 First remote repository content!
+second line in first file

To show only the summary of changes to the file one can use the --stat parameter already shown in previous commands such as log or show.

$ git diff --stat HEAD~2 (1)
 first_file.txt  | 1 +
 second_file.txt | 1 +
 2 files changed, 2 insertions(+)

Where diff really shines tho is showing changes to a single file only.

$ git diff HEAD~2 second_file.txt (1)
diff --git a/second_file.txt b/second_file.txt
new file mode 100644
index 0000000..20d5b67
--- /dev/null
+++ b/second_file.txt
@@ -0,0 +1 @@
+Second file

Find who changed what

Sometimes it is important to know who changed what. For example when questions arise why a change was commited or why something was implemented a certain way. And git has a tool ready for that too.

git blame

$ git blame first-file.txt
^8070030 (Urs Roesch        2018-10-06 13:06:01 +0000  1) First file (1)
2e97d380 (Linux Torvalds    2019-12-25 08:07:28 +0000  2) Second line (2)
e5149955 (Junio C Hamano    2020-01-04 11:08:45 +0000  3) Third line (3)
e5149955 (Junio C Hamano    2020-01-04 11:08:45 +0000  4) Fourth line
e5149955 (Junio C Hamano    2020-01-04 11:08:45 +0000  5) Fifth line
00000000 (Not Committed Yet 2020-10-03 09:10:49 +0000  6) (4)
00000000 (Not Committed Yet 2020-10-03 09:10:49 +0000  7)
00000000 (Not Committed Yet 2020-10-03 09:10:49 +0000  8)
00000000 (Not Committed Yet 2020-10-03 09:10:49 +0000  9)
00000000 (Not Committed Yet 2020-10-03 09:10:49 +0000 10) Apologies, I lost count
As with any git command there is a slew of additional options a few of the more useful ones examined in the next few examples.

When only wanting to review changes starting at a particular revision the SHA1 can be provided followed by two dots.

$ git blame 2e97d380.. -- first-file.txt
^2e97d38 (Linux Torvalds    2019-12-25 08:07:28 +0000  1) First file (1)
^2e97d38 (Linux Torvalds    2019-12-25 08:07:28 +0000  2) Second line
e5149955 (Junio C Hamano    2020-01-04 11:08:45 +0000  3) Third line (2)
e5149955 (Junio C Hamano    2020-01-04 11:08:45 +0000  4) Fourth line
e5149955 (Junio C Hamano    2020-01-04 11:08:45 +0000  5) Fifth line (3)

While the previous example shows changes between a commit and the current HEAD the same can be done between two arbitrary revisions.

$ git blame 80700303..2e97d380 -- first-file.txt
^8070030 (Urs Roesch        2018-10-06 13:06:01 +0000  1) First file (1)
2e97d380 (Linux Torvalds    2019-12-25 08:07:28 +0000  2) Second line (2)

There is also the option to view all changes up to certain revision. This is achieved by two dots followed by the revision hash.

$ git blame ..2e97d380 -- first-file.txt
^8070030 (Urs Roesch        2018-10-06 13:06:01 +0000  1) First file (1)
2e97d380 (Linux Torvalds    2019-12-25 08:07:28 +0000  2) Second line (2)
Instead of the fickle SHA1 hashes one can use tags e.g. git blame v2.2.0.. — first-file.txt or the switch --since e.g. git blame --since=3.weeks — first-file.txt

Another options is for limiting the comparision to a range of lines in the file with the switch -L.

$ git blame -L 2,4 first-file.txt
2e97d380 (Linux Torvalds    2019-12-25 08:07:28 +0000  2) Second line (1)
e5149955 (Junio C Hamano    2020-01-04 11:08:45 +0000  3) Third line
e5149955 (Junio C Hamano    2020-01-04 11:08:45 +0000  4) Fourth line (2)

Locate patterns in files

Locating certain text patterns in a large repository can be daunting. But git grep provides most of the options familiar from the Unix command grep tailored to work under the specifics of a repository.

git grep

$ cat first-file.txt
First file
Second line
Third line
Fourth line
Fifth line

$ git grep First
first-file.txt:First file (1)

There is a way to limit the search to files matching a name pattern, not unlike normal grep. However the syntax is slightly different as wildcards are required to be properly escaped.

$ cp first-file.txt first-file.copy (1)
$ git add first-file.copy (2)

$ git grep First
first-file.txt:First file
first-file.copy:First file (3)

$ git grep First -- '*.txt' (4)
first-file.txt:First file (5)

There is a way to include untracked files in the search by adding the --untracked switch.

$ echo "Second file" > second-file.txt (1)
$ git grep --untracked Second (2)
second-file.txt:Second file (3)
Descending into a sub directory within the git tree limits the search to the files under said sub directory.

Summary

Module 5 - Tags

Goals

  • List tags with tag.

  • Create tags with tag <name>.

  • Remove tags with tag -d.

  • Push tags to remote repositories using push --tags

  • Delete tags in remote repositories.

List tags

Tags have their own git command aptly named tag. Issuing tag without any options list all defined tags. The list of options for tag at least for lightweight and annotated tags is comparatively small.

git tag

$ git tag (1)
v1.0.0 (2)
v1.1.0
git tag 01
Figure 10. Example visualization of revisions, tags and branches in git.

Create tags

Creating tags for the current revision is very straight forward. The only additional option is to provide a name for the tag.

git tag (create)

$ git tag v1.3.0 (1)
git tag 02
Figure 11. Git tree after adding the new new tag v1.3.0 to HEAD.

git tag (create → revision)

$ git tag v1.2.0 64b67952 (1)
git tag 03
Figure 12. Tag 1.2.0 was added to a revision prior to HEAD.

git tag (annotated)

$ git tag -a milestone_1 -m "This is the first milestone" (1)
$ git show milestone_1 (2)
tag milestone_1 (3)
Tagger: Urs Roesch <Urs Roesch>
Date:   Sun Nov 8 17:17:17 2020 +0100

This is our first milestone (4)

commit 45a6088dd900e5363dddbb656360661adb94c1a1 (HEAD -> production, tag: milestone_1) (5)
Author: Urs Roesch <Urs Roesch>
Date:   Sun Nov 8 10:47:36 2020 +0100

    first-file: New milestone reached
git tag 04
Figure 13. Annotated tags point to a tag object which points to the revision.

Push tags

When pushing changes to a remote repository tags are not being transmitted. Some automated workflows rely heavily on tags. In this rather short module the tat

git push

$ git push --tags (1)
Enumerating objects: 9, done. (2)
Counting objects: 100% (9/9), done.
Delta compression using up to 8 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 748 bytes | 748.00 KiB/s, done.
Total 7 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/octocatterpillar/git-tutorial.git
 * [new tag]         milestone_1 -> milestone_1 (3)
There is an options for push called --follow-tags which can be provided during a normal push. Said option will only push annotated tags!

Delete tags

Deleting tags is as easy as creating them. Simply provide the -d switch before the tag name.

There is deletion of a lightweight tag and an annotated tag. Although they look exactly the same on surface the difference is shown in the figure to each command.

Tags deleted locally which have already been pushed to a remote repository will be downloaded again with the next fetch.

git tag (delete)

$ git tag -d v1.1.0 (1)
git tag 05
Figure 14. The git tree after removing tag v1.1.0.

git tag (delete → annotated)

$ git tag -d v1.1.0 (1)
git tag 06
Figure 15. The tag has vanished but the tag object remains.

Summary

Module 6 - Branches

Goals

  • Show branches in repository with branch.

  • Create new branches with checkout -b.

  • Delete branches with branch -d or branch -D

  • Use rebase to keep branches up to date.

  • Rename branches with branch -m.

  • Solve merge conflicts.

Show branches

Per default with git init a branch named master is created. The master branch is just a convenience name pointing to a SHA1 revision.

The master branch can be renamed or deleted as it does not have any relevance to git. It is just an alias.

git branch

$ git branch
* master (1)
git branch 01
Figure 16. Repository with 3 commits and branch master pointing to revision ccc...

Create a new branch

One of the stated goals of the git creator Linus Torvalds was to make branching as cheap and easy as possible. When working with large repositories and many contributors branching is a necessity. But with a good understanding and a bit of practice it quickly become second nature.

git branch

$ git branch testing (1)
$ git branch
* master (2)
  testing
$ git checkout testing (3)
Switch to branch 'testing'
$ git branch
* master (4)
  testing
Branch names can contain the Unix directory separator / for grouping changes e.g. bugfix/ticket-123 or release/v1.2.3.
There is a shortcut for creating and switching to the newly minted branch all at once. The command git checkout -b <branchname> will achieve the same as git branch <branchname> followed by git checkout <branchname>.
git branch 02
Figure 17. After creation of branch master and testing pointing to the same revision.

git add & commit

$ echo 'Branched file' > branched-file.txt (1)
$ git add branched-file.txt
$ git commit -a -m  "First file under branch testing" (2)
[testing 4b9b86d] First file under branch testing
 1 file changed, 1 insertion(+)
 create mode 100644 branched-file.txt
git branch 03
Figure 18. With the new commit the testing pointer moved to the new commit.

Working with multiple branches

For this section the assumption is made that that there is a bug in the current master branch that has not been addressed under testing as it is used to develop new features. To fix the bug a new branch is created called bugfix starting out with the same revision as master.

git checkout

$ git checkout master
Switched to branch 'master'
$ git branch
* master (1)
  testing
Since git version 2.23 the switch command can be used instead of checkout. To create a new branch git switch --create <branch name> is used. The short option for --create is -c. With git switch - one can toggle between current and last used branch. This is not unlike cd - under the Bash shell.
git branch 04
Figure 19. Moving back to master the HEAD is now again at commit ccc…​.

git checkout -b

$ git checkout -b bugfix (1)
Switched to a new branch 'bugfix'
$ git branch
* bugfix (2)
  master
  testing
git branch 05
Figure 20. After checking out bugfix it points to the same revision as master.

git commit

$ git diff
diff --git a/first-file.txt b/first-file.txt
index 4c5fd91..aa24abd 100644
--- a/first-file.txt
+++ b/first-file.txt
@@ -1 +1 @@
-First file
+First file with bugfix (1)
$ git commit -a -m "Bugfix for first file"
[bugfix a27a927] Bugfix for first file
 1 file changed, 1 insertion(+), 1 deletion(-)
git branch 06 mod
Figure 21. With the new commit to bugfix the branches start diverging.

Merging branches

With the bug fix in place the task is to merge it back into the master branch So other users could can use it as well. Assuming the master branch is then pushed to a remote repository that is.

git merge

$ git checkout master (1)
Switched to branch 'master'
$ git branch
* master (2)
  testing
$ git merge bugfix (3)
Updating e303af7..a27a927
Fast-forward
 first-file.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
git branch 07 mod
Figure 22. After the merge bugfix and master point to the same revision.

git branch -d

$ git branch
  bugfix
* master (1)
  testing
$ git branch -d  bugfix (2)
Deleted branch bugfix (was a27a927).
$ git log  --oneline -n 1
a27a927 (HEAD -> master) Bugfix for first file (3)
git branch 08 mod
Figure 23. With the bugfix branch deleted only master and testing remains.

Merging branches

With the bug fix merged into branch master the next logical step is to fold the changes into the testing branch to ensure the next release does include the fixed version. When working with multiple branches this operation is required to not fall back to far with master and preventing lots of merge conflicts.

git rebase

$ git branch (1)
* master
  testing
$ git checkout testing (2)
Switched to branch 'testing'
$ git rebase master (3)
Successfully rebased and updated refs/heads/testing. (4)
Conducting a rebase between two branches requires a common ancestor in the tree.
git branch 09
Figure 24. After the rebase; master and testing are again in sync.

Renaming branches

As learned earlier branches are just arbitrary names pointing to a revision within the git tree. As such renaming is a very painless and fast operation. No files have to be copied just the reference requires an update.

git branch -m

$ git branch (1)
* master
  testing
$ git checkout testing (2)
Switched to branch 'testing'
$ git branch -m production (3)
$ git branch
  master
* production (4)
git branch 10
Figure 25. Structurally there is no change other then the branch name now being production.

Resolving merge conflicts

A merge conflict is a situation where the same file has been modified by one or more person at the same location in the file. Generally git does an excellent job working around merge conflicts. But occasionally during merge or rebase operations git interrupts and requires human intervention to solve a conflict between two revisions.

As a general rule many merge conflicts can be prevented or minimized by:

  • Communicating changes between team members regularly.

  • Regular rebases with the merge target branch.

  • Creating small and atomic commits.

git commit (conflict)

Edit file first-file.txt under branch master.
$ git checkout master (1)
Switched to branch 'master'
$ vi first-file.txt (2)
$ cat first-file.txt
First file with bugfix from branch "master" (3)
$ git commit -m "first-file: Add from 'master'" first-file.txt  (4)
[master 64b6795] first-file: Add from 'master'
 1 file changed, 1 insertion(+), 1 deletion(-)
Edit file first-file.txt under branch production.
$ git checkout production (1)
Switched to branch 'production'
$ vi first-file.txt (2)
$ cat first-file.txt (3)
First file with bugfix from branch "production"
$ git commit -m "first-file: Add from 'production'" first-file.txt (4)
[production bdfdc4a] first-file: Add from 'production'
 1 file changed, 1 insertion(+), 1 deletion(-)
git branch 11 mod
Figure 26. There are now new commits in both master and production.

git rebase (conflict)

$ git rebase master
Auto-merging first-file.txt (1)
CONFLICT (content): Merge conflict in first-file.txt (2)
error: could not apply bdfdc4a... first-file: Add from 'production'
Resolve all conflicts manually, mark them as resolved with (3)
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply bdfdc4a... first-file: Add from 'production'
Inspecting the current environment
$ git branch (1)
* (no branch, rebasing production) (2)
  master
  production
Inspecting the file with the merge conflict.
$ cat first-file.txt (1)
<<<<<<< HEAD (2)
First file with bug fix from branch "master" (3)
======= (4)
First file with bug fix from branch "production" (5)
>>>>>>> bdfdc4a... first-file: Add from 'production' (6)

Manually edit file (conflict)

$ vi first-file.txt (1)
$ cat first-file.txt
First file with bugfix from branch "master" and from "production" (2)
$ git add first-file.txt (3)
$ git rebase --continue (4)
(5)
[detached HEAD 7c857af] first-file: Add from 'master' and 'production'
 1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/production.
$ git branch
  master
* production (6)
git branch 12
Figure 27. There is an orphan commit ddd…​ and a new one eee…​ with parent ccc…​.

git checkout --theirs (conflict)

$ git checkout --theirs -- first-file.txt (1)
$ cat first-file.txt
First file with bugfix from branch "production" (2)
$ git add first-file.txt (3)
$ git rebase --continue (4)
(5)
[detached HEAD 45a6088] first-file: Add from 'production'
 1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/production.
$ git branch
  master
* production (6)
git branch 12
Figure 28. There is an orphan commit ddd…​ and a new one eee…​ with parent ccc…​.

git checkout --ours (conflict)

$ git checkout --ours -- first-file.txt (1)
$ cat first-file.txt
First file with bugfix from branch "master" (2)
$ git add first-file.txt (3)
$ git rebase --continue (4)
Successfully rebased and updated refs/heads/production.
$ git branch
  master
* production (5)
git branch 13
Figure 29. Both branches master and production point to ccc…​ and ddd…​ became an orphan.

Summary

Module 7 - Customizing git commands

Goals

  • Create command short cuts.

  • Deleting aliases.

  • Configure aliases for often used commands.

  • Solve complex recurring workflows with custom git scripts.

Command shortcuts

For some the git commands such as checkout are too cumbersome to type each time what if one could shorten that to say co like on subversion.

git config alias

$ git config --global alias.co checkout (1)
$ git config --global alias.ci commit
$ git config --global alias.br branch
$ git config --global alias.st status

$ git st --short (2)
A  second-file.txt

While simple alias shortcuts are certainly useful one can also create aliases containing some of the rather long options.

$ git config --global alias.lol  'log --oneline --no-decorate' (1)
$ git config --global alias.top 'log -n 3 HEAD' (2)

$ git lol
49b7a9c The rest (3)
e514995 Third line
2e97d38 Second line
8070030 First commit

To modify an alias one can simply use the same command as when creating it.

$ git config --global alias.top 'log -n 1 HEAD' (1)

$ git top
commit 49b7a9cf2a8f1ae6ba94141268716ba0b07949d6 (HEAD -> master)
Author: Urs Roesch <github@bun.ch>
Date:   Tue Nov 3 05:23:49 2020 +0000

    The rest

To remove an alias the switch --unset is used.

$ git config --global --unset alias.top (1)
$ git config --global --get-regex 'alias.*' (2)
alias.st=status
alias.lol=log --oneline --no-decorate
alias.co=checkout
alias.ci=commit
alias.br=branch

Complex aliases

Aliases in git can be more than just shortcuts for overly lengthy commands. One can cram multiple commands into a single alias. This can be done by prefixing by starting the command with an exclamation mark !.

git config alias

$ git config --global alias.sync-upstream \
  '!sh -x -c "git fetch upstream && git rebase upstream/master master"' (1)

$ git sync-upstream
+ git fetch upstream (2)
From https://github.com/sample/repository (3)
   41cc734..1bd94bb  master     -> upstream/master
 * [new tag]         v1.60      -> v1.60
+ git rebase upstream/master master (4)
Current branch master is up to date. (5)

While the above example is fairly sophisticate there is no way to pass parameters to the alias. With the next sample a list of files is passed to the vi editor and when finished editing the files are added to staging area.

$ git config --global alias.vi '!sh -x -c "vi \"$@\" && git add \"$@\""' (1)

$ git vi second-file.txt
+ vi second-file.txt (2)
+ git add second-file.txt

The sky is the limit! If a difficult operation can be put into a simple alias go for it.

Custom commands

For some task even a complex alias is not cutting it! For such instances there is the option of creating a custom command. Any programming language available on the system can be used to do so. To integrate the newly minted command into git the script must be named git-<command> and be placed in a directrory included in $PATH.

git opush (bash script)

#!/usr/bin/env bash

# -----------------------------------------------------------------------------
# Small script to push upstream without a fuss
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# Setup
# -----------------------------------------------------------------------------
set -o errexit
set -o nounset
set -o pipefail

# check bash version compatiblity requires 4.2 or better
shopt -u compat41 2>/dev/null || {
  echo -n "\nBash Version 4.2 or higher is required!\n";
  exit 127;
}


# -----------------------------------------------------------------------------
# Globals
# -----------------------------------------------------------------------------
declare -r SCRIPT=${0##*/}
declare -r VERSION=0.3.1
declare -r AUTHOR="Urs Roesch <github@bun.ch>"
declare -r LICENSE="GPLv2"
declare -g FORCE=""
declare -g REMOVE=""

# -----------------------------------------------------------------------------
# Functions
# -----------------------------------------------------------------------------
function usage() {
  local exit_code=${1:-1}
  cat <<USAGE

  Usage:
    ${SCRIPT//-/ } [options]

  Opttions:
    -h | --help    This message
    -f | --force   Force a push to upstream
    -r | --remove  Remove the repository from upstream
    -V | --version Display version and exit

  Description:
    Origin push to upstream without a fuss. Exludes pushes to master.

USAGE

  exit ${exit_code}
}

# -----------------------------------------------------------------------------

function parse_options() {
  while [[ ${#} -gt 0 ]]; do
    case ${1} in
    -h|--help)    usage 0;;
    -f|--force)   FORCE="true";;
    -r|--remove)  REMOVE="true";;
    -V|--version) version;;
    -*)           usage 1;;
    esac
    shift
  done
}

# -----------------------------------------------------------------------------

function version() {
  printf "%s v%s\nCopyright (c) %s\nLicense - %s\n" \
    "${SCRIPT}" "${VERSION}" "${AUTHOR}" "${LICENSE}"
  exit 0
}

# -----------------------------------------------------------------------------

function current_branch() {
  git rev-parse --abbrev-ref HEAD
}

# -----------------------------------------------------------------------------

function push_origin() {
  local branch=$(current_branch)
  if [[ ${branch} == master ]]; then
    echo "Not pushing master!"
    exit 1
  fi
  git push ${FORCE:+-f} origin ${REMOVE:+:}${branch}
}

# -----------------------------------------------------------------------------

function push_tags() {
  git push --tags
}

# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------
parse_options "${@}"
push_origin
push_tags

Placing the script git-opush under ${HOME}/bin wich is in the path one can execute with git opush. In below case the command is invoked while under branch master.

$ git opush
Not pushing master!

Summary