Git For Teams

Git explained with a team-based approach

Created by Charles Sarrazin / @csarrazi

Atomic commits

What are we talking about ?

One commit should have a reduced perimeter.

A commit should contain a single feature, or sub-feature.

At all times, the application should be in a consistent state.

All commits should leave the application in a working state. Thus, you should not leave WIP commits on a given branch or PR.

The tools of the trade

You need to master add and reset, but also diff, show and of course, commit.

Enabling the detailed status

Check the details of all new untracked files.


$ git status
...
Untracked files:
  (use "git add ..." to include in what will be committed)

  vendor/
...
            

$ git config --global status.showUntrackedFiles all
$ git status
...
Untracked files:
  (use "git add ..." to include in what will be committed)

  vendor/scripts/bootstrap.min.js
  vendor/scripts/jquery.min.js
  vendor/scripts/underscore.js
...
            

Understanding the stage

The stage, or index: what is validated to be committed.

Lets the user customize precisely what he wishes to commit.

git add pathspec... = take a snapshot of pathspec and add this in the next commit's code.

Synonym: git stage. Nothing in common with svn add.

Mandatory to version a new file (untracked), unlike a file which is already versioned

Checking the stage

Diff version


$ git diff --staged
diff --git c/index.html i/index.html
index 5237399..85e642f 100644
--- c/index.html
+++ i/index.html
@@ -1,5 +1,5 @@
 <!doctype html>
-<html>
+<html lang="fr">

            

Snapshot version (whole file)


$ git show :0:pathspec
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>Git ProTips</title>
...
            

Cleanup the diff

Most of the time, we don't care about whitespaces.


$ git diff
...
 <!doctype html>
-<html>
+<html lang="fr">
 <head>
-  <meta charset="utf-8">
-  <title>Git ProTips</title>
+    <meta charset="utf-8">
+    <title>Git  ProTips</title>
...

$ git diff -w
...
 <!doctype html>
-<html>
+<html lang="fr">
 <head>
...
            

Refining the diff

Line by line may be not enough, and not granular enough...


$ git diff --word-diff-regex=.
...
<!doctype html>
<html{+ lang="fr"+}>
...

$ git diff --color-words=.
...
<!doctype html>
<html lang="fr">
...
            

To configure this once and for all:


$ git config --global diff.wordRegex .
$ git diff --word-diff
...
<!doctype html>
<html{+ lang="fr"+}>
...
            

File fragments

Who said we needed to stage the whole file at a time?


$ git add -p index.html
...
 <!doctype html>
-<html>
+<html lang="fr">
 <head>
...
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y
...
   <h1>Git ProTips</h1>
+  <footer>© 2014 Ma Boîte</footer>
...
Stage this hunk [y,n,q,a,d,/,K,g,e,?]?  n
            

This is a critical feature to know, because in real-life, we always have 2 or 3 different things going on in the same file...

Unstaging files

To remove a file (or all files) from the stage, git reset.


$ git reset index.html
Unstaged changes after reset:
M index.html
            

Canceling the whole stage:


$ git reset
Unstaged changes after reset:
M index.html
            

Partial unstaging

Just like you can add files, you can unstage fragments.


$ git reset -p index.html
...
...
 <!doctype html>
-<html>
+<html lang="fr">
 <head>
...
Unstage this hunk [y,n,q,a,d,/,j,J,g,e,?]? n
...
   <h1>Git ProTips</h1>
+  <footer>© 2014 Ma Boîte</footer>
...
Unstage this hunk [y,n,q,a,d,/,K,g,e,?]?  y
            

Renaming and removing

git add . is not enough before Git 2.0: Only the working directory is taken into account, not removed files.


Changes not staged for commit:
...
  deleted:    index.html

Untracked files:
...
  home.html
            

To handle both known and untracked files, you should use -A (--all):


$ git add -A && git status
On branch master
Changes to be committed:
...
  renamed:    index.html -> home.html
            

Amending

the latest commit

git commit --amend replaces the current state.

Did you forget to add a dependency?


