Log in

Git Introduction

On this page, I try to describe some key git workflows, utilizing SmartGit. It is written in an imperative style, but passively following the descriptions, commands and screenshots should be fairly clear. The headings are for quick navigation, but the guide assumes a linear reading.

For each command, I show how to execute it via menus, via keyboard shortcuts (on Manjaro Linux), and the git command line that SmartGit executed as a result. This should allow you to replicate the workflows in the tool of your choice. I am particularly fond of SmartGit's merging support, you may find that other tools are different in that regard. I will also describe merging conflicts without tool support.

I will not go into patches, as I haven't used those at all. Except for initially migrating to git, using patches should not be necessary at all.

Contents

Example Setup

First of all, I have created a repository forge.git, and cloned it as forge-a. The repository contains an empty .gitignore file, and our_program:

Welcome to Git!
Imagine this file contains a sensible program.

Feature 1
- this is a feature

Feature 2
- this is another feature

Git-tut-1-project.png

The forge-a repository is supposed to represent the clone of another developer with push access to the repository.

Cloning a repository

Now, "you" clone your own copy of the repository.

  • Menu: Repository/Clone...
  • Shortcut: Ctrl+Shift+O

In this particular case, I will clone the "Local Git repository" in /data/tmp/git_tut/forge.git into /data/tmp/git_tut/forge-b, but that is of course specific to the example.

  • $ git clone -v --progress --branch master /data/tmp/git_tut/forge.git /data/tmp/git_tut/forge-b

The commands SmartGit executes are often more verbose than necessary; I will still put the full commands here so that I don't remove important parts by mistake. Just keep in mind that if you think the command is unnecessarily specific, it probably is! Also, except for clone commands, all commands are run from the repository directory.

A clone is simply a new repository that references its origin; it is not more or less a repository than what you cloned. This is in contrast to centralized code versioning systems like SVN, where you are strictly the client to a single repository server.

Inspecting History

Now that you have your own clone, let's look at the history: select repository forge-b, then

  • Menu: Query/Log
  • Context Menu "forge-b": Log
  • Toolbar: Log
  • Shortcut: Ctrl+L
  • (similar) $ git log

Git-tut-2-log.png

