Git, beyond beginner


This post assumes familiarity with git already and it is intended more like a cheetsheet, rather than a how to.

Contents:

  1. Aliasing commands
  2. Amending and squashing
  3. Remote repos
  4. Forking
  5. Tagging
  6. Branching
  7. Integrate changes

1. Aliasing commands

This will allow you to improve your git workflow by creating shortcuts to more complicated git commands.
To see all the current alias confgurations of your git instance, run

git config --list | grep alias

One way to add git aliases is through direct command and it looks something like:git config --global alias.<your_alias_command> <the_git_command>

For example, the command git checkout can be replaced with an alias such as git co (less typing). To achive that, run:

git config --global alias.co checkout

Another way to achive the same effect (for *nix) users is to locate the ~/.gitconfig file and below the [alias] tag add:

:
[alias]
    co = checkout
:

Here are some nice aliases to view the commit history:

[alias]
	lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
	lg1 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all
	lg2 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n''          %C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all

Top ↑

2. Amending and squashing

Simple mistakes can be made when committing (eg: wrong spelling in the commit message or a forgotten file). Rather than committing again the forgotten file with a proper message, one can add the file and modify the commit message using:

git commit --amend -m "New Message"

The commit history will show only one commit containing the previous contents and the amended contents.

NOTE!!! this should be done only for the local commits. DO NOT amend public commits.

Now imagine a situation whereby you are frequently committing code (eg: write some debugging code and then test the results several times), only to find yourself at the end of the process to have 10 commits for only a few changes in the real code.
This would look rather ugly if you were to push to the public repository, after all, the real changes to the code a only a few and the intermediary helping steps in between are not relevant.
In those cases, we could squash the last 10 local commits into a single commit, like so:

git reset --soft HEAD~5

Top ↑

3. Remote repos

Since you are using git, most probably it is for collaborative purposes, so, the local work needs to be shared using a remote repository. To see the settings towards the remote repo, run git remote -v and the result will be something like:

origin  https://github.com/Microsoft/MS-DOS.git (fetch)
origin  https://github.com/Microsoft/MS-DOS.git (push)

Here you can see that the default name of the remote is origin, and two remote addresses (one for fetch and one for push).
For detailed information about the remote repo, run git remote show <remote_repo_name>:

git remote show origin

To add our own remote:

git remote add our-own-remote https://example.com

To remove the remote configuration: git remote remove <remote_repo_name>.

This will result in:

origin  https://github.com/Microsoft/MS-DOS.git (fetch)
origin  https://github.com/Microsoft/MS-DOS.git (push)
our-own-remote  https://example.com (fetch)
our-own-remote  https://example.com (push)

In order to download information from the remote repo, but not merge it with our local repo, all we have to do is git fetch <remote_repo_name>:

git fetch origin

To merge the changes of the remote repo into our own local repo, we run git merge <remote_repo_name>/<remote_branch> <local_branch>:

git merge origin/master master

To accomplish the above two steps in one command, we use git pull, which is an alias of git fetch && git merge

Top ↑

4. Forking

This feature is very useful for collaboration in case the collaborators do not have write access to the repository.
As a first step, create a fork of the repository you want to contribute to, then clone that fork onto your own local machine.

In order to keep track of the changes made on the original repository, you need to add an upstream remote address to your configuration.

git remote add upstream <original_repo_git>
git remote -v show

You will see two remote repositories, origin which is our fork and upstream which is the original repository. The name upstream can be anything easy to remember.

origin  https://www.our-fork.com (fetch)
origin  https://www.our-fork.com (push)
upstream        https://wwww.original-remote-repository.com (fetch)
upstream        https://wwww.original-remote-repository.com (push)

To incorporate changes from upstream into our own fork:

git fetch upstream
git merge upstream/master
git push origin master

Top ↑

5. Tagging

We use tagging in git to mark important milestones in the development process, like a stable release or a feature release, etc...

Listing all tags in the repo in alphabetical order:

git tag

To search for tags based on regex, one can run:

git tag --list <pattern>

To create a tag which contains extra information:

git tag -a "<tag_name>" -m "Some message about the tag"

In order to push the tags to the remote origin repo:

git push origin 1.1       // for one tag called 1.1
git push origin --tags    // for all tags

Retrieving extra information about a tag can be achieved through:

git show <tag_name>

Top ↑

6. Branching

Simply put, branches are references to a certain commit point in the repository. In a sightly more fancy way, branches are directed acyclic graphs or DAG. By default, we have a master branch (the name can be anything, but most of the time no one bothers to change it). We can diverge from this branch and create extra branches based on our needs.
Let's say we want to create a development branch and switch the HEAD to it:

git checkout -b develoment

If the goal is only to create the branch but not switch, we can run only:

git branch development

Like this, the original branch remains unchanged while we are working on our divergent branch.

To see how the branching looks like, use the log command:

git log --oneline --decorate --graph --all

To list up the branches along with their last commit:

git branch -v

Top ↑

7. Integrate changes

Having this great feature of branching, is what makes git very popular. However, this would be useless if we cannot combine (merge) the code that we branched out from, with the master track or the branch that we diverged from.
This is where merging and rebasing comes into play.

7.1. Merging

Imagine you start working on a feature and you branch out from the master at some point:

git checkout -b feature

Screenshot-from-2018-10-10-12-31-03

But there is a bug in the master branch and we need to fix it before we are done with the feature branch. So we switch (checkout) from our current working branch to the master branch, and fix the problem.(NOTE you should commit whatever work is untracked in the feature branch, before switching).

git checkout master
git checkout -b fix

Screenshot-from-2018-10-10-12-57-38-1

So now we are done with the fix branch and we will merge it into the master before continuing the work on the feature branch. You could also delete the fix branch once it is merged.

git commit -m "done with the fix"
git checkout master
git merge fix
git branch -d fix # delete the branch

git checkout feature

Screenshot-from-2018-10-10-13-00-57

We can make some more commits on our feature branch and then merge it into the master branch.

git checkout master
git merge feature

Screenshot-from-2018-10-10-13-11-04

Top ↑

7.2. Rebasing

Rebasing is also a type of merging, but it works by applying the patch of changes introduced by the branch we are working on and applying it on top of the target branch (eg: the master branch).
To achieve that:

git checkout feature
git rebase master

Screenshot-from-2018-10-10-13-45-44

The snapshot labeled 5I from the rebasing is the same as the snapshot labeled 6 from the merging. The only difference is in the history of the log. Rebasing keeps a linear commit history log.

As a rule of thumb, do NOT rebase on public branches, use regular merge because is safer. Merging is a safe option that preserves the entire history of your repository, while rebasing creates a linear history by moving your feature branch onto the tip of master.

If you’re not entirely comfortable with git rebase, you can always perform the rebase in a temporary branch:

git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch

Top ↑

7.3. Merging conflicts

Sometimes, things are not that smooth, especially when the same part of the code was modified in two different branches. In those cases, we get a merge conflict, and that needs to be solved manually.

Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

The the part(s) of file(s) with conflicts will be amended with something like:

<<<<<<< HEAD:home.py
print("Hello World!")
=======
print("Great to see you, World!")
>>>>>>> feature:home.py

The part between <<<<<<< HEAD and ======= is the code version in your target branch, while the part between ======= and >>>>>>> feature is the code version in our feature branch.

These files are not staged. Once you fix all the conflicts, do a git add ., which will signal that the conflicts are solved, followed by a git commit.

Top ↑