$ git add vendor/scripts/underscore.min.js
$ git commit --amend --no-edit
            

Versioned a sensitive file?


$ git rm --cached config/database.yml
$ echo config/database.yml >> .gitignore && git add .gitignore
$ git commit --amend --no-edit
            

Made a typo in the message?


$ git commit --amend -m 'The message without typos'
            

Visualizing a commit or a snapshot

git show [object] displays a commit (by default HEAD), a tree, a snapshot...


$ git show # ou explicitement : git show HEAD
commit 8a5a383
Author: Charles Sarrazin <charles@sarraz.in>
Date:   Sun Oct 26 15:04:17 2014 +0100

    First index

diff --git a/index.html b/index.html
...
            

The content of app/initialize.js on the legacy branch?


$ git show legacy:app/initialize.js
'use strict';
...
            

Correctly using the stash

Saving untracked files, and adding a useful message:


(master *+%) $ git stash save -u 'BS3 migration'
Saved working directory and index state On master: BS3 migration
HEAD is now at 8a5a383 Asynchronous GA Trackers
(master $) $
            

In order to let your prompt remind you that you have some stash, don't forget to activate the GIT_PS1_SHOWSTASHSTATE environment variable, which will add a $.

To fetch the stash, do not use apply, prefer pop:


(master $) $ git stash pop --index
...
(master *+%) $
            

pop actually tries to apply the stash and, if it works, follows up with a drop. There's nothing worse than leaving a stash which was already reintegrated.

Even if the shash saves the stage by default, the stash does not restore it by default, in order to prevent automatic merges in the stage. So please always try first with --index.

Using the logs

Using a relevant format

By default, the log is too verbose, without graph, etc.


$ git config --global alias.lg "log --graph \
  --pretty=tformat:'%Cred%h%Creset -%C(auto)%d%Creset %s \
  %Cgreen(%an %ar)%Creset'"
            
Display example for git lg

Filtering and limiting

--grep on complete messages

--author on the name / email of the author

-- pathspec... on paths (directories, files)

-n limits the number of lines after filtering


$ git lg --author=Beauchamp --grep '^Merge' -n10 -- app/Resources/sass
						
Simple log filtering

Checking for divergence

Want to see the commits between two branches / tags? (in short, get back to their common ancestor) Use A...B.

The full graph

$ git lg master..bootstrap3-to-rebase
									
A bad listed graph

$ git lg master...bootstrap3-to-rebase
									
The listing limited to the divergence

Finding the culprit

Instead of using git blame in order to find the culprit of a certain commit, you should search on the active contents of the diffs: results will be more precise and relevant. Also, it works for deleted lines.

Options -S (inserts OR deletions) and -G (all the diffs)


$ git lg -S "Donec sed" -1 -p index.html
...
* 6d731f3 - Content tweaks (Charles Sarrazin 1 year, 1 month ago)
|
| diff --git a/index.html b/index.html...
...
| @@ -60,18 +60,18 @@
...
| <h2>Three-point rebasing</h2>
| - <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa.</p>
| + <p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa.</p>
| <p><a class="btn" href="#">View details »</a></p>
...
            

Fragments & functions

Since Git 1.8.4 (April 2013), it is also possible to filter by file fragment, and by function body. You can use either an interval(-L 1,100:index.html), or a regex:


$ git lg -L :getCheckIns:app/lib/persistence.js
* 12164bc - Refactoring gestion Check-In Details, et gestion corner-cases (Charles Sarrazin 2 days ago)
|
| diff --git a/spa-final/app/lib/persistence.js b/spa-final/app/lib/persistence.js
| --- a/spa-final/app/lib/persistence.js
| +++ b/spa-final/app/lib/persistence.js
| @@ -81,4 +86,7 @@
|  function getCheckIns() {
| -  return collection.toJSON();
| +  return collection.map(modelWithCid);
|  }
|
* d714350 - Initial import (Charles Sarrazin 1 year ago)

  diff --git a/app/lib/persistence.js b/app/lib/persistence.js
  --- /dev/null
  +++ b/app/lib/persistence.js
  @@ -0,0 +34,4 @@
  +function getCheckIns() {
  +  return collection.toJSON();
  +}
            

