Essential Git - a pragmatic guide
Tags: git, vcs, cheatsheet
Published: 2023-09-01 15:13:10

## Introduction

Git is a ~~dreaded~~ ubiquitous version control system that helps you keep track of changes to your local files and allows you to collaborate with others on software projects. This serves as a practical guide that covers a number of the essential, most common commands in Git that you'll find yourself frequently using in your daily developer life.

There is nothing earth-shattering here: it's merely a compilation of simple commands and useful variations on them that I find very helpful to use. All of this and more is obviously covered by the [official Git documentation](https://git-scm.com/doc). Of course, there's only one problem with the documentation: it's impenetrable; hence, this guide.

If you absolutely hate Git and feel intimidated by it, this is your lifeline. Bookmark it for when the going gets tough.

## Basic Terminology

Before we get started, let's go over some important definitions to avoid ambiguity down the line:

- **Commit:** a snapshot that captures the state of your project at a particular point in time.
- **HEAD:** a pointer that usually points to the tip of the currently checked out branch. The tip of a branch is the most recent commit in that branch.
- **Branch:** a series of commits with a pointer that points to the most recent one.
- **Working directory:** the local workspace hosting your entire project files and directories and where you experiment with new changes.
- **Staging area:** a place where changed files are added before they can be part of the next commit.
- **Repository:** the hidden .git directory that is created and used by Git to keep track of changes to your files; it contains a history of all of your commits.
- **Remote:** the name of a pointer, typically named "origin," that points to the URL of a cloud-hosted repository.

## Getting Started

When you're setting up Git for the first time on your local machine, you need to set up your name and email address:

```bash
git config --global user.name "John Doe"
git config --global user.email "john@email.com"
```

Optionally, you may want to set up your default text editor. As an example, here's how to set up Visual Studio Code as the default editor:

```bash
git config --global editor.core "code --wait"
```

To initialize a new Git project (i.e., create a new Git repository and start tracking changes to your project files), simply run:

```bash
git init
```

To clone an existing Git repo instead:

```bash
git clone <url>
```

## Snapshotting

### `git status`

Show the current state of your repository. This command shows you the following:

- A list of files that are not tracked by Git,
- a list of files that are added but have been modified since the last commit,
- and a list of files that are in the staging area (also known as the index) and ready to be committed.

Run `git status -s` to see the output in a shorter format.

### `git add <file1> <file2> ...`

Add changed files or previously untracked files from the working directory to the staging area. Use `git add .` to add all files at once.

### `git commit -m "commit message"`

Create a new commit with files that are currently in the staging area. The commit message can be anything you want to describe the changes made in the current commit. 

To add a separate commit message that explains in more detail the changes in your commit, run: `git commit -m "commit message" -m "another commit message"`. Multiple message flags are concatenated as separate paragraphs. The first message is usually a short summary of the changes (50 characters or less), and the second message is a more detailed explanation.

You can add all tracked files and commit them in one go by running `git commit -am "commit message"`. However, keep in mind that this <u>only works if you're committing files that are already tracked by Git</u>.

To add new changes to the most recent commit instead of creating a new one, or to modify the commit message run `git commit --amend` or `git commit --amend -m "new commit message"` after staging your new changes if there are any.

Note that `git commit --amend` will launch your default text editor with the commit message of the most recent commit. You can edit the message and save the file to change the commit message or simply close the editor without making any changes to keep the same message.

If you want to create an empty commit with no changes, run `git commit --allow-empty -m “commit message”`. I personally find this useful in one situation: to test or trigger a CI/CD pipeline. 

### `git log`

Show a log of commits for the current repository along with the current position of HEAD (which commit it points to). Each commit has a hash, an author, a date, and a commit message. 

A useful variant is `git log --oneline` which shows a shortened commit hash alongside the commit message.

You use the `--grep` option to search for commits that contain a specific string in their commit message. For example: `git log --grep="commit message"`. You can limit your search to a specific number of commits by using the `-n` flag: `git log --grep="commit message" -n 5`. And you can also use `-i` flag to make the search case-insensitive.

## Branching

### `git branch <name>`

Create a new branch called `<name>` based on the currently checked out branch.

### `git branch`

Get a list of all branches in the current repository. Depending on your shell configuration, the currently checked out branch, i.e., the branch that HEAD currently points to, will be shown in green with an asterisk.

You get the same output by running `git branch --list`.

### `git switch <branch>` or `git checkout <branch>`

Switch to an existing branch named `<branch>`. What happens when you run this command is that HEAD starts pointing to the `<branch>` pointer and `<branch>` becomes the currently checked out branch.

### `git switch -c <branch>` or `git checkout -b <branch>`

Create a new branch `<branch>` and then switch to it. This effectively combines `git branch <branch>` and `git switch <branch>`. 

### `git branch -d <branch>`

Delete the branch named `<branch>`. You can force delete it by using the `-D` flag. 

