---
tags: devtools2022
---
# 03-Advanced Git Part 1
**[Day1 PM Natalie]** Recap of git for **development** and **collaboration**, deep diving into advanced git tools.
## Schedule and Learning Objectives
* **1330-1400**: Project setup, tools, **basic** git concepts
* **1400-1500**: Advanced git tools part 1
* .gitignore, basic objects
* reset, branch, cherry pick, merge, rebase, stash
* bisect, squash
* **1500-1530**: Break
### Install `nvm` and `node`
Before you start, make sure `node` is installed. Managing node [using `nvm` is recommended.](https://github.com/nvm-sh/nvm)
```
sudo apt update
sudo apt install curl
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
```
The nvm installer script creates an environment entry to the login script of the **current** user. You should reload them for the changes to take effect. After that, use it to install `node`.
```
source ~/.bashrc
nvm install node
```
> If you use `zsh`, then it will be your `~/.zshrc`.
Please see [`nvm` documentation](https://github.com/nvm-sh/nvm) for switching between `node` versions.
### Starter app
Create a `next` starter app:
```shell
npx create-next-app@latest next-devtool
```
You should have the following project structure:

> Tip: ignoring `node_modules` in Dropbox is not supported natively. You can however install [this module](https://www.npmjs.com/package/dropbox-ignore-node_modules): `npm i dropbox-ignore-node_modules -g` globally. Then enter `di` command to ignore.
Now just run `npm run dev` to start the development server and view your webpage at `http://localhost:3000` by default.
## `git` terminal UI
You can do most of the git commands in the terminal or visualise it with VSCode integrated `git` pane. However, it's sometimes useful to look at some other git terminal UI like the following to enhance your productivity (not to mention they look pretty):
1. [gitui](https://github.com/extrawurst/gitui)
2. [lazygit](https://github.com/jesseduffield/lazygit)
## Fundamentals of `git`
For starters, this [git cheat sheet](https://www.atlassian.com/git/tutorials/atlassian-git-cheatsheet) is very useful.
The most common **basic git commands** are:
1. **Setting up**: `init, clone, config, alias`
2. **Updating repo:** `add, commit, diff, stash`
3. **Inspection**: `status, tag ,blame`
4. **Cleaning up**: `checkout, clean, revert, reset, rm`
5. **Rewriting history**: `rebase, reflog, commit --amend`
Let's go through each of them as a recap. The basic `next` app starter code already has `git` initialised. You can use this [VSCode extension](https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph) to visualise the git graph easily.
Now add `.vscode/` to `.gitignore`. You can then `add` then `commit`. Your graph will then be updated:

You can change more things and use `git diff` to observe the difference.

Finally, you can temporarily remove your recent changes by`git stash`. By default, running `git stash` will stash:
* changes that have been added to your index (staged changes)
* changes made to files that are currently tracked by Git (unstaged changes)
But it will **not** stash:
* new files in your working copy that have not yet been staged
* files that have been ignored
Let's add a few changes. Suppose we want the server to run on `https`. Run the commands below:
```shell
mkdir certificate
cd certificate
openssl genrsa -out key.pem
openssl req -new -key key.pem -out csr.pem
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
rm csr.pem
cd ..
touch server.js
```
The above will create a `certificate/` directory with a private key and a self-signed **certificate**, and a new file `server.js`. Your directory should look like this now:

Then paste the following code inside `server.js` and save it.
```javascript=
// custom server file added to support https
const { createServer } = require("https");
const { parse } = require("url");
const next = require("next");
const fs = require("fs");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const httpsOptions = {
key: fs.readFileSync("./certificate/key.pem"),
cert: fs.readFileSync("./certificate/cert.pem"),
};
app.prepare().then(() => {
createServer(httpsOptions, (req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
}).listen(3000, (err) => {
if (err) throw err;
console.log("> Server started on https://localhost:3000");
});
});
```
If you type `git status` now to check the status of your repo, notice that only `server.js` is reported, and not your `.pem` files (your keys and certs).
> This is because `*.pem` is inside `.gitignore`.

Now you can test run using the command `node server.js`. You might be faced with the **warning** in your browser because the certificate is *not valid* (it's issued by you, you're not exactly a *valid* authority). Just click proceed anyway.
> In Chrome, you can type `chrome://flags/#allow-insecure-localhost` to the address bar, and you should see highlighted text saying: *Allow invalid certificates for resources loaded from localhost*. Click **Enable**.

Let's now create a new branch, and add the changes in:
```
git branch https
git checkout https
git add server.js
git commit -m "feat: add https"
```
Then, go back to the `main` branch and make one new change and then commit, e.g: update readme:
```
git checkout main
git add README.md # after making some edit on readme
git commit -m "misc: update readme"
```
Now we have such state where the main branch has made a new commit.

We can use `rebase`:
```
git checkout https
git rebase main
```
This **moves** the entire `https` branch to begin on the **tip** of the `main` branch, effectively incorporating **all of the new commits** in `main`.
> Rebasing re-writes the project history by creating **brand new commits** for **each** commit in the `https` branch.

Major benefit of `rebase` vs `merge`:
* Eliminates the unnecessary merge commits required by git merge.
* Results in a perfectly **linear** project history. This makes it **easier** to navigate your project with commands like git log, git bisect, and gitk.
To compare, let's say we have 1 more commit ahead in `main`, and another branch called `http-merge`.

Here's what the graph looks like when comparing `merge` vs `rebase`:

With `rebase`, we don't have that weird **fork** from `main` out to other branches.
This can be easily seen suppose you add one new commits to each of the 3 branches: `main`, `https`, `https-merge` separately thereafter:

### Dealing with remote repository
If you do add remote repositories, sometimes you'd need to `fetch` the remote if more updates are made by others. GitHub, GitLab, Bitbucket, etc are all possible sites that can host your remote repos.
To list your remote repos, you can type `git remote -v`.
To **add** a remote repo, do:
```
git remote add REMOTE-ID REMOTE-URL
```
`REMOTE-ID` is usually `origin` or `upstream`, but you can name it whatever you want. **Conventionally**, (in terms of GitHub forks):
1. `upstream` generally refers to the original repo that you have forked
2. `origin` is your fork: your own repo on GitHub, clone of the original repo of GitHub
After the remote is added, you can `fetch` to download the most recent changes:
```
git fetch REMOTE-ID
```
and then rewrite your branch with `REMOTE-ID/BRANCH`. Call this command in the branch you want to rewrite:
```
git rebase REMOTE-ID/BRANCH
```
For example, suppose `origin/main` has a commit that's not available locally, and we are at a branch called `https` now:

We can **rebase** our `https` branch and have the following state now, similarly applying the two features: `add https` and `debug: add log message` **on top** of the remote's `add unicorn coloring`.

### git `checkout`
A "checkout" is the act of **switching** between different versions of a target entity.
You can `checkout` a branch, and running the command without anything means that you checkout this branch.
When you have found a commit reference (the SHA1 hash) to the point in history you want to visit, you can utilize the git checkout command to **visit** that commit.

There's a **detached HEAD** warning because when we check out a previous commit, the `HEAD` no longer points to a branch but **DIRECTLY** to a commit.
Nothing you do in here will be saved in your repository. To continue developing, you need to get back to the “current” state of your project:
```
git checkout main
```
### New branch with git `checkout`
Suppose you have checked out to an old commit, and would like to make a branch off this point. Simply do:
```
git checkout -b [NEW_BRANCH_NAME]
```
This new branch contains everything up to this commit we have checked out.

### Updating local copy
Suppose we are at branch `main`, and we would like to update it with our `origin/main`. If we have some unsaved changes, we cannot `pull`. We can either `stash` it first or `commit`, depending on whether that feature is "ready".

### Merge Conflict
Suppose the same line in the same file: `globals.css` is modified both locally and remotely (but with different values). If that change is committed locally, and *then* we try to pull the remote repo, then we will be faced with **merge conflict**, to which we need to resolve **manually**.

Simply go to **source control** in VSCode and accept the changes you want. You can open the **Merge Editor** for better UI.

We need to then **stage** the change and commit again.

If you want to avoid the situation altogether, abort the merge by doing:
```
git merge --abort
```
...and everything looks like the way it was before that bad `pull`.

### Avoiding Disaster
You can use the option `--ff-only` in `pull`:

> `--ff-only`: only update to the new history if there is **no** divergent local history.
Obviously it will fail because there *is* a divergent in the history. Either you fix that or you do a `git pull --rebase` and manually fix the conflict by selecting which version you want (incoming or current), and then **staging** it with `git add` again.
With proper rebasing, we end up with a nice linear path in `main` and `origin/main`:

### Temporary Changes
Suppose you did some changes but you aren't ready to commit yet. You can `stash` it with a named index so that it is easier to find them later on:
```
git stash save [NAME]
```

A `stash` is global. For instance, you can also stash in the other branch:

You can `pop` (remove from stash) the changes back using the index:
```
git stash pop [INDEX]
```
For example, suppose we have 3 entries in the stash. If we do a `git stash pop 1`, we are left with 2 entries in the stash:

You can also `apply`(apply but keep in stash)/`pop`(apply and remove from stash) stash to different branches. If you want to delete a stash entry, do either:
```
git stash drop [INDEX] # remove 1 entry
```
or (clear everything)
```
git stash clear
```

### Reset
If we havent commited anything, we can `unstage` (undo add) by typing `git reset`.
> All your changes will still be there.
Suppose we want to go back to some previous commit. We can do that either by `hard` or `soft` reset, depending on whether we want to keep the changes thus far or not.
We create a new branch called `reset-demo` for this purpose. Suppose we want to roll back until (including) the second commit ever made. We use the command:
```
git reset --soft [COMMIT_HASH]
```

Notice how now we have 3 uncommited changes instead of 1:

If we do a `hard` reset, then we simply unroll to the state of the second commit, and **losing everything**:
```
git reset --hard [COMMIT_HASH]
```

### Revert
We can also undo a commit with `revert`. For example, if we execute `git revert HEAD`, Git will create a **new** commit with the **inverse** of the last commit.
However this will clutter your history and doesn't undo as cleanly as `reset`.
TODO: compare revert, checkout, and reset
https://www.atlassian.com/git/tutorials/resetting-checking-out-and-reverting
## Advanced Techniques
### git reflog
After a `hard` reset, you might realise you have made a mistake. What happens then?
Suppose we have the following `log` after a hard reset:

Worry not, just type:
`git reflog`
Notice that the logged states are sorted in **chronological** order, with the **newest** one at the top.

Read the 5th log labeled `reset`. That's the bad "reset", and the state before it with hash `cf243bf` is the last good state of which we can return to!
We can either use `git reset --hard/--soft [COMMIT_HASH]` again or create a new branch:
```
git branch [NEW_BRANCH] [COMMIT_HASH]
```
The output of the former using reset is:

#### Recovering a branch
Initially, we have the following state:

Suppose we accidentally deleted a branch `main-delete-branch-demo`, and then we go about our day forgetting about it.
```
git branch -D [BRANCH_NAME]
```

In order to recover it, we need to trace when we `checkout` **to** this deleted branch, e.g from another branch into this `main-delete-branch-demo`. We can do the same with any commit state inside `main-delete-branch-demo `.

Now, we can simply take that commit hash and create a new branch with it:
```
git branch [REVIVED_BRANCH_NAME] [COMMIT_HASH]
```

### Cherry Pick
Suppose a branch `https-merge` has last 2 commits that we want to **apply** to another branch `https`. We just specifically **WANT** these 2 commits, and nothing else.
On branch `https-merge`, we can do a `soft reset`, and make a single commit instead.
> an alternative way is to `rebase -i` and **squash** commits together.
Then checkout branch `https`, and **cherry pick** that new last commit and voila, we have that two commits **applied** on `https`.

#### Undoing a commit
Suppose the most recent commit in branch `main` should actually be made in `https`.

We can simply **cherry pick** this commit:
```
git checkout [CORRECT_BRANCH]
git cherry-pick [COMMIT HASH]
```

Finally, we shall remove the commit from `main` using `reset --hard`:

### Squash
Now suppose it's time to `merge` branch `https` to `main`. We do not want so many commits for instance. We can either do a `git merge --squash [BRANCH]` in `main`, or `squash` it in `https` right now.
To do the latter, type the command and change all `pick` into `s` (squash). This will rewrite your own branch history.
```
git rebase -i HEAD~6
```

When successfull, you're faced with just one commit joining everything. They will by default provide helpful messages for you.

If you squash while merging, you won't lose that history in your branch. Let's do this with `https-merge` branch. Checkout the `main` branch:

Then type the command:
```
git merge --squash https-merge
```
This will **not** make the commit for us. It simply *migrate* the merge changes to us and we **NEED** to make the commit ourselves.

### Bisect
It is common to find the situation whereby you have made so many commits:

But when you run it, you notice there's some **bug**. Let's assume the red text in the screenshot below is NOT supposed to be there.

We can use `git bisect` to find the culprit commit.
First, let's start it:
```
git bisect start
```
Then since we are at a "bad" state, we label this commit as bad:
```
git bisect bad
```
Then we go to another commit that we know is "good" (the bug wasnt there yet).
```
git bisect good [COMMIT_HASH]
```
`bisect` will then automatically bring us to the commit in the middle:

We just need to test the site again using `npm run dev`, state whether it's good or bad.
> This can be quite tedious if you have lots of commits.
Finally, we find that commit `feat: add unicorn coloring` is indeed the culprit.

This is indeed the commit that changes the `globals.css` file wrongly.

You can end the bisect using `git bisect reset`:

and then actually modify `globals.css` to fix the mistake, then make new commits etc.
> If that commit is standalone and you would like to remove **everything** from that commit, you can use `git rebase -i HEAD~x` and `drop` that commit from history.