Choosing pushes

And not pushing that frequently

You should only push your current branch

By default, when running git push only, Git will try:

Before 2.0: all tracked branches with the same name

After 2.0: the current branch if the tracked* branch has the same name**

What we want: the current, regardless of the remote name**


$ git config --global push.default upstream
            
* push.default = matching — same name = indentical remote and local names
** push.default = simple

Initial push

The first time you want to push a branch that you wish to track later, don't forget to track the upstream:


(stats-v3) $ git push -u origin stats
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 488 bytes | 0 bytes/s, done.
Total 5 (delta 0), reused 0 (delta 0)
To git@github.com:tdd/private-tests.git
 * [new branch]      stats -> stats
Branch stats set up to track remote branch stats from origin.
            

Cleaning up before pushing

Pre-push habit: cleanup your local history, which should be more or less dirty.


$ git lg @{u}..
$ git rebase -i @{u}
            

The interactive rebase helps us cleanup our work before sharing it with the rest of the team.

Yet another reason not to push too often. You should commit often (10-30 times a day), but push more rarely (2-3 times/day).

Pull ≠ Merge!

By default, git pull finishes with a merge. This is stupid.

When you pull, you do not actually merge a remote branch locally: you update your branch with the latest changes.

Also, this makes your graph less readable:

Dirty graph

Pull = Rebase

A pull should rather Replay your local work on the remote branch: by definition, a rebase.

However, you should make sure not to inline a merge in the local copy by mistake

In order to prevent this:


$ git config --global pull.rebase preserve
            

Working with branches

Creating a new branch

Most of the time, when you create a branch, it is to work on the branch directly.


(master) $ git branch feature
(master) $ git checkout feature
(feature) $
            

(master) $ git checkout -b feature
(feature) $
            

Note that if you wish to collaborate on a branch already existing on the remote, a simple checkout is enough.


(master) $ git branch -a
* master
  origin/master
  origin/topic
(master) $ git checkout topic
Branch topic set up to track remote branch topic from origin.
Switched to a new branch 'topic'
(topic) $
            

Better visualization of branches

Two options are useful for git branch:

-a lists all local and remote branches.

-vv adds the first commit lines and, for the tracked branch, the state of the tracking.


(2014-octobre u+1) $ git branch -avv
* 2014-octobre                 abaca0f [origin/2014-october: ahead 1] Removing old demos
  legacy                       41b5bf7 [origin/legacy] Bash packaging script + debrief Zip file
  master                       0208acb [origin/master] Fix .groc.json
  v2014                        521350a [origin/v2014: behind 2] Backport Backbone plugins target link towards backplug.io
  v2015                        27b1791 [origin/v2015] Updated docblocks
  remotes/origin/2014-october  10ad1b1 Updated annotated source code
  remotes/origin/bs3           49bc984 In-session tweaks
  remotes/origin/bs3-basis     650f025 Tweak export connectivity
...
            

Which branch contains what?

Use case: commit 73abc4f is the source of the bug, and you want to know where you need to propagate the fix.


(master) $ git branch -a --contains 73abc4f
* master
  stats
  origin/master
  origin/3-2-stable
  origin/stats
            

Use case: Are there any branches not yet merged in master?


(master) $ git branch -a --no-merged
  stats
  origin/fix/143
  origin/fix/148
  origin/stats
  origin/max/experiment-web-audio
            

Merging

You should only merge when you need to identify that a feature has been integrated in the target branch (bugfix, feature, story, etc.).

If the branch is a child, prevent fast-forwards:


(master) $ git merge --no-ff fix/143
            

Want to details the merged commits?


(master) $ git merge --log stats
            

Or, if you want to do it all the time:


(master) $ git config --global merge.log true
            

Using the dash

Very often, when you merge, rebase, cherry-pick or simply checkout, this is made from/to the previous branch (the one you were on earlier).

Just as you can use cd - in your shell to go back to the previous directory, you can use - to go back to the previous branch. This is a shortcut for the classic @{-1}.


