Git


Git Reference Guide — From Fundamentals to Everyday Workflow

Sparse Checkout

Best if you need to contribute code or keep the files updated.

This method uses the sparse-checkout feature (available in Git 2.25+) to clone the "shell" of the repo without downloading the file contents, then explicitly selects the folder you want to populate.

The Commands:

# 1. Clone the repo structure without downloading files (saves bandwidth)
git clone --filter=blob:none --sparse <REPO_URL>

# 2. Enter the repository directory
cd <REPO_NAME>

# 3. Tell Git which directory you actually want
git sparse-checkout set <TARGET_DIRECTORY_PATH>
  • Note: Replace <TARGET_DIRECTORY_PATH> with the relative path from the repo root (e.g., src/components, not C:/Users/...).

  • Result: Your local folder will contain only the .git folder and the specific directory you requested.

Adding a Remote

In Git, any external repository you connect to is known as a remote. Developers typically name the primary remote repository — often hosted on GitHub, GitLab, or another platform — origin.

The remote is considered your team’s authoritative source of truth: the version of code everyone agrees is canonical and up-to-date.

Example:

This simply registers the remote; no data is pulled yet.


Git Log Essentials

git log shows your project’s commit history. Useful variations include:

  • git log -n 10 — show the last 10 commits

  • git log --oneline — compact one-commit-per-line summary

  • git log --oneline --graph --all — visualize history as a branching graph

  • git cat-file -p <hash> — inspect the contents of a commit object

Example:


Git Configuration

Git settings exist in four scopes, each overriding the previous:

  1. system — applies to all users on the device (/etc/gitconfig)

  2. global — applies to your user (~/.gitconfig)

  3. local — applies to the repository (.git/config)

  4. worktree.git/config.worktree, a file that configures Git for part of a project

Most developers use --global for identity and editor preferences, and --local for project-specific overrides.

Common Commands

Configuration command:

  • git config --add --global user.name "John Doe"

  • git config --add --global user.email "john.doe@example.com"

Let's break down the commands:

  • git config: The command used to manage your Git configuration settings.

  • --add: Flag indicating you want to add a new configuration entry.

  • --global: Flag specifying this configuration should be saved globally in your ~/.gitconfig. The alternative is --local, which saves the configuration only for the current repository.

  • user: The configuration section.

  • name: The configuration key within that section.

  • "John Doe": The value you're assigning to the key.

Additional commands:

  • git config --get <key>: retrieve an individual configuration key's value, e.g. user.name

  • git config --list: display all current global configuration settings.

  • git config --list --local: display all current local configuration settings

  • git config --remove-section [section]: delete an entire configuration section, e.g. company.property="some value". This will remove the company section and everything within it.

  • git config --unset company.property: delete a specific key within a section (removes one occurrence). In this case, one of the "property" keys.

  • git config --unset-all company.property: delete all occurrences of a key (property) within a section.


Branching

https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshellarrow-up-right

