Git Notes
Git Repository
Concepts
File States
- untracked
- staged
- committed
Commit
- snapshot of the repository at a given time
- it is not a diff
- diffs are calculated
Commits are just a file
- first two hash characters = the directory within
.git/objects - the rest is the filename
- this is to prevent inode busting from putting all commits under one directory
- commit stores entire history via pointers
Hash
-
unique; depends on every aspect of the git operation:
- author name and email
- commit
- message
- date and time
- parent commit hashes
Git Objects
- git is made of up objects in the .git/objects directory
- a commit is just one type of these objects
- these objects will match commits shown in git log
catwill show binary nonsensegit logwill output the object in hexadecimal format- git compresses files in the .git/objects directory
- git deduplicates files common across commits
- git does not change or overwrite past trees or blobs
git init # create git repo in current directory
git status # see file states
git add . # does not recursively add
git log
git --no-pager log
git --no-pager log -n 10 # prints last 10 commits NOT in the pager (less)
# other options
--oneline # puts everything on one line
--parents # shows parent commmits
--graph # shows commit/branch lines
--all # shows all branches
--decorate # branch + tag info, can be short, full,no
git log --decorate=fullgit internals
- tree: git's way of storing a directory
- blob: git's way of storing a file
# use the first 7 characters of the commit hash (4 might be sufficient)
git cat-file -p <commit hash> # this will show the tree hash git cat-file -p <tree hash> # this will show the blob hash git cat-file -p <blob> # this will show the file hash git cat-file -p <file hash> # shows contents of the filegit config
You can store any data you want in your git config files
- git might not use the data, but it can live there
- use –global and –local flags after the operation
- git config files can also be directly edited
- git is case insensitive; defaultBranch and defaultbranch are the same
Config files are located in the following places in reverse priority:
| Location | Config File |
|---|---|
| System | /etc/gitconfig |
| Global | ~/.gitconfig |
| Local | .git/config |
| Worktree | .git/config.worktree |
Local definitions will take priority over global definitions.
git config set
git config set --global user.name "Bill S. Preston, Esq."
# useless data for git, but allowed
git config set --local organization.ceo "Ted"
# --add functionally does the same thing
git config --add --global user.email "bill@email.com"git config list
git config list --local
git config list --global
cat .git/config # local data
cat ~/.gitconfig # global datagit config get
git config get <key> # <key> made up of section.value
git config get user.name # section = user, value = name
git config get --global user.name # --global goes after get/setgit config unset
git config unset <key> # <key> needs a section and value
git config unset --all <section> # unset all of the same section
git config unset user.name
git config unset user # THIS FAILS - needs a key and valueduplicates
# you can have multiples of a key
# the last one is what is tracked in git
git config set --append organization.ceo "Warren"
git config set --append organization.ceo "Carson"
git config set --append organization.ceo "Sarah"
# unset all of the same key
# otherwise git will not allow unsetting that key
git config unset --all <key>Remove a section
git config remove-section <section>
git config remove-section userBranch
- Branch is a named pointer to a specific commit.
- When you create 10 branches, you are not creating 10 copies of the project.
git branch # shows you which branch you are on
git branch -m oldname newname # rename
git config set --global init.defaultBranch mainCreate a branch
git branch <branchname>
git switch -c <branchname>
# Create a new branch off of a specific commit hash
git switch -c <branchname> <commit hash>
git checkout <branchname> # only works if <branchname> exits
git branch -d <branchname> # delete branch
git branch -D <branchname> # force delete a branch (unmerged commits)When you create a branch:
- git uses the current commit you are on as the branch base
- HEAD moves to that branch
After deleting a branch
- The commits from that branch will no longer be visible in git log
- Even with
git log --all, you will no longer see any unmerged commits from that branch - Commits will still be visible in reflog
HEAD and tip
tip = most recent commit in a branch
HEAD = commit you are working off of
HEAD usually will be the tip, but does not necessarily have to be.
The commit hash of HEAD of a branch is stored in .git/refs/heads
HEAD is stored in a file:
- .git/HEAD
- This points to the branch HEAD stored in .git/refs/heads
Merge
Typical Workflow
- Fetch or pull updated main branch
- Create a branch to do stuff
- Merge branch to main when sufficient via pull request or git merge on cli
Merge Commit
- Commit which merges two branches with diverging histories.
- Only kind of commit with two parent commits.
- Relative to Merge Base, aka best common ancestor
git merge <feature_branch_name> # usually done from main branchFast-forward merge
- Simplest
- Tip of feature branch becomes the tip of main branch
- NO MERGE COMMITS
- Feature branch is merged into the main branch prior to additional main branch commits
Rebase
Most misunderstood features of git
- Potential drawbacks for the unintiated:
- e.g. someone changes the public history of a branch which screws up a ton of stuff
Ideal scenario:
- You are working on a feature branch
- There have been updates to main
- You want to pull those updates from main onto your feature branch
- After rebasing, this allows for a fast-forward merge
Said otherwise
- Push the parent commit of the feature branch to the new tip of the main branch
- The tip of the feature branch is now a fast-forward merge away from the main branch
Allows maintaining a merge-commit-free history
- Has some benefits
- Allows using certain git features if you maintain your history this way
When on the feature branch, rebase with main
git rebase main # from the feature branchWARNING: NEVER REBASE A PUBLIC BRANCH LIKE MAIN OR MASTER
- Out of habit, it is a good idea not to rebase your own private/fork main branches
Rebase vs Merge
- Rebase will change commit hashes
-
Will have to address conflicts if rebasing:
- If there are conflicts in rebasing there would be conflicts in merging as well
- Merging does not have any benefits over rebasing in this regard
- Generally if there are no conflicts in a merge, there will be no conflicts in rebase
-
Rebase advantage:
- Reverting commits
- Only really done at scale
-
Merge advantage:
- Shows true history
-
Merge vs rebase might be enforced by some teams on the main branch
- For your own branches you can generally do whatever you want.
-
An opinionated strategy:
- Always rebase
- All merges then become fast-forward merges
- If you are in a situation where you need to revert, it becomes really easy to do.
- Squashing becomes easy
With merge conflict resolution
-
you COMMIT the resolution
git add <file> git commit [-m "Message"]
With rebase conflict resolution
-
you CONTINUE the resolution
git rebase --continue - you DO NOT commit rebase resolution
If you happen to accidentally commit rebase resolutiton
-
undo the commit
git reset --soft HEAD~1 - then continue the rebase
Undoing Changes
Git fundamentally does 3 things
- Add
- Delete
- Modify
Conflicts arise on the modify portion
- If two commits edit the same line, git will throw a conflict
- Requires personal editing and deconfliction
- Comments in code can cause conflict
git reset –soft
git reset --soft <commit hash>
git reset --soft HEAD~1 # go back one commit from HEAD- Undo the last commit and committed changes in the index (staged but not committed)
- Commit changes will be uncommited and staged
- Uncommited changes will remain staged or unstaged as before
git reset –hard
git reset --hard <commit hash>- Makes working directory and staging area match the last commit exactly
- Disregards local changes
DANGER: GIT RESET –HARD IS PERMANENT
- If we simply delete a commited/tracked file using
rm -rf filename, we can easily restore - If we use hard reset prior to committing that file, those changes are gone for good
- Still recoverable through reflog
Change Commit Message
Rather than soft resetting to change a commit message, you can just do this:
# this is a destructive item because it changes the commit hash
# only do for latest commit which is not merged to main
git commit --amendThis is a destructive operation and will change the commit hash.
Git Remote
Remotes are just git repos
- We can have remotes that reside on our local machine.
- Origin: If treating the remote as the authoritative source of truth, name it "origin"
-
Upstream:
- Sometimes this will be the authoritative.
- Origin becomes what you're working in (fork of upstream).
- You only want upstream when working with a lot of people and/or forking, etc.
git fetch
- This fetches objects and bookeeping information about a repo into
.git/objects - It does not pull the files into your local branch(es).
Remote Commands
git remote add <remote> <uri> # notice this is uri, can be a file address
git remote add origin ssh://git@forge.site/user/repo.git
git ls-remote
git push origin main # push the local main branch to origin
# push a local branch to a remote branch of a differnet name
git push origin <localbranch>:<remotebranch>
git push origin :<remotebranch> # deletes a remote branch
git log origin/master --oneline
git merge <remote>/<branchname>
git merge origin/main # merge from the main branch on originGit Pull
Pulls actual filechanges onto your local branch, not just metadata.
# ensures we merge on a pull, may or may not want this
git config set pull.rebase false
git pull origin main # pulls the main branch from originWorkflow Example:
- Remote is truth
-
Rebase by default
git config set --global pull.rebase true
Solo work:
- Everything on a single branch (main)
Team work
- Work on new-branch
- Push to origin new-branch
- Open PR
- Ask for review
- After review, hit merge on remote forge
- Delete new-branch merging
Gitignore
Example:
Adding node-modules to .gitignore will ignore every file that has exactly node-modules in its name.
It will not ignore files or directories that contain node-modules-2
Exclude file
.git/info/exclude
- Alternative to .gitignore
- Allows your personal ignored files to not be part of the repo
- Team members cannot see what kind of shady stuff you're hiding from the repo
Nested .gitignore
- .gitignore does not have to be a t the project root
- There can be
.gitignorein any directory of the project
Pattern Recognition
Patterns starting with a forward slash are anchored to that directory
- /main.py would ignore main.py in the root directory but not any other directories
- "*" matches any number of characters except forward-slash
- "#" comments
-
"!" indicates negation: negate specific files that would otherwise be ignored
,*.txt !/important.txt # negates important.txt despite the *.txt wildcard
Order matters:
- one entry can override another
-
correct:
temp/* !temp/instructions.md # this ignores all temp/ files except for instructions.md -
incorrect:
!temp/instructions.md temp/* # this overrides the above negation
What to ignore:
Ignore things that can be generated or sensitive data
- compiled code
- minified files
- dependencies (e.g. node_modules, venv, packages, etc.)
- personal or specific preferences (e.g. editor settings)
- sensitive or dangerous things (e.g. .env files, passwords, API keys, etc.)
Fork
- Not a git operation
- Provided by git forges
- Creates a copy of the original repo that you can edit
Why fork?
- Want to contribute to the project
-
Add second remote called upstream which points to original repo
git remote add upstream <uri>- Can bring in latest changes to your fork
PR tips
Review code on the actual git forge site
- Different look than your editor
- Removes bias, potentially see stuff differently
Make sure maintainers want these changes
- Look for issues
- Consider creating an issue beforehand
Add yourself as a contributor
In the contributors/ directory, add:
- A file with your forge username.txt
- A link to your forge profile in the text
Reflog
| Location | Descripion |
|---|---|
| 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 move ago |
- Reflog will show history of all operations, even destructive ones.
- Can be used to restore after hard reset or long forgotten commits.
- Records tip of branches and HEAD when updated; tracks every last step of HEAD.
Restoring a deleted but previously committed file using git cat-file:
- Use git reflog to find the commit hash of the now deleted commit.
-
Use
git cat-file -p <hash>to get the treegit cat-file -p <commithash> # gets the tree git cat-file -p <tree> # gets the blob git cat-file -p <blob> # gets the file-hash git cat-file -p <file-hash> # shows contents of file
Restoring a deleted file the fast way
# Instead of everything above, we could have just done:
git merge HEAD@{1}Conflicting Changes
- It is ok when the same line is modified in two separate commits
- it Is not ok when the same line is modified in two commits with the same parent
Merge Conflicts
The TOP changes are OURS
-
Should indicating HEAD (which is always local/ours)
<<<<<<<< HEAD
The BOTTOM changes are THEIRS
-
Should indicate the branch, e.g. main
>>>>>>>> main
You can manually accept both changes.
To reset to a specific commit:
git reset --hard <commit hash>Multi Conflict
The git checkout command can checkout the individual changes during a merge conflict using the –theirs or –ours flags.
- –ours keeps the version of the file from your current branch (the one you're on before merging)
- –theirs uses the version of the file from the branch you're merging into your current branch
git checkout --theirs <path/to/file>
git checkout --ours <path/to/file>Rebase Conflicts
Most of the bad rap rebase gets is around conflicts because it rewrites history.
Rarely will we have conflicts on our own stuff
In the "real world," what happens most often is:
- You switch to a new branch, say fix_bug, which is a copy of main.
- While you're fixing the bug, someone else merges their changes into main.
- You fix the bug, and it so happens that you edited the same files (and lines) that the other person did.
- You open a Pull Request to merge (or rebase) fix_bug into main, then git tells you there's a conflict.
- You resolve the conflict on your branch.
- You complete the Pull Request with the conflict resolved.
REMEMBER YOU SHOULD NEVER REBASE MAIN MEANING YOU SHOULD NEVER TYPE THE WORDS:
git rebase <feature_branch> # NEVER DO THISIF REBASING, YOU SHOULD ONLY TYPE:
git rebase main # rebase onto main
git rebase master # rebase onto masterThis is saying "rebasing onto main/master."
Detatched HEAD
After rebasing main on the feature branch and conflict arises:
-
Check the branch
- YOU WILL NOT BE ON ANY BRANCH
- your branch will be: (no branch, rebasing <featurebranch>)
-
This is called detached HEAD
- temporary state
- allows you to resolve the conflict before proceeding with the rebase
Resolving Conflicts
Use git checkout –theirs and git checkout –ours In a merge
- –ours refers to your current branch
-
this is most likely MAIN/MASTER
- sinced based bros will be rebasing onto a feature branch
- and only (fast forward) merging onto main/master
In a rebase
- –ours refers to the branch you're rebasing onto.
- in this case we are rebasing from the feature branch onto a different branch
- it is probably main/master
"Accept current change" is the same as
git checkout --ours <filepath>- <<<<<<
- top portion of the conflicting file
"Accept incoming change" is the same as
git checkout --theirs <filepath>- >>>>>>
- bottom portion of the conflicting file
After resolving conflicts
Add the file.
DO NOT COMMIT: YOU ARE STILL ON THE REBASE BRANCH
Instead continue the rebase:
git rebase --continueCheck the log
git log --oneline --allNOTE
- the commit we discarded is gone from git history
- still accessible with reflog
Repeat Resolution (rerere)
- "Reuse recorded resolution"
- It allows you to ask Git to remember how you've resolved a hunk conflict.
- Next time it sees the same conflict, Git can resolve it for you automatically.
- Applies to rebasing and merging
git config set --local rerere.enabled true
git config set --local rerere.enabled false # disable rerere
rm -rf .git/rr-cache # delete any remaining rerere cacheConflict After Rebase Example:
wagslane@MacBook-Pro-2 megacorp % git rebase main
Auto-merging customers/favs.md
CONFLICT (add/add): Merge conflict in customers/favs.md
error: could not apply ad9b194... K
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Recorded preimage for 'customers/favs.md' # <--- THIS LINE HERE
Could not apply ad9b194... KNOTICE:
Recorded preimage for 'customers/favs.md' # <--- THIS LINE HERERerere is now recording how to resolve the conflict
- Either accept –ours (main) or –theirs (favs branch)
- Or accept both by manually editing the file
If we recreate the same conflict from another (third) branch (favs2)
wagslane@MacBook-Pro-2 megacorp % git rebase main
Auto-merging customers/favs.md
CONFLICT (add/add): Merge conflict in customers/favs.md
error: could not apply ad9b194... K
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Resolved 'customers/favs.md' using previous resolution. # <--- THIS LINE HERE
Could not apply ad9b194... KNOTICE
Resolved 'customers/favs.md' using previous resolution. # <--- THIS LINE HERE- if you open the conflicting file, the changes have already been made to match the first one
Continue the rebase with
git rebase --continueSquashing
Squashing groups a series of sequential, small commits into one commit. This is preferred by some teams
git rebase -i HEAD~n # n is number of commits we want to squash
# HEAD is current commit, ~n is n previous commits
# -i creates an interactive rebase-
git will open editor with a list of commits
- -i flag is interactive
- Allows us to edit commit history
-
Change the word "pick" to "squash" for all but the first commit
- First commit will be at the top of the editor prompt
- Save and close the editor
Squashing Best practices
-
Identify the main branch remote
- Might have to reset this
- Most likely origin
-
Create a temporary branch
- Rebase is destructive
-
Perform the rebase on temp branch
- Emacs git-rebase-mode has its own keybindings; do not need to directly edit, just press "s" on all lines that should be changed from "pick" to "squash"
-
Delete the main branch
git branch -d main -
Rename temp branch to main
git branch -m temp_main main -
Push and reset the main branch remote
git push -u origin main # git might suggest: # git push --set-upstream origin main # which does the same thing
Force push
Force push required because this is a destructive operation on the remote main
git push origin main --forceYour git history is rewritten, but full history is still accessible with reflog.
Stash
The git stash command:
- Records the current state of your working directory and the index (staging area).
- Like your computer's copy/paste clipboard.
- It records those changes in a safe place
- Reverts the working directory to match the HEAD commit
git stash
git stash -m "jdsl v0 almost working" # adds a message
git stash list
git stash pop # applies the first stash on the stack
# applies the first stash on the stack but keeps it in the stash list
git stash apply
git stash apply stash@{2} # Apply the third (0, 1, 2) most recent stash
git stash drop # removes a stash without applying
git stash drop stash@{2} # Remove the third most recent stashThe pop command will (by default):
- Apply your most recent stash entry to your working directory
- Remove it from the stash list.
- It effectively undoes the git stash command. It gets you back to where you were.
Stash is a Stack Data structure
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.
- Stores index and worktree.
Revert
To revert a commit, you need to know the commit hash of the commit you want to revert (git log)
git revert <commit-hash>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?:
- Working on your own branch,
- Just undoing something you've already committed,
- E.g cleaning everything up so you can open a pull request
When to Revert?:
- Undo a change that's already on a shared/public branch (especially if it's an older change)
- It won't rewrite any history, and therefore won't step on your coworkers' toes.
Diff
# show the changes between the working tree and the last commit
git diff
# show the differences between the previous commit and the current state, including the last commit and uncommitted changes
git diff HEAD~1
# show the change between two commits
git diff COMMIT_HASH_1 COMMIT_HASH_2Cherry-pick
Useful if you want one change from a branch but do not want the entire branch history.
git cherry-pick <commit-hash>- 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.
git cherry-pick <commit-hash>Bisect
Git bisect is a binary search between known good and known bad commits.
- Finds the middle commit between a good and bad commit.
- You check to see if that bug is present in that middle commit.
- If not present, identify that commit as good. It then searches the middle of that and the bad commit.
- Repeat until the bug is found.
- Drastically cuts down time to find bugs.
| Commits to Check | Max Checks to Find |
|---|---|
| 1 | 1 |
| 2 | 1 |
| 10 | 4 |
| 100 | 7 |
| 1000 | 10 |
| 10000 | 14 |
-
Start the bisect with
git bisect start -
Select a "good" commit with
git bisect good <commit hash> # (a commit where you're sure the bug wasn't present) -
Select a "bad" commit with
git bisect bad <commit hash> # (a commit where you're sure the bug was present) -
Git will checkout a commit between the good and bad commits. You must test to see if the bug is present in that commit.
git show HEAD - Execute
git bisect goodorgit bisect badto identify the current commit as good or bad. - Loop back to step 4 (until git bisect completes).
-
Exit the bisect mode with git bisect reset.
git bisect reset
Then, take action to address the commit:
# you want to revert the commit that introduced the bug - first "bad commit"
# you do not want to revert the last good commit
git revert <bad_commit_hash>You can refer to commits as HEAD
- After the first bisect, you are really just saying HEAD is good or bad
-
For each iteration you simply manually check the file for the bug
git show HASH git show HEAD # most likely -
git has no way of knowing what is the bug
- You don't "tell" git what the bug is and/or what to search for.
- All git is doing is finding the middle commit.
Automating git bisect
From the git bisect man page:
Bisect run If you have a script that can tell if the current source code is good or bad, you can bisect by issuing the command:
$ git bisect run my_script arguments
Note that the script (my_script in the above example) should exit with code 0 if the current source code is good/old, and exit with a code between 1 and 127 (inclusive), except 125, if the current source code is bad/new.
This could be accomplished by a script that:
- searches a file for whatever the problem is
- returns 0 if the source is good
- returns 1+ if the source is bad
For example:
#!/bin/sh
# bisect-script.sh
if grep -q "SCANNING" "scripts/scan.sh"; then
exit 1
else
exit 0
fi- save and mark this script as executable somewhere in the repo
- mark your good and bad initial commits
Then run:
git besect run ./bisect-script.shWorktree
A worktree (or "working tree" or "working directory"):
- The directory on the local filesystem where code tracked in Git lives.
- Usually just the root of your Git repo (where the .git directory is).
-
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
git worktree listStash is still useful for small and short-lived changes. Worktrees are better for long-lived changes.
Worktrees accomplish a similar goal as stash, branch, clone, etc:
- Allow you to work on different changes without losing work.
Linked worktrees but are particularly useful when 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).
- Keep a light footprint on your machine that's still connected to the main repo (not a clone).
The Main Worktree
- Contains the .git directory with the entire state of the repo
- Heavy (lots of data)
- A new main working tree requires a git clone or git init.
Linked Worktree
- Contains a .git file with a path to the main working tree
- Light (essentially no data), about as light as a branch.
- Can be complicated to work with when it comes to env files and secrets.
git worktree add <path> <branch> # create a new linked worktreeNo Duplicate Branches
Linked worktrees behave just like a "normal" git repo. You can:
- create new branches
- switch branches
- delete branches
- create tags
- etc
BUT there is one thing you cannot do:
- you cannot work on a branch that is currently checked out by any other working tree (main or linked worktree).
Example: try to switch to main/master from a linked worktree.
- tldr; you can't
Upstream
Linked Worktrees are tracked in the .git/worktrees directory
When you make a commit in a linked worktree, that commit is automatically reflected in the main worktree Why this makes sense:
- The linked worktree doesn't have a .git directory
- It is not a separate repository.
- It is just a different view of the same repository.
You can almost think of a linked worktree as just another branch in the same repo but with its own space on the filesystem.
Delete Worktrees
git worktree remove <worktree name>NOTE
- This deletes the worktree: it removes the worktree directory from the filesystem
- It does not remove the branch.
- Run
git branchin the main git repo and you will see the branch without the "+" prefix which a worktree will have.
Alternatively:
- Delete the worktree directory manually
-
Then prune all worktrees
git worktree prune
Tags
A tag is a name linked to a commit that does not move between commits, unlike a branch.
Tags can be created and deleted, but not modified.
git tag # lists all tags
git tag -a "tag name" -m "tag message" # create a tag on the current commitSemver - Semantic Versioning
Naming convention for software - vMajor.Minor.Patch
- Major: breaking changes
- Minor: safe features
- Patch: safe bug fixes
It has two primary purposes:
- To give us a standard convention for versioning software
- To help us understand the impact of a version change: if it's safe (how hard it will be) to upgrade to
Conventional Tags
Tags are used for all sorts of reasons. Sometimes they're used to denote releases. Tags that follow semver are common.
git tag -a v3.10.2 -m "Fixed a lil bug"