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, notC:/Users/...).Result: Your local folder will contain only the
.gitfolder 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 commitsgit log --oneline— compact one-commit-per-line summarygit log --oneline --graph --all— visualize history as a branching graphgit cat-file -p <hash>— inspect the contents of a commit object
Example:
Git Configuration
Git settings exist in four scopes, each overriding the previous:
system — applies to all users on the device (
/etc/gitconfig)global — applies to your user (
~/.gitconfig)local — applies to the repository (
.git/config)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.namegit config --list: display all current global configuration settings.git config --list --local: display all current local configuration settingsgit 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-Nutshell
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 ref 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 --oneline. 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 --onelineOr 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 --parentsEach asterisk
*represents a commit in the repository. There are multiple commit hashes on each line because the--parentsflag 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 b8dfd64is our recent merge commit. The first hash,89629a9is 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_classicsbranch and themainbranch 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/headsdirectory. 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-Merging
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:
Find the "merge base" commit, or "best common ancestor" of the two branches. In this case,
B.Replay the changes from
main, starting from the best common ancestor, into a new commit.Replay the changes from
featureontomain, starting from the best common ancestor.Records the result as a new commit, in our case,
F.Fis special because it has two parents,CandE.
Fast-Forward Merge
https://git-scm.com/docs/git-merge#_fast_forward_merge
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 commit is created.
This is a common workflow when working with Git on a team of developers:
Create a branch for a new change
Make the change
Merge the branch back into
main(or whatever branch your team dubs the "default" branch)Remove the branch
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 rebase and git merge 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 reset 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/branchFor example, if you wanted to see the commits of a branch named
remote_branchfrom a remote namedoriginyou would run::git log origin/remote_branchOr:
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.pngapp.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
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-reflog
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:
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
Find the commit in the reflog.
Inspect it using
git cat-file -p <hash>.Use the tree → blob hashes to locate the file.
Restore the file manually.
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:
Run
git reflogto see the history of where yourHEADhas been positioned. Locate the commit hash that matches the "drafts" modification you lost.Recall the
git cat-file -pcommand from the initial course? Pass it the abbreviated commit hash from the reflog output.Extract the
treehash from that commit to locate theblobhash for thenotes.txtdocument.Run the command one final time to display the content of the removed document.
Rebuild the
notes.txtdocument at the repository root with identical content.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:
--oursreplaces the file with the version from your current branch (the one you're merging into)--theirsreplaces 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:
You create a new branch, let's call it
update_styles, branched fromdevelop.While you're working, a teammate merges their modifications into
develop.You finish your work, and it turns out you modified the same files (and specific lines) that your teammate changed.
You submit a Pull Request to merge (or rebase)
update_stylesintodevelop, and Git reports a conflict.You address the conflict on your branch.
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,
--oursmeans your active branch, but during a rebase,--oursmeans the branch you're rebasing onto. So, if you're rebasing onto develop while working from another branch, select--oursto 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
Execute the appropriate
git checkoutcommand to replace theupdate_stylesbranch's modifications with those fromdevelop.After editing the file,
addit, but don't commit the modificationsWith
rebaseconflicts, unlike merge conflicts, we don'tcommitto resolve the issue. Instead, we--continuethe rebase.
Execute
git rebase --continueto proceed with the rebase.Run
git logto view the updated commit timeline. Notice that theJcommit (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:
We initiated a rebase of
update_stylesontodevelop, which means we're rewriting the timeline ofupdate_stylesto incorporate all modifications fromdevelop.We essentially eliminated all modifications from the
Jcommit by choosing to preserve the modifications fromdevelopinstead ofupdate_styles.We continued the rebase, and Git determined that the
Jcommit 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
Activate the
rererefeature by executing this command in your repository:git config --local rerere.enabled trueSwitch to
preferencesand rebase it ontodevelop. You should observe something like:
See how it's documenting our actions as we handle the conflict? This demonstrates rerere working!
Accept "both" modifications for the conflict, resulting in a file like:
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!
Finally, switch to the
preferences2branch and rebase it ontodevelop. 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:
Start an interactive rebase with the command
git rebase -i HEAD~n, wherenis the number of commits you want to squash.Git will open your default editor with a list of commits. Change the word
picktosquashfor all but the first commit.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 push 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:
Create a new branch off of
main.Go about your work on the feature branch making commits as you go.
When you're ready to get your code into
main, squash all your commits into a single commit.Push your branch to the remote repository.
Open a pull request from the feature branch into
main.Merge the pull request once it's approved.
Git Stash
The git stash command 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 options, 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 structure. 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:
Make some changes to your working directory
Stage those changes
Make some more changes without staging them
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 reset is a sledgehammer, git revert 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 logOnce you have the hash, you can revert the commit using
git revert.git revert <commit-hash>
Git Diff
The git diff command 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 stagedgit reset --hard: Undo commits and discard changesgit revert: Create a new commit that undoes a previous commitWhen 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 resetis 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 revertis 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 command solves this.
How to Cherry Pick
First, you need a clean working tree (no uncommitted changes).
Identify the commit you want to cherry-pick, typically by
git loging the branch it's on.Run:
Bisect
So we know how to fix problems in our code. We can either:
Revert the commit with the bug (this is more common on large teams)
"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 command 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.
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:
Start the bisect with
git bisect startSelect a "good" commit with
git bisect good <commitish>(a commit where you're sure the bug wasn't present)Select a bad commit via
git bisect bad <commitish>(a commit where you're sure the bug was present)Git will checkout a commit between the good and bad commits for you to test to see if the bug is present
Execute
git bisect goodorgit bisect badto say the current commit is good or badLoop back to step 4 (until
git bisectcompletes)Exit the bisect mode with
git bisect reset
If you're interested, the git blame 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
.gitdirectory 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 worktreecommand that allows us to work with worktrees. The first subcommand we'll worry about is simple:git worktree listIt 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:
Stash (temporary storage for changes)
Branches (parallel lines of development)
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:
You want to switch back and forth between the two change sets without having to run a bunch of
gitcommands (not branches or stash)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
.gitdirectory with the entire state of the repoHeavy (lots of data in there!). To get a new main working tree requires a
git cloneorgit init
A Linked Worktree
Contains a
.gitfile with a path to the main working treeLight (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 worktree at a given path:
Adding <branch> is optional. It will use the last part of the path as the branch name.
Create a linked worktree as a sister directory to your main working tree called
ultracorp. Use the defaultultracorpbranch.catthe contents of the linked worktree's.gitfile: notice that it's just a path to the main working tree!List all the worktrees from the root of the main working tree to make sure you see the new
ultracorpworktree.
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 subcommand:
An alternative is to delete the directory manually, then prune all the worktrees (removing the references to deleted directories):
Tags
We're going to wrap this course up on an easy one: tags.
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 tagTo 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!
Create a tag on the latest commit (P) with the name
candidateand a descriptive message.List the tags to verify it was created.
Use
git logto 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", 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:
To give us a standard convention for versioning software
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 semver are common.
To tag:
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".
Optionally, you can push your tags up to your remote GitHub repo with git push origin --tags.
Last updated