A branch represents an independent line of development. Instead of modifying main directly, you create branches to isolate work. For example, you might want to create a feature that changes the color schema without editing the main/master branch directly. Instead, you could work on that new branch and merge it into the main branch (or choose to delete if you don't need it).

Check all branches including the active one

Creating and Switching Branches

Renaming and Deleting Branches

Setting Default Branch Name

Branches are simply references — stored as files under .git/refs/heads/.

Which commits are part of “Joe_branch”?

Answer: A-B-G-H

When you create a new branch, it uses the current commit you are on as the branch base. For example, if you're on your main branch with 3 commits, A, B, and C, and then you run git switch -c my_new_branch, your new branch will look like this:

So, if you create a new branch my_new_branch and run git log, you will have the same commits as the main branch.


Reading Commit History

Useful enhancements to git log:

Decorations

Run git log --decorate=full. You should see that instead of just using your branch name, it will show the full ref name. A refarrow-up-right is just a pointer to a commit. All branches are refs, but not all refs are branches.

Run git log --decorate=no. You should see that the branch names are no longer shown at all.

The second is --onelinearrow-up-right. This flag will show you a more compact view of the log. I use this one all the time, it just makes it so much easier to see what's going on.

  • git log --oneline

  • Or check a branch’s logs by name: git log --oneline main

Graph view

During merges

The --parents flag shows each commit along with its parent(s), which is especially useful when examining merge commits (which have two parents).

Example to the above command:

If you’re conducting a merge operation, such a command might be useful:

  • git log --oneline --decorate --graph --parents

    • Each asterisk * represents a commit in the repository. There are multiple commit hashes on each line because the --parents flag logs the parent hash(es) as well.

Let’s say you’ve just conducted a merge and run the above command. This is an output you might get (similar to this).

  • The first line, with these three hashes: 89629a9 d234104 b8dfd64 is our recent merge commit. The first hash, 89629a9 is the merge commit's hash, and the other two are the parent commits.

  • The next section is a visual representation of the branch structure. It shows the commits on the add_classics branch and the main branch before the merge. Notice that they both share a common parent.

  • The next two lines are just "normal" commits, each pointing to their parent.

  • The last line is the initial commit and therefore has no parent.


Git Internals: Refs and Files

Git stores all metadata under .git/.

  • The "heads" (or "tips") of branches are stored in the .git/refs/heads directory. If you cat one of the files in that directory, you should be able to see the commit hash that the branch points to.

    • .git/refs/heads/*

  • Remote branch refs → .git/refs/remotes/*

  • Current position → .git/HEAD

You can inspect a branch pointer:

This prints the commit hash the branch it currently references.


Merging

https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Mergingarrow-up-right

Standard Merge

A regular merge finds the merge base (best common ancestor), combines changes, and creates a merge commit with two parents.

Example:

Before:

After:

The merge will:

  1. Find the "merge base" commit, or "best common ancestor" of the two branches. In this case, B.

  2. Replay the changes from main, starting from the best common ancestor, into a new commit.

  3. Replay the changes from feature onto main, starting from the best common ancestor.

  4. Records the result as a new commit, in our case, F.

  5. F is special because it has two parents, C and E.

Fast-Forward Merge

https://git-scm.com/docs/git-merge#_fast_forward_mergearrow-up-right

If your branch contains all commits from main, Git can simply move the pointer without creating a merge commit. A "fast-forward" – it's like simply fast-forwarding your branch's history to include the new commits from the merged branch. Let's say we start with this:

Before:

And we run this while on main:

  • git merge feature

After:

Fast-forward merges keep history linear.

Because feature has all the commits that main has, Git automatically does a fast-forward merge. It just moves the pointer of the "base" branch to the tip of the "feature" branch:

Notice that with a fast-forward merge, no merge commitarrow-up-right is created.

This is a common workflow when working with Git on a team of developers:

  1. Create a branch for a new change

  2. Make the change

  3. Merge the branch back into main (or whatever branch your team dubs the "default" branch)

  4. Remove the branch

  5. Repeat


Rebase

Rebasing replays your commits on top of a new base, producing a linear history.

Before:

We're working on feature, and want to bring in the changes our team added to main so we're not working with a stale branch. We could merge main into feature, but that would create an additional merge commit. Rebase avoids a merge commit by replaying the commits from feature on top of main. After a rebase, the history will look like this:

After:

Rebase is ideal for cleaning up or updating your in-progress branches.

Important Rule

Never rebase shared/public branches like main. Rewriting history breaks other developers’ clones.

When to Rebase

git rebasearrow-up-right and git mergearrow-up-right are different tools.

An advantage of merge is that it preserves the true history of the project. It shows when branches were merged and where. One disadvantage is that it can create a lot of merge commits, which can make the history harder to read and understand.

A linear history is generally easier to read, understand, and work with. Some teams enforce the usage of one or the other on their main branch, but generally speaking, you'll be able to do whatever you want with your own branches.

Warning

You should never rebase a public branch (like main) onto anything else. Other developers have it checked out, and if you change its history, you'll cause a lot of problems for them.

However, with your own branch, you can rebase onto other branches (including a public branch like main) as much as you want.


Reset

git reset moves the current branch pointer to a different commit. The git resetarrow-up-right command can be used to undo the last commit(s) or any changes in the index (staged but not committed changes) and the worktree (unstaged and not committed changes).

Soft Reset

Moves HEAD but keeps your changes staged.

The --soft option is useful if you just want to go back to a previous commit, but keep all of your changes. Committed changes will be uncommitted and staged, while uncommitted changes will remain staged or unstaged as before.

The files will remain as they were, only the commit history will be modified.

Hard Reset (Dangerous)

Moves HEAD and discards all working directory and staging changes. This is irreversible unless recovered via reflog.

I want to stress how dangerous this command can be. If you were to simply delete a committed file, it would be trivially easy to recover because it is tracked in Git. However, if you used git reset --hard to undo committing that file, it would be deleted for good. The changes within that file will be deleted forever.

Always be careful when using git reset --hard. It's a powerful tool, but it's also a dangerous one.


Working With Remotes

A remote is another repository with (usually) shared history.

We can have "remotes", which are just external repos with mostly the same Git history as our local repo.

When it comes to Git (the CLI tool), there really isn't a "central" repo. GitHub is just someone else's repo. Only by convention and convenience have we, as developers, started to use GitHub as a "source of truth" for our code.

Add a Remote

In git, another repo is called a "remote." The standard convention is that when you're treating the remote as the "authoritative source of truth" (such as GitHub) you would name it the "origin".

By "authoritative source of truth" we mean that it's the one you and your team treat as the "true" repo. It's the one that contains the most up-to-date version of the accepted code.

The URI is probably either a Github/Gitlab URI or your local path if you’re using a local repo as the remote.

Fetch Remote Metadata

Adding a remote to our Git repo does not mean that we automagically have all the contents of the remote. First, we need to fetch the contents.

However, just because we fetched all of the metadata from the remote repo doesn't mean we have all of the files.

Inspect Remote Branch Logs

The git log command isn't only useful for your local repo. You can log the commits of a remote repo as well!

  • git log remote/branch

    • For example, if you wanted to see the commits of a branch named remote_branch from a remote named origin you would run:: git log origin/remote_branch

    • Or: git log origin/update_dune --oneline

Merge a Remote Branch

For example, if you wanted to merge the remote_branch branch of the remote origin into your local main branch, you would run this inside the local repo while on the main branch:

  • git merge origin/remote_branch


Git Pull

Fetching is nice, but most of the time we want the actual file changes from a remote repo, not just the metadata.

git pull is essentially:

Syntax:

The [...] syntax means that the bracketed remote and branch are optional. If you execute git pull without anything specified it will pull your current branch from the remote repo.

Prefer Rebase on Pull (cleaner history)

Keep your serious projects on GitHub. If your computer explodes, you'll have a backup, and if you're ever working from another computer, you can simply clone the repo and get back to work.

Consider using rebase instead of merge. Configure Git to rebase by default on pull rather than merge—this keeps your history linear and easier to follow. To set this up, add the following to your global Git config:


.gitignore

You can have .gitignore files in multiple locations

A .gitignore file doesn't have to live only at the top level of your project. You can place .gitignore files in various folders throughout your codebase, and each one will control what gets ignored in its own folder and any folders beneath it, the patterns apply recursively within their directories.

Example scenario:

The .gitignore inside the resources/ folder lists:

  • internal_use.png

  • app.py

The root .gitignore lists:

  • virtualenv/scripts/init

What actually gets ignored:

Only internal_use.png from the resources folder will be ignored. Even though app.py appears in the nested ignore file, it won't be ignored because that rule only applies within the resources/ directory and its subdirectories—but app.py actually lives in the parent directory, outside the scope of that nested ignore file.

Common Patterns

Wildcards

Rooted patterns

Negation

Directory rules

Order matters — later patterns override earlier ones.


HEAD is simply “where you are now.” Usually it points to the current branch, which in turn points to a commit.

Like all things in .git's internals, HEAD is just stored in a file.


Reflog — Your Emergency Undo Button

https://git-scm.com/docs/git-reflogarrow-up-right

git reflog tracks where HEAD and branches have pointed over time. It's pronounced ref-log, not re-flog and is kinda like git log but stands for "reference log" and specifically logs the changes to a "reference" that have happened over time. Even commits from deleted branches still appear here (temporarily).

Reflog uses a different format to show the history of a branch or HEAD: one that's more concerned with the number of steps back in time. For example:

reflog
meaning

HEAD@{0}

where HEAD is now

HEAD@{1}

where HEAD was 1 move ago

HEAD@{2}

where HEAD was 2 moves ago

HEAD@{3}

where HEAD was 3 moves ago

...

...

Recovering a Deleted Commit or File

  1. Find the commit in the reflog.

  2. Inspect it using git cat-file -p <hash>.

  3. Use the tree → blob hashes to locate the file.

  4. Restore the file manually.

  5. Commit the recovered file.

Reflog is often the only way to recover work after a destructive reset.


Data Recovery

You've just deleted a branch that contained a one-of-a-kind commit. Everything's gone forever, right?

Not quite.

Remember how git reflog tracks the history of where all your references have pointed? Now's the moment to use that feature.

Follow these steps to retrieve lost data:

  1. Run git reflog to see the history of where your HEAD has been positioned. Locate the commit hash that matches the "drafts" modification you lost.

  2. Recall the git cat-file -p command from the initial course? Pass it the abbreviated commit hash from the reflog output.

  3. Extract the tree hash from that commit to locate the blob hash for the notes.txt document.

  4. Run the command one final time to display the content of the removed document.

  5. Rebuild the notes.txt document at the repository root with identical content.

  6. Save that document with the commit message: B: restoration

Alternative to the above method:

Merging Made Simple

Working with Git's low-level commands is incredibly cumbersome. We had to manually copy values and execute the cat-file operation three separate times!

I wouldn't suggest using that approach in practice, but I wanted to emphasize that the underlying plumbing commands are always available when you need them.

Fortunately, there's a much simpler method. The git merge command accepts what's called a "commit-like reference":

git merge <commit-like reference>

A "commit-like reference" is anything that points to a commit (such as a branch name, tag, commit hash, or HEAD@{2})

Put another way, instead of this lengthy process:

We could have simply executed:


What "Commit-like References" Mean in Git

In Git, a "commit-like reference" (or "commitish") refers to any expression that Git can interpret as pointing to a commit. Examples include:

  • A full commit hash (e.g. f8e9d2a)

  • A branch identifier (e.g. develop, bugfix-auth)

  • A tag label (e.g. v2.1)

  • A reflog pointer (e.g. HEAD@{3})

  • Relative position indicators (e.g. HEAD^, develop~2)

In other words, a "commit-like reference" isn't limited to the actual commit hash itself—it encompasses any notation that allows Git to locate a particular commit object, whether directly through its hash or indirectly through a named reference or relative position.


Understanding Merge Conflicts

Conflicts occur when two incompatible modifications lack a shared ancestor commit.

Fixing conflicts requires manual intervention. When they arise, Git flags the problematic files and prompts you to resolve them by editing the content directly. Here's what it looks like:

Your text editor may color-code these conflict markers for visibility, but fundamentally, you're just working with plain text. The upper portion, between <<<<<<< HEAD and =======, shows your current branch's version. The lower portion, between ======= and >>>>>>> develop, displays the incoming branch's version. HEAD refers to the most recent commit on your active branch.

Single-file conflicts are fairly straightforward to handle. But what about scenarios with conflicts across multiple files?

Handling Multiple Conflicts

Using Checkout for Conflict Resolution

We've been manually editing files to fix conflicts, but Git actually provides built-in utilities to streamline this process.

The git checkout command can select specific versions during a conflict using the --theirs or --ours options:

  • --ours replaces the file with the version from your current branch (the one you're merging into)

  • --theirs replaces the file with the version from the incoming branch (the one being merged in)

To clarify:

If you want to keep your current branch's (develop's) version, use:

If you want to accept the incoming branch's (remove_users) version, use:

You can specify individual file paths for which version to keep.

About Conflict Commit Messages

You might have noticed that when resolving merge conflicts, you don't automatically get the typical merge commit message format like:

Merge branch 'develop' into add_products

Instead, you must manually write a message. Yes, more typing. This actually makes sense: Git can't know how you resolved the conflict, so it encourages you to document your resolution approach through a descriptive custom commit message.


Handling Rebase Conflicts

Let's be honest: much of rebase's negative reputation stems from conflicts! Don't worry. With a little practice, you'll handle them just fine.

Rebasing can feel intimidating because it modifies Git's history, meaning careless actions could result in irreversible data loss. However, once you grasp the underlying mechanics, you'll find it creates a cleaner, more comprehensible Git timeline for both you and your team.

Here's the typical real-world scenario:

  1. You create a new branch, let's call it update_styles, branched from develop.

  2. While you're working, a teammate merges their modifications into develop.

  3. You finish your work, and it turns out you modified the same files (and specific lines) that your teammate changed.

  4. You submit a Pull Request to merge (or rebase) update_styles into develop, and Git reports a conflict.

  5. You address the conflict on your branch.

  6. You finalize the Pull Request with the conflict now resolved.


When you encounter a rebase conflict

While you'll definitely see a conflict, you might notice something... unexpected. This time, the HEAD pointer contains develop's modifications rather than update_styles's! That's because rebase switches to the target branch—in this case develop—so it can replay the modifications from update_styles on top of it.

If you had merged develop instead of rebasing, HEAD would still reference update_styles because Git doesn't perform branch switching behind the scenes during a merge. With rebase, however, you need to think somewhat backwards... --theirs represents the update_styles branch which, paradoxically, contains what we typically consider "our" modifications.

Running git branch might show:

During an active rebase, you're in a special mode called "detached HEAD." This temporary state lets you handle the conflict before proceeding with the rebase.


Addressing the Conflict

The same git checkout --theirs and git checkout --ours commands we used with merge work for resolving rebase conflicts.

You may also notice that your editor (depending on which one you use) provides UI elements to assist with conflict resolution. For instance, in VS Code you'll see options like:

"Accept incoming change" is equivalent to git checkout --theirs, and "Accept current change" is equivalent to git checkout --ours.

  • During a merge, --ours means your active branch, but during a rebase, --ours means the branch you're rebasing onto. So, if you're rebasing onto develop while working from another branch, select --ours to preserve the "develop" branch's modifications.

Here's roughly how it should work when you have conflicting modifications to the same file across both branches (in this scenario "develop" and "update_styles"):

Example

  1. Execute the appropriate git checkout command to replace the update_styles branch's modifications with those from develop.

  2. After editing the file, add it, but don't commit the modifications

    • With rebase conflicts, unlike merge conflicts, we don't commit to resolve the issue. Instead, we --continue the rebase.

  3. Execute git rebase --continue to proceed with the rebase.

  4. Run git log to view the updated commit timeline. Notice that the J commit (the one containing the modifications we discarded) has vanished... we'll explore why shortly.

What just happened??? Why did our J: commit simply disappear!?!

Let's examine what occurred during our rebase conflict resolution:

  1. We initiated a rebase of update_styles onto develop, which means we're rewriting the timeline of update_styles to incorporate all modifications from develop.

  2. We essentially eliminated all modifications from the J commit by choosing to preserve the modifications from develop instead of update_styles.

  3. We continued the rebase, and Git determined that the J commit served no purpose, and since we're already rewriting history, it simply removed it.

If our modifications had been more complex—say we had preserved some modifications from the J commit while overwriting others—then Git would have retained the J commit in the timeline.


Repeated Conflict Resolution Setup

A frequent criticism of rebase is that you might need to manually fix identical conflicts repeatedly. This becomes particularly noticeable when working with a lengthy feature branch, or even more commonly, when managing several feature branches that are all being rebased onto develop.

RERERE Comes to the Rescue

The git rerere feature is somewhat obscure. The acronym stands for "reuse recorded resolution" and, true to its name, it enables Git to memorize how you've handled a specific conflict chunk so that when it encounters the identical conflict again, Git can automatically apply your previous solution.

Put simply, when activated, rerere remembers your conflict resolution approach (applicable to both rebasing and merging) and will automatically implement that same resolution upon encountering the identical conflict again. Quite handy, isn't it?

Activating RERERE:

Example scenario with 3 branches:

  • 2 branches containing modified files and 1 primary branch that we'll rebase onto.

Task

  1. Activate the rerere feature by executing this command in your repository: git config --local rerere.enabled true

  2. Switch to preferences and rebase it onto develop. You should observe something like:

See how it's documenting our actions as we handle the conflict? This demonstrates rerere working!

  1. Accept "both" modifications for the conflict, resulting in a file like:

  1. Stage the changes and proceed with the rebase. Keep the commit message unchanged (M: ...). Note that Git might automatically launch your editor to modify the commit message—simply save and exit to continue. Upon completion you should see:

Notice Git is storing how we handled the conflict!

  1. Finally, switch to the preferences2 branch and rebase it onto develop. It should automatically resolve the conflict and display a message like:

Simply stage the changes and continue the rebase, once more keeping the commit message unchanged (M: ...).


Accidental Commit

Remember how with merge conflicts you commit the fix, but with rebase conflicts you use --continue to proceed?

For the cases where you've mistakenly committed when resolving a rebase conflict instead of continuing...

  • How to Fix the Mistake

If you accidentally commit while resolving a rebase conflict, simply run:

git reset --soft HEAD~1

The --soft option preserves your modifications while reversing the commit. Afterward, you can proceed with --continue for the rebase as intended.


Git Squashing

Every dev team will have different standards and opinions on how to use Git. Some teams require all pull requests to contain a single commit, while others prefer to see a series of small, focused commits.

If you join a team that prefers a single commit, you will need to know how to "squash" your commits together. To be fair, even if you don't need a single commit, squashing is useful to keep your commit history clean.

What Is Squashing?

It's exactly what it sounds like. We take all the changes from a series of commits and squash them into a single commit.

How to Squash

Perhaps confusingly, squashing is done with the git rebase command! Here are the steps to squash the last n commits:

  1. Start an interactive rebase with the command git rebase -i HEAD~n, where n is the number of commits you want to squash.

  2. Git will open your default editor with a list of commits. Change the word pick to squash for all but the first commit.

  3. Save and close the editor.

The -i flag stands for "interactive," and it allows us to edit the commit history before Git applies the changes. HEAD~n is how we reference the last n commits. HEAD points to the current commit (as long as we're in a clean state) and ~n means "n commits before HEAD."

Why Does Rebase Squash?

Remember, rebase is all about replaying changes. When we rebase onto a specific commit (HEAD~n), we're telling Git to replay all the changes from the current branch on top of that commit. Then we use the interactive flag to "squash" those changes so that Rebase will apply them all in a single commit.


Force Push

So we did some naughty stuff. We squashed main which means that because our remote main branch on GitHub has commits that we removed, git won't allow us to push our changes because they're totally out of sync.

The git pusharrow-up-right command has a --force flag that allows us to overwrite the remote branch with our local branch. It's a very dangerous (but useful) flag that overrides the safeguards and just says "make that branch the same as this branch."

git push origin main --force


Squashing PRs

So we did a weird thing (squash commits on main), now let's do the common thing: squash all the commits on a feature branch for a pull request. If your team prefers single-commit-pull-requests, this will likely be your workflow:

  1. Create a new branch off of main.

  2. Go about your work on the feature branch making commits as you go.

  3. When you're ready to get your code into main, squash all your commits into a single commit.

  4. Push your branch to the remote repository.

  5. Open a pull request from the feature branch into main.

  6. Merge the pull request once it's approved.


Git Stash

The git stash commandarrow-up-right records the current state of your working directory and the index (staging area). It's kinda like your computer's copy/paste clipboard. It records those changes in a safe place and reverts the working directory to match the HEAD commit (the last commit on your current branch).

To stash your current changes and revert the working directory to match HEAD:

To list your stashes:

Pop

Stash has a few optionsarrow-up-right, but the ones that you will use most are:

The pop command will (by default) apply your most recent stash entry to your working directory and remove it from the stash list. It effectively undoes the git stash command. It gets you back to where you were.

What is the Stash?

The git stash command stores your changes in a stack (LIFO) data structurearrow-up-right. That means that when you retrieve your changes from the stash, you'll always get the most recent changes first.

A "stash" is a collection of changes that you've not yet committed. They might just be "raw" working directory changes, or they might be staged changes. Both can be stashed. So, for example, you can:

  1. Make some changes to your working directory

  2. Stage those changes

  3. Make some more changes without staging them

  4. Stash all of that

When you do, the "stash entry" will contain both the staged and unstaged changes and both your working directory and index will be reverted to the state of the last commit. It's a very convenient way to "pause" your work and come back to it later.

Multiple Stashes

You can stash changes with a message. I rarely use a stash message personally because I rarely have more than one stash entry. I imagine if you keep a crazy deep stash then messages are useful, but I find that I usually stash, and then just a few minutes or hours later, I pop it back out. That said, here's the syntax to provide a message:

Additional stashing techniques

Apply Without Removing from Stash This will apply the most recent stash changes, just like pop, but it will keep the stash in the stash list.

Remove a Stash Without Applying This will remove the most recent stash from the stash list without applying it to your working directory.

Reference a Specific Stash Most stash commands allow you to reference a specific stash by its index.



Git Revert

Where git resetarrow-up-right is a sledgehammer, git revertarrow-up-right is a scalpel.

A revert is effectively an anti commit. It does not remove the commit (like reset), but instead creates a new commit that does the exact opposite of the commit being reverted. It undoes the change but keeps a full history of the change and its undoing.

  • Using Revert

    To revert a commit, you need to know the commit hash of the commit you want to revert. You can find this hash using git log.

    git log

    Once you have the hash, you can revert the commit using git revert.

    git revert <commit-hash>


Git Diff

The git diff commandarrow-up-right shows you the differences between... stuff. Differences between commits, the working tree, etc.

I frequently use it to look at the changes between the current state of my code and the last commit. For example:


Revert vs. Reset

  • git reset --soft: Undo commits but keep changes staged

  • git reset --hard: Undo commits and discard changes

  • git revert: Create a new commit that undoes a previous commit

  • When to Reset

    If you're working on your own branch, and you're just undoing something you've already committed, say you're cleaning everything up so you can open a pull request, then git reset is probably what you want.

  • When to Revert

    However, if you want to undo a change that's already on a shared branch (especially if it's an older change), then git revert is the safer option. It won't rewrite any history, and therefore won't step on your coworkers' toes.


Cherrypick

There comes a time in every developer's life when you want to yoink a commit from a branch, but you don't want to merge or rebase because you don't want all the commits.

The git cherry-pick commandarrow-up-right solves this.

How to Cherry Pick

  1. First, you need a clean working tree (no uncommitted changes).

  2. Identify the commit you want to cherry-pick, typically by git loging the branch it's on.

  3. Run:


Bisect

So we know how to fix problems in our code. We can either:

  1. Revert the commit with the bug (this is more common on large teams)

  2. "Fail forward" by just writing a new commit that fixes the bug (this is more common on small teams)

But there's another question:

How do we find out when a bug was introduced?

That's where the git bisect commandarrow-up-right comes in. Instead of manually checking all the commits (O(n) for Big O nerds), git bisect allows us to do a binary search (O(log n) for Big O nerds) to find the commit that introduced the bug.

For example, if you have 100 commits that might contain the bug, with git bisect you only need to check 7 commits to find the one that introduced the bug.

commits to check
max checks to find

1

1

----------------

--------------------

2

1

----------------

--------------------

10

4

----------------

--------------------

100

7

----------------

--------------------

1000

10

----------------

--------------------

10000

14

git bisect isn't just for bugs, it can be used to find the commit that introduced any change, but issues like bugs and performance regressions are a common use case.


How to Bisect

There are effectively 7 steps to bisecting:

  1. Start the bisect with git bisect start

  2. Select a "good" commit with git bisect good <commitish> (a commit where you're sure the bug wasn't present)

  3. Select a bad commit via git bisect bad <commitish> (a commit where you're sure the bug was present)

  4. Git will checkout a commit between the good and bad commits for you to test to see if the bug is present

  5. Execute git bisect good or git bisect bad to say the current commit is good or bad

  6. Loop back to step 4 (until git bisect completes)

  7. Exit the bisect mode with git bisect reset

If you're interested, the git blamearrow-up-right command can be used to see who made the change, not just when it was made.

From man git-bisect:


Worktrees

We've been saying "worktree" all throughout this course but I've been misusing it... I am sorry :(

I've been saying "worktree" when I meant "main worktree", which is more precise because you can have more than one working tree.

  • What Is a Worktree?

    A worktree (or "working tree" or "working directory") is just the directory on your filesystem where the code you're tracking with Git lives. Usually, it's just the root of your Git repo (where the .git directory is). It contains:

    • Tracked files (files that Git knows about)

    • Untracked files (files that Git doesn't know about)

    • Modified files (files that Git knows about that have been changed since the last commit)

  • The Worktree Command

    Git has the git worktree commandarrow-up-right that allows us to work with worktrees. The first subcommand we'll worry about is simple:

    git worktree list

    It lists all the worktrees you created.

List all your worktrees.

Copy/paste the output into the textbox and submit.

Linked Worktrees

We've talked about:

  1. Stash (temporary storage for changes)

  2. Branches (parallel lines of development)

  3. Clone (copying an entire repo)

Worktrees accomplish a similar goal (allow you to work on different changes without losing work), but are particularly useful when:

  1. You want to switch back and forth between the two change sets without having to run a bunch of git commands (not branches or stash)

  2. You want to keep a light footprint on your machine that's still connected to the main repo (not clone)

The Main Worktree

  • Contains the .git directory with the entire state of the repo

  • Heavy (lots of data in there!). To get a new main working tree requires a git clone or git init

A Linked Worktree

  • Contains a .git file with a path to the main working tree

  • Light (essentially no data in there!), about as light as a branch

  • Can be complicated to work with when it comes to env files and secrets

Create a Linked Worktree

To create a new worktreearrow-up-right at a given path:

Adding <branch> is optional. It will use the last part of the path as the branch name.

  1. Create a linked worktree as a sister directory to your main working tree called ultracorp. Use the default ultracorp branch.

  2. cat the contents of the linked worktree's .git file: notice that it's just a path to the main working tree!

  3. List all the worktrees from the root of the main working tree to make sure you see the new ultracorp worktree.

Delete Worktrees

You may never need to stash again! Okay, stash is still useful for tiny stuff, but worktrees are so much better for long-lived changes.

However, at some point you will need to clean up your worktrees. The simplest way is the remove subcommandarrow-up-right:

An alternative is to delete the directory manually, then prunearrow-up-right all the worktrees (removing the references to deleted directories):


Tags

We're going to wrap this course up on an easy one: tagsarrow-up-right.

A tag is a name linked to a commit that doesn't move between commits, unlike a branch. Tags can be created and deleted, but not modified.

  • How to Tag

    To list all current tags:

    git tag

    To create a tag on the current commit:

    git tag -a "tag name" -m "tag message"

MegaCorp™ is getting ready to release a new version of their enterprise software!

  1. Create a tagarrow-up-right on the latest commit (P) with the name candidate and a descriptive message.

  2. List the tags to verify it was created.

  3. Use git log to see how tags show up in the logs.


Semver

It's kinda weird to just name tags any old thing. We're developers, we like structure, sameness, and sometimes even bike-shedding.

"Semver"arrow-up-right, or "Semantic Versioning", is a naming convention for versioning software. You've probably seen it around, it looks like this:

The "v" isn't technically part of "semver", but it's often there to say "this is a version".

It has two primary purposes:

  1. To give us a standard convention for versioning software

  2. To help us understand the impact of a version change and if it's safe (how hard it will be) to upgrade to

Each part is a number that starts at 0 and increments upward forever. The rules are simple:

  • MAJOR increments when we make "breaking" changes (this is typically a big release, for example, Python 2 -> Python 3)

  • MINOR increments when we add new features in a backward-compatible manner

  • PATCH increments when we make backward-compatible bug fixes

To sort them from highest to lowest, you first compare the major versions, then the minor versions, and finally the patch versions. For example, a major version of 2 is always greater than a major version of 1, regardless of the minor and patch versions.

As a special case, major version 0 is typically considered to be pre-release software and thus the rules are more relaxed.

Conventional Tags

Tags are used for all sorts of reasons, but sometimes they're used to denote releases. In that case, tags that follow semverarrow-up-right are common.

To tagarrow-up-right:

Pretty much anywhere you can use a commit hash, you can use a tag name when working with the git CLI. It's a "commitish"arrow-up-right.

Optionally, you can push your tags up to your remote GitHub repo with git push origin --tags.


Last updated