(master) $ git rebase master experiment
(experiment) $ git checkout -
(master) $
            

(fix/148) $ git checkout 3-2-stable
(3-2-stable) $ git merge -
(3-2-stable) $ git checkout master
(master) $ git cherry-pick -
(master) $
            

So, you worked on the wrong branch...

Use case: You thought that your feature would have taken 1 commit, and 10 minutes...

However, 3 commits and 2 hours later, here you are, and there's still work to do!

You should have made a branch...


  (master) $ git branch fix/158
  (master) $ git reset --soft HEAD~3
(master +) $ git checkout fix/158
 (fix/158) $
            

Cherry-picking

Fetching a unique commit, without its history.


(master) $ git cherry-pick 3-2-stable
            

git cherry will help you choose candidates:


(master) $ git cherry -v HEAD topic
+ 3abb73d7cc8d8655f8b99816fed56c6030c28551 /img/ -> /images/
- ba05b8d03de5540181af34a10f1b07debb0ea5fc Stats JS
+ 363f53d53d78384f29dc68d900b04ac0b56d20f6 Nav stats
            

Or, using git log --cherry (preferred):


(master) $ git lg --cherry HEAD...topic
* 363f53d - (topic) Nav stats (Charles Sarrazin 1 year, 1 month ago)
= ba05b8d - Stats JS (Charles Sarrazin 1 year, 1 month ago)
* 3abb73d - /img/ -> /images/ (Charles Sarrazin 1 year, 1 month ago)
            

Handling conflicts

Most conflicts are simple to handle.

You just need the correct methodology:

  1. git status as soon as possible to check what is wrong
  2. Examine the first conflicting file (IDE/text editor or git mergetool)
  3. Choosing between the two versions of the file*
  4. git add to mark the conflict as resolved.
  5. If there are remaining conflicts, go back to (2).
  6. Otherwise, finalize using git commit or git rebase --continue
* This is simple most of the time, but may sometimes require some more information

Anatomy of a conflict

By default, merge style:


<<<<<<< HEAD
SVN est un outil de gestion de source largement répandu
et extrêmement pratique.
=======
SVN est un outil de gestion de source largement répandu
malgré sa profonde stupidité et la plaie de son usage.
>>>>>>> truth
            

It is often useful to see the pre-conflict version:


<<<<<<< HEAD
SVN est un outil de gestion de source largement répandu
et extrêmement pratique.
||||||| merged common ancestors
SVN est un outil de gestion de source largement répandu.
=======
SVN est un outil de gestion de source largement répandu
malgré sa profonde stupidité et la plaie de son usage.
>>>>>>> truth
            

$ git config --global merge.conflictStyle diff3
            

Examining snapshots

Sometimes, using the diff is not enough, especially when the conflicting code references other parts, which could sucessfully be merged.

In this case, you can examine the snapshots from the recipient-side:


(master *) $ git show :2:intro.md
# ou : git show HEAD:intro.md
# ou : git show master:intro.md
            

Or from the merged code side:


(master *) $ git show :3:intro.md
# ou : git show MERGE_HEAD:intro.md
# ou : git show truth:intro.md
            

Or from the common ancestor (before the divergence):


(master *) $ git show :1:intro.md
            

The merge log

Very rarely, even snapshots may not be enough to understand the problem, and you may want to retrace the changes until the head of the branches.

This can be done using the already seen A...B syntax, but as during a merge, Git has access to all the context, a simple --merge is enough:


(master *) $ git lg --merge -p intro.md
* 9d8dafd - (truth) Truth (Charles Sarrazin 12 minutes ago)
...and here the diff...
* f068b20 - (HEAD, master) Disinfo (Charles Sarrazin 12 minutes ago)
...and here the diff...
(master *) $
            

Giving up

If you really don't have enough information to resolve the conflict, you can cancel the conflict and return to a clean state:


(master *) $ git merge --abort
(master) $
            

Or, when in a rebase:


(master *) $ git rebase --abort
(master) $
            

The end

You can find the slides on github