In particular, the log shows that the master branch is up-to-date with origin, and that master is the current branch, denoted as (origin[>master).

Creating branches

Let's leave the log for now and improve feature 1. To do this, we create a feature branch. Make sure that forge-b is selected, then

  • Menu: Branch/Add Branch...
  • Contect Menu "Local Branches": Add Branch...
  • Shortcut: F7

Branch name is feature-1-patch, then "Add Branch & Checkout"

  • $ git branch --no-track feature-1-patch
  • $ git checkout feature-1-patch

Now we edit Feature 1 to "this is the improved feature". Let's also add a change we don't want to commit yet, as that happens sometimes. In the end, the program looks like this:

Welcome to Git!
Imagine this file contains a sensible program.
Some description, but we're not sure whether to commit this...

Feature 1
- this is the improved feature

Feature 2
- this is another feature

Git-tut-3-changes.png

The Index, staging, and committing

To commit only some changes, we need to stage exactly these. The Index Editor will help us here. Select the changed file, then

  • Menu: Local/Index Editor
  • Context Menu file: Index Editor
  • Toolbar: Index Editor
  • Shortcut Ctrl+Alt+T

Using the arrows, I put the Feature 1 change on the index, then save.

Git-tut-4-index-editor.png

  • $ git update-index --cacheinfo 100644 a71fab16d4bd5d79f9e547b194773d7261d1d43c our_program

As you can see, the git command contains hashes and probably references a temporary file somewhere, so it's hard to reproduce that on the command line; I still show the Index Editor as it neatly visualizes an important concept of Git: HEAD vs. Index vs. Working Tree

  • HEAD is what's already committed into the repository
  • The Index are changes ready for commit. It will be important especially for merging.
  • The Working Tree is just the file system

Now let's commit the staged changes ("staged" means "on the index")

  • Menu: Local/Commit...
  • Context Menu "forge-b" or file: Commit...
  • Toolbar: Commit
  • Shortcut: Ctrl+K

Smartgit lets you choose "Staged Changes" (the whole index) or "Local Changes" (specific changed files on the working tree). Let's keep with staged changes, enter "improve feature 1" as the message, and commit.

  • $ git commit --cleanup=whitespace --file=/tmp/smartgit-360896725199907360tmp/commit-1151560046432673715.tmp

(SmartGit gives the commit message as a temporary file)

Now we decide to commit the other change as well. As this is the only change to the file, we stage it as a whole. Select the file, then

  • Menu: Local/Stage
  • Context Menu: Stage
  • Toolbar: Stage
  • Shortcut: Ctrl+T
  • $ git add --force -- our_program

and commit again.

More on branches

Let's take a look at the log. In SmartGit, be sure that you select the repo, otherwise you'll only see commits that changed the selected file.

Git-tut-5-log.png

(origin[master) is still there, but now we have (>feature-1-patch).

It's time to say what a branch really is: just a label! The history in a Git repository is a graph of commits, identified by a hash, and each commit may have labels attached to it. Having a branch checked out means that the file system mirrors the state of the commit referenced by the branch. There are also special branches that correspond to branches in other repositories: the (origin[master) is shorthand for two branches origin/master and master. You can't checkout or modify origin/master directly; this is only done by interacting with the remote repository.

It's clear that feature-1-patch is local, and has no origin equivalent.

Now, let's create a conflict by going back to forge-a. Change our_program to this:

Welcome to Git!
Imagine this file contains a sensible program.

Feature 1
- this is part 1 of the feature
- this is part 2 of the feature

Feature 2
- this is another feature

Let's commit this with message "change feature 1", but without staging for a change. The commands are:

  • $ git add --force -- our_program
  • $ git commit --file=/tmp/smartgit-360896725199907360tmp/commit-1142649907362181323.tmp -o -- our_program

As you can see SmartGit stages for us, but also lists explicitly which files to commit.

Pushing commits

Looking at forge-a history, you see some new stuff:

Git-tut-6-log.png

There's [>master 1>) and (origin/master[ separately, denoting that the local master branch is different than the origin's. The puzzle-like connection tells us that master "tracks" the corresponding origin branch.

Let's push the new commit. Either from the log or in the project overview, do

  • Menu: Remote/Push...
  • Context Menu "master": Push 'master'...
  • Context Menu "forge-a": Push...
  • (probably some other context menus)
  • Toolbar: Push
  • Shortcut: Ctrl+U
  • $ git push --porcelain --progress --recurse-submodules=check origin refs/heads/master:refs/heads/master

If you haven't noticed, this is the first time since cloning forge-b that we interacted with the origin repository forge.git! Everything else so far was local, which means it was fast and we can do it regardless of internet access or permissions. Distributed version control systems let you track changes independently of the choice if, when, and how to share these changes.

Pulling commits

Back in forge-b, let's pull:

  • Menu: Remote/Pull...
  • Context Menu "forge-b": Pull...
  • etc.
  • Toolbar: Pull
  • Shortcut: Ctrl+P

The dialog shows "Fetch Only", because our current branch (feature-1-patch) does not exist remotely. A full "pull" is a combination and fetch and merge/rebase.

  • $ git fetch --progress --prune --recurse-submodules=no origin

In the log, to the left make "origin" visible to see everything that's in the repository:

Git-tut-7-log-fork.png

Forks in the history

We have a fork in the history; let's resolve that by two strategies: Merge and Rebase. First, we create a new branch to be able to do both strategies. Select the HEAD commit "describe improvements", and add branch feature-1-patch-copy (e.g. using F7 or the toolbar).

  • $ git branch --no-track feature-1-patch-copy edc514e84680ad14779e20f276fca671ce409c41

Merging branches

Now, try to merge feature-1-branch (which should still be HEAD) with origin/master. Select the origin/master branch's commit, then

  • Menu: Branch/Merge...
  • Context Menu: Merge...
  • etc.
  • Shortcut: Ctrl+M

Select "Create Merge-Commit", but we expect a conflict anyways, so it makes no difference.

  • $ git merge --no-ff origin/master

Resolving conflicts

We got a conflict! What does the file system look like?

Welcome to Git!
Imagine this file contains a sensible program.
Some description, but we're not sure whether to commit this...

Feature 1
<<<<<<< HEAD
- this is the improved feature
=======
- this is part 1 of the feature
- this is part 2 of the feature
>>>>>>> origin/master

Feature 2
- this is another feature

The <<<< ... ==== .... >>>> separates the conflicting versions on HEAD (what we had before attempting to merge) and origin/master (what we're trying to merge with). Also, the "Some description" line is not showing a conflict, though it did change in one of the file's versions. Nice!

What does merging look like in SmartGit? Step out of the log and double-click on the conflicting file:

Git-tut-8-conflict-solver.png

A three-way merge screen like the Index Editor. Of course, we don't just want to take one side, but intelligently decide how to integrate both versions:

Welcome to Git!
Imagine this file contains a sensible program.
Some description, but we're not sure whether to commit this...

Feature 1
- this is the improved part 1 of the feature
- this is part 2 of the feature

Feature 2
- this is another feature

You can enter this in the middle of the Conflict Solver, or put it in the file (note that the middle editor is labelled "Working Tree"). Save and close, and SmartGit will tell you to stage changes to mark conflicts as resolved. Do so.

  • $ git add --force -- our_program

Now commit to finish the merge. In the log, the result looks like this:

Git-tut-9-log.png

Rebasing branches

For the Rebase strategy, check out the feature-1-patch-copy branch, e.g. by double-clicking it.

  • $ git checkout feature-1-patch-copy

Select the origin/master branch's commit, then

  • Menu: Branch/Rebase HEAD (...) to...
  • Context Menu: Rebase HEAD (...) to...
  • etc.
  • Shortcut: Ctrl+D
  • $ git rebase 2d6183ae9284c72ce1a0927485092e45b2a363bd feature-1-patch-copy

Again, this fails. We resolve conflicts in the same way, save, stage, and continue the rebase.

  • Menu: Branch/Rebase/Continue...
  • Context Menu "forge-b": Commit...
  • Toolbar: Continue
  • Shortcut: Ctrl-K

If you chose an option other than the menu, say that you want to Continue Rebase instead of Create Commit.

  • $ git add --update
  • $ git rebase --continue

Merge vs Rebase

One more look at the log for both versions separately:

Git-tut-10-log-rebase.png Git-tut-11-log-merge.png

What version is preferrable? It depends on taste and on situation, and I won't be able to bring all arguments here. Rebase keeps the history linear, but some call this an outright lie. If you look at the timestamps, you see that the dates are not increasing, because earlier commits are applied on top of later ones. Also, how did development of feature-1-patch go when viewed separately from master?

Rebase also has a practical implication: suppose you run tests on every commit. The original "improve feature 1" commit passed tests, but does the rebased commit pass the tests as well? That version of the code was never manually committed by a developer, and rebasing can automatically create lots of commits when there are no conflicts. Passing the tests can be seen as a more thorough conflict check that Git does not perform during the rebase.

A merge commit is a single commit that can be tested by the developer. However, having merge commits everwhere in the history can make the history ugly, and as with every part of software, readability is a desirable property of the history as well.

The question merge vs rebase should be best decided per-project, not per-developer. That doesn't mean that only one is ever valid, just what is preferred in what situation. For example, rebasing can be risk-free for small changes (1-2 commits) that are developed in a short amount of time by one developer. As soon as multiple developers work on a feature, or the master branch has advanced a lot while implementing the feature, a proper merge will likely make more sense.

Forks and Pull requests

This concludes the part focused on git command usage. Before going to forks and pull requests, let's review what happens in day-to-day usage of git, based on the example above (time passes downwards).

I won't show remote tracking branches like origin/master, instead I will show the actual branches at each remote. What's the difference? A remote tracking branch is what a particular clone thinks another repository's branch looks like, based on the last push/pull. Also, the "origin" in the branch name is the label git uses for a specific repository. When talking about different repositories with different origins, this gets confusing.

a forge.git/master, master
b
c feature-1-patch

Developer b at forge-b cloned forge.git and committed changes on top of commit a.

                   push
                   --->
a forge.git/master      a
d master                b forge.git/master, master

Developer a at forge-a committed changes on top of commit a and pushed them to origin.

   merge                     or    rebase
a                               a
|\                              d forge.git/master, master
b |                             b'
c |                             c' feature-1-patch
| d forge.git/master, master 
|/
e feature-1-patch

Developer b at forge-b incorporated changes into his repository by rebase or merge strategy.

Forking

Now let's add the feature of server-side cloning a repository - forking. Suppose developer b wants to share his feature-1-patch, but can't push to forge.git. They can fork forge.git and push to that fork:

a
|\
b |
c |
| d forge.git/master, forge-b.git/master, master
|/
e
f forge-b.git/feature-1-patch, feature-1-patch

Other developers can access forge-b.git and see commits b, c, e, f. Suppose the feature 1 patch is finished, then developer b would integrate it into master, and delete the feature branch. There are two strategies: merge and fast-forward.

  fast-forward               or    merge
a                               a
|\                              |\
b |                             b |
c |                             c |
| d forge.git/master            | d forge.git/master
|/                              |/|
e                               e |
f forge-b.git/master, master    f |
                                |/
                                g forge-b.git/master, master

The fast-forward simply assigns the branch label to commit f; merge creates a new merge commit for f and d. Let's go with the fast-forward variant for the rest.

Pull requests

Now that forge-b.git/master is in the open, also the forge.git team can inspect the code, and developer be can request them to do so. A pull request is just that: a request to look at code, followed by discussion, possible refinement, and finally possibly a pull.

After a successful pull request, the history could look like this:

a
|\
| b
| c
d |
|\|
| e
| f
g |
| h forge-b.git/master
|/
i forge.git/master

Since developer b's last pull request, a commit g was added to forge.git (possibly from another pull request). Also, the maintainers insisted on a fix h before merging the pull request into their repository.