You can delete multiple branches by listing them: `git branch -d <branch1> <branch2> <branch3>`.

You can't delete a branch if it's currently checked out. You have to switch to a different branch first.

### `git branch -m <new-name>`

Rename the currently checked out branch to `<new-name>`. To rename a different branch: `git branch -m <old-branch-name> <new-branch-name>`.

### `git checkout`

`git checkout` is a versatile command that can be very useful sometimes. These are some of the ways you can use it:

- `git checkout <commit>`: this goes into detached HEAD mode by making HEAD point directly to the commit that you specify (instead of pointing to the tip of a branch). In this mode, you can view and make changes and at any point you may revert your changes by running `git switch <branch>` or `git switch -` to switch to the branch HEAD was pointing to before using the `checkout` command. If you'd like to keep the changes you make in detached HEAD, you can run `git switch -c <branch>` to create a new branch with your new changes.
- `git checkout HEAD~n`: check out the *nth* ancestor commit of HEAD.
- `git checkout HEAD <file>` or `git checkout -- <file>`: discard staged and unstaged changes to match HEAD. Similar to running `git restore`, explained below.

## Merging

There are two steps required in merging:

1. First, you switch to the branch you want to merge changes *into*.
2. Then you run the merge command.

For example, to merge `feature` branch into `main`:

```bash
git switch main
git merge feature
```
Some useful options:
- To show a list of branches that have been merged into the currently checked out branch: `git branch --merged`
- To show a list of branches that have *not* yet been merged into the currently checked out branch: `git branch --no-merged`
- To merge changes in `feature` branch into `main` but using only a single commit: `git merge --squash main`. All commits from the branch will be squashed into a single commit. You’ll see all changes have been staged and you’ll have to manually create a new commit.

**Fast-Forward Merge**

In most cases, the merge will be a fast-forward merge: the base branch (`main`) pointer will simply be updated to point to the same commit that the merged branch (`feature`) points to.

**Non-Fast-Forward Merge**

Sometimes the merge isn't exactly straightforward. This happens when two branches diverge. 

Suppose that you create a new branch based on `main` and call it `feature`. Let's say that you add a couple of new commits on `feature` and at the same time there's a new commit on `main` that you currently don't have. When you try to merge, Git will create a new merge commit.

You can manually create a merge commit by running `git merge --no-ff <branch>`. This helps you maintain a clear history of when a branch was merged into another branch.

**Resolving Merge Conflicts**

Merge conflicts happen when the same part of the file is modified differently on the two merged branches. When this happens, Git will let you decide what to keep and what to remove. 

Merge conflicts are resolved manually. Adding files using `git add <file>` tells Git that conflicts have been resolved.

Run `git merge --abort` after a merge has caused conflicts to return to the pre-merge state. It's important to note that there is no guarantee that uncommitted changes will be reconstructed by running this command.

After resolving conflicts and adding files to the staging area, you may create a commit manually or run `git merge --continue` to be prompted for a merge commit message.

### Cherry-Picking

You may use `git cherry-pick` to merge specific commits from one branch to another. 

For example, if you want to merge `<commit-hash>` from the branch `main` to the branch `feature`, you run the following:

```bash
# swtich to the target branch first
git switch feature
# now run the cherry-pick command
git cherry-pick <commit-hash>
```

Running this command may cause merge conflicts. You can resolve them normally. You get the familiar abort and continue options as you do with regular merges.

## Comparing

### `git diff`

Show the difference between the working directory (the current state of your files) and the staging area. In other words, this shows you changes to your working directory that you could add to the staging area by running `git add .`. The `git diff` command accepts different options and arguments:

- `git diff --staged` or `git diff --cached`: show changes that have been staged relative to the most recent commit.
- `git diff HEAD`: list all changes, staged and unstaged, since the last commit.
- `git diff <file1> <file2>`: show working directory changes to specified files.
- `git diff <branch1> <branch2>`: compare the tip of `branch1` to the tip of `branch2`.
- `git diff <commit1> <commit2>`: compare changes between the given commits (which are passed as commit hashes).
- `git diff --staged <commit> <file>` : show changes that have been staged for file `<file>` since the commit `<commit>`. It basically answers the question: how is `<file>` going to be different in the next commit compared to the old commit `<commit>`?

## Stashing

You'll notice that if you attempt to switch branches while having uncommitted changes, Git will do one of two things:

- If your changes don't conflict with the branch you're switching to, then Git switches the branch aand automatically applies your uncommitted changes to the new branch. 
- If your changes do conflict with the branch you're switching to, then Git will prevent you from switching branches.

Stashing is helpful here. Let's say you have some incomplete work on a feature and right now you really want to work on a different feature altogether. You don't want to finish working on that now, and your changes are not yet ready to be committed. What do you do in this case? You stash, of course.

You can stash as many times as you like. Your changes are saved to a stack-like data structure where the top entry is the most recent.

### `git stash`

Stash away the current changes (staged and unstaged) and get back to a clean working tree matching the most recent commit. `git stash push` does the exact same thing.

There are several variations on the basic stash command:

- `git stash --keep-index`: stash only unstaged changes, keeping staged changes intact.
- `git stash -p`: interactively choose which chunks of changes to stash.
- `git stash push <file1> <file2>`: stash changes made to the provided file paths in one stash entry.
- `git stash list`: show a list of stashed changes.
- `git stash pop`: reapply the most recently stashed changes to the working directory and remove them from the stash.
- `git stash apply`: reapply the most recently stashed changes to the working directory *without removing them from the stash*
- `git stash apply n`: reapply stashed changes saved at index `n`.
- `git stash drop n`: drop/delete stashed changes saved at index `n`.
- `git stash clear`: clear all stashed changes.

## Undoing

### `git restore <file>`

Discard unstaged changes made to `<file>` and restore its state to the most recent commit. You can specify multiple files `git restore <file1> <file2>` or all files `git restore .`. 

You can also remove staged changes by using the `--staged` option.

### `git reset <commit>`

Reset the current branch to a particular commit and keep the changes in the working directory. If you'd like to discard the changes, simply use the `--hard` option.

*Warning: any changes you discard with the `--hard` option are permanently removed, so this should be used sparingly.*

### `git revert <commit>`

Create a new commit to undo the changes from an earlier commit. 

To undo the effects of a revert, you can revert the revert commit. If there have been changes since the revert commit, you'll have to resolve conflicts.

This is the command to use—instead of `git reset`—when you want to undo changes that have already been shared with other collaborators on the same project. 

If you use `git reset` to undo changes that have already been shared, you'll have to force push your changes to the remote repository. This will overwrite the history of the remote repository and could potentially cause problems for others.

## Collaborating

GitHub, BitBucket and similar websites make it easy to share code and collaborate on projects. Git has several commands to make this possible.

### `git clone <url>`

Use this to clone an existing repository hosted on GitHub or a similar website. When using this command, the remote is set automatically for you.

### `git remote add <name> <url>`

Sometimes you create a new local repository and then you want to add a "remote" repository to push changes to. 

"Origin" is a typical name for a remote repository. For example: `git remote add origin https://github.com/yourUsername/yourRepoName.git`.

- To show a list of remotes: `git remote`
- To show a list of remotes with URLs: `git remote -v`
- To rename an existing remote: `git remote rename <old-name> <new-name>`
- To remove a remote: `git remote remove <name>`

### `git push <remote> <branch>`

Push committed changes from your local branch to your remote branch. If no arguments are given, `<remote>` defaults to "origin" and `<branch>` defaults to the current branch. To push all branches, use the `--all` option.

- To show a list of remote  branches: `git branch -r`.
- To show a list of all branches, including remote branches: `git branch -a`.
- To create a local branch from a remote branch: `git switch <branch>`.

### `git fetch <remote> <branch>`

Get the latest changes from the remote repository without merging them locally. If no arguments are given, `<remote>` defaults to "origin" and `<branch>` defaults to the current branch.

### `git pull <remote> <branch>`

Fetch changes then merge them locally. This is equivalent to running `git fetch` then running `git merge`. With no arguments, `<remote>` defaults to "origin" and `<branch>` defaults to currently checked out branch.

## Rebasing

`git rebase` can be used in two ways.

**As an alternative to `git merge`**

As an example, let's suppose that you created a new branch, `<feature>`, based on `main`. While you were working on your new feature, new changes have been added to the `main` branch. You want to pull these changes and merge them. But you can use `rebase` in this case.

First, switch to your branch: `git switch <feature>`. Now run `git rebase main` to *re*base your branch commits on the latest changes from `main`. This command will create new commits to make a linear commit history but these new commits will keep their original metadata, such as author name and date.

*Warning: do not rebase commits that you've already shared with other team members. This should only be used locally on changes you haven't shared yet. Otherwise, you'll have to force push your changes to the remote repository which will overwrite the history of the remote repository and cause problems much like what happens when you use `git reset` instead of `git revert`.*


**As a way to clean up your commit history**

Do you want to delete, squash, rename, or reorder your commits before sharing them? Use interactive rebase for that.

```
git rebase -i HEAD~n
```

Running this command will launch your default editor with instructions on what each command does. You must specify how far back you want to go in your commit history by specifying `HEAD~n`. 

Rebasing is a powerful (and potentially dangerous) tool. If you're brave enough, you can read more about it [here](https://git-scm.com/book/en/v2/Git-Branching-Rebasing).

*Note: `git rebase` uses `git cherry-pick` behind the scenes and that might mean that you’ll get more conflicts than when using a regular `git merge`.*

## TL;DR

Git is ~~terrifying~~ awesome. 

---
If you're interested, there's a (slightly) less verbose version of this guide on [GitHub Gist](https://gist.github.com/asemarian/bc565ddffb4e0e5ce0e8b684e652df0d). That's what actually inspired me to write this guide in the first place.