<font color="#ffff00">⚠</font> I need to restructure these to make them usable for anyone else; for now they're just my raw notes.
# Introduction
I remember a physicist friend of mine saying that while Einstein's *Special* Theory of Relativity was cool, it followed naturally from work others had done, and wasn't a work of genius. He said that Einstein's genius was shown in producing his *General* Theory of Relativity, which was a radical departure for physics.
Studying git, I'm coming to a similar conclusion about Linus Torvalds: his creation of Linux was cool, but it was straightforward—it wasn't anything anyone hadn't done before—he “just” built another UNIX clone, but in open source, and on a small machine. Linus' *real* genius was in creating git, which was a complete break from the way version control had been thought about before.
A word about git tutorials: there are a lot of them out there… but most of them seem to have been written by people who didn’t understand git in much depth themselves (kind of like *this* document). The tutorials are full of sequences of commands that could be done more directly. They also tend to be “unmotivated”—they don’t explain *why* git features are the way they are—explanations that would make it easier to understand how to use those features. Further, they almost always shy away from explaining what git is *really* doing behind the scenes, using imperfect metaphors instead.
The Git Book ([https://git-scm.com/book](https://git-scm.com/book)) is probably the best source of authoritative information.
# Some Good Diagrams
Git explained through lots of good diagrams: **[https://dev.to/nopenoshishi/understanding-git-through-images-4an1](https://dev.to/nopenoshishi/understanding-git-through-images-4an1)**
Basic Git workflows ([source](https://xcancel.com/NikkiSiapno/status/1593882400983072769)):
![[git-flow-diagram-1.png]]
Another view of basic Git workflows ([source](https://mukulrathi.com/git-beginner-cheatsheet/)):
![[git-basic-workflow-2.png]]
Yet another view of Git workflows ([source](https://en.wikibooks.org/wiki/Git/Introduction)):
![[Git_operations.svg|537]]
Multiple users working on a single remote repo ([source](https://bytebytego.com/guides/git-workflow/)):
![[git-multiple-users.png]]
# Key Git Concepts
* **Distributed** version control: you have the **full** repository and its entire history, including all branches, on your own machine; **all** activities against that repository are **local**; this makes everything fast, and everything works offline. There's a separate set of actions (clone, pull, and push) for causing your local repository to interact with remote repositories.
* SVN and CVS (version control systems that were popular pre-git) don’t work this way—you have a local working directory only, not a full copy of the repository, and all users’ actions against the repository are processed by a single remote server, making them slow.
* **No git repository is inherently more or less authoritative than any other; there is no hierarchy**. You decide whether and where to push and pull from other repositories on your own. The “official” Linux kernel repository is Linus’ repository, only because people trust him to have the master version; he pulls only from contributors he trusts, who pull from those they trust, etc.—git is inherently based on a “web of trust”... but it’s not an explicit web: there’s no config file that stipulates this, only convention.
* Github modifies this somewhat: a Github repo is obviously more authoritative than those that pull from it… but among forks, branches, and clones, there’s still no hierarchy.
* Git doesn't assume or force anything about your workflow and how you interact with commits—it was designed to give you tons of flexibility. It gets this flexibility by adding a third "area" between your workspace and the repository: "staging". If you have a simple workflow you can ignore staging, but if you want you can use it to get very fine-grained control over how and when things are committed.
* The above two items mean that whereas you would just "commit" in SVN, to get the same effect in git you have to (1) "add" then (2) "commit" then (3) "push".
* Another way to think about this: SVN (and most other VCS) impose a specific workflow on users, and because users are constrained to that workflow, the command set is simpler. To give you complete flexibility, git’s commands are at a lower level, and you have to string these lower-level functions together to accomplish the workflow you want. It’s a tradeoff of higher complexity for a more flexible workflow. Think of it like the trade-off between high-level programming languages versus assembly language.
* Git branches are just pointers (so they’re fast and cheap) versus other version control systems, which are full copies of files and histories (slow and expensive).
* Github isn't just a cloud-based git repository—it adds its own semantics beyond git (like "fork" and "pull request") to simplify collaborative development.
* It turned PRs into actual things; git itself doesn’t know anything about PRs.
* It added Github Actions to support CI/CD.
* It added an issue tracker and wiki to projects.
Git has some different design targets than traditional version control systems:
* It is explicitly designed to be distributed, offline, and collaborative
* It is designed to make branching and merging cheap and easy
Why is git so complicated?
* Because it makes **no assumptions about authority**—all repositories are equal in its eyes, so the user has to tell git more about how you want it to treat them.
* Because it **imposes no workflow**—so the user has to tie together lower-level operations in a way that suits their purposes.
**One thing Git is *not* good at: handling binaries**. It’s designed for text files, and its cleverness breaks down when it has to deal with large binary files that it can’t diff. That’s why *videogame companies usually don’t use git*: games have too many binary assets (like artwork, textures, etc.) that they need to keep under version control, and git sucks at version control for binary files.
## Terminology inside of Git
* The "working directory" is known as "the **tree**".
* "Staging" is known as "the index".
* The repository ("repo") is known as "the **object database**"—it's an immutable append-only database that is occasionally garbage collected.
* Files are identified using hashes of their contents—not their names.
* There's no concept internally of files having different versions—they're entirely different objects.
# My Git Configuration
There's a system-wide /etc/gitconfig file, each user has a personal "global" \~/.gitconfig file, and each repository can have a local .gitconfig file that overrides the global configuration.
Here's how I set up my own .gitconfig (running these commands sets options inside .gitconfig for you):
```bash
git config --global user.name "Forrest Thiessen"
git config --global user.email
[email protected]
git config --global core.editor emacs
git config --global credential.helper store
git config --global format.pretty oneline
```
Note that `credential.helper store` will store your credentials in plaintext on your local machine—so be careful with it. (There’s a way to do this with ssh keys, too, which is more secure).
# Creating a Repository
Create an empty local repository:
```bash
cd <directory>
git init
```
This just sets up an empty git database inside the directory—that's it.
Each repository can have a .gitignore file that tells git to ignore specific files in the working directory: they won’t be checked in, even if the rest of the directory is.
# Staging and Committing
The staging area starts out as a copy of HEAD on the current branch. As you git add files to it, you put copies from the working directory into the staging area. When you git commit, the whole staging area becomes a new HEAD node in the current branch of the repository.
```bash
git add <file> # adds a copy of the file to the staging area.
git commit # saves a permanent snapshot of the contents of the staging area
git push # pushes a commit snapshot to a remote repository
```
A mental model mistake I made was thinking the repository was remote--git is a DVCS (D=Distributed)—you have your own **full** repository **locally**.
git init creates a **local** repository, and git commit does commits to that repository. Only when you're doing **distributed** version control do you need a remote repository—and git push pushes your local repository status to the remote repository. You can do everything you need—the full scope of Git’s features—locally, without ever doing a git push.
You can just commit changes in the working directory directly by doing git commit \-a. However, that only looks at files that are *already* in the repository. If you add new files you need to git add them, first. git add also allows you to have more granular control over what you package up into a commit—you can add and remove individual files until you're happy with what's in staging, then commit. For my style, though, I don't normally need that level of control, so git commit \-a will be common.
Copy the *current state* of a file (not the file itself) into the staging area, to stage files for the next commit to the repository.
```bash
git add <file(s)>
```
Important: if you make further changes to the file at this point, those changes are *not* in the staging area and won't be committed (unless you git add again).
See what git thinks is going on with files in the working directory and staging area
```bash
git status
git status -s # short version
```
Commit files in staging to the repository
```bash
git commit -m "Commit message describing what you're committing"
```
Or, automatically stage and directly commit all tracked files in the working directory, without needing to manually stage them first.
```bash
git commit -a -m "Commit message"
```
Note: this only commits files that were already in staging. If you created new files, you’ll need to `git add` them, before they will be included in the commit.
Undo working directory changes and staging; restore things to repository state
\[NOTE: Is that correct? Or does reset just unstage everything and leave the working directory alone?\]
```bash
git reset --hard HEAD
```
Notes:
* Most commands operate on HEAD unless you give another argument
* If you’re configured correctly, remote commands operate on origin unless you give another remote
* `Origin` is just an alias to whatever remote repo Git thinks is appropriate. Some commands will set it for you. For example, if you `git clone` a remote repository, origin is set to that remote repository.
Why have a staging area at all? It makes it so you can carefully craft commits exactly the way you want--including in ways that are unrelated to the way you worked on the code. Without it you'd be forced to do one thing, commit it, do another, commit it, etc... but with a staging area you can work on the code as a whole, then break changes to different parts of it up into separate commits... if you want to.
See a history of commits:
```bash
git log
git log --oneline
git log --oneline --since='5 minutes ago' --max-count=5
```
Restore the working directory to the state as of a particular commit
```bash
git checkout <first 7 characters of commit hash OR branch name ("master") OR tag>
```
Restore a particular file in the working directory to its state in the staging area
```bash
git checkout <file>
```
"master" (now usually called "main" instead) is the default branch within a repository. HEAD is the last commit node in whatever branch you're currently working with.
Name the current HEAD node
```bash
git tag <name>
```
You can just type `git tag` by itself to get a list of current tags.
Remove a file from a repo
```bash
git rm <file name> # Deletes the file *and* stages the change
```
Rename a file
```bash
git mv <old name> <new name> # Moves the file *and* stages the change
```
Git is aware of directories, but it doesn’t care much about them—it’s focused on files, and an empty directory will not be recorded by git. *Directories only show up in git if there are files in them*.
# Branching
A branch is a pointer to a commit object; every time you do a new commit, the new commit has a pointer that points to its predecessor, and the branch pointer advances to the new commit.
“master” is the default name of your main branch. {Update: now days people call it “main” instead of “master”}
HEAD is a pointer to the end of the branch you’re currently on.
Create a new branch:
```bash
git branch <new branch name>
```
Creating a branch doesn’t move you to the new branch, though; to move to the new branch (i.e. to move the HEAD pointer to the end of the new branch, and to update the files in your working directory to match):
```bash
git checkout <branch>
```
Alternately, you can create a branch and check it out in one step:
```bash
git checkout -b <new branch>
```
Note: git won’t let you checkout a different branch if you have uncommitted files in the staging area.
When you want to merge two branches together:
```bash
git merge <branch you want to merge into your current branch>
```
Note: merging one branch into another updates the “to” branch, but it doesn’t do anything to the “from” branch—it’s still there for possible future use, unless you delete it.
“Squash merge”: Turns all the individual commits on a branch and turns them into a *single* commit on the target branch. Instead of tons of work-in-progress commits showing up on main, you just get a single clean commit. PRs are commonly squash-merged:
```bash
git merge --squash <branch you want to merge into your current branch>
```
To delete a branch:
```bash
git branch -d <branch to delete>
```
List of all branches:
```bash
git branch
git branch -a # show all branches both locally and remote
```
*My branch was still on GitHub, even after I deleted it locally and pushed. Did these, recommended by Stack Overflow:*
```bash
git remote prune origin
git branch -d -r origin/messing-around # got rid of local tracking
```
*They didn’t work either—I was able to finally delete the branch using the GitHub UI*
## The Stash
Sometimes you want to switch to a different branch, do something quick, then come back to the original branch. The "stash" is a "stack" you can push your current branch onto so you can go work on something else, then "pop" your original branch off the stack again. It's a true stack—you can push/pop multiple levels onto/off of it.
To save your current state without committing by pushing it onto your “stash” stack:
```bash
git stash
```
This pushes your current working directory onto the stash stack and restores your working directory to its last clean state.
To see what’s on your stash stack:
```bash
git stash list
```
To pop the stack:
```bash
git stash apply
git stash apply --index # Restore staging as well (apply normally doesn’t)
git stash apply stash@{2} # Restore a state not on the top of the stack
git stash drop stash@{2} # Delete a state on the stack without restoring it
```
Turn a stash into a branch
```bash
git stash branch <new branch name>
```
# Pull Requests
## History
In the “old days” before GitHub, a collaborator who wanted to submit code to project would build it in a branch on their own local copy of the owner’s repo, then run
```bash
git request-pull <start-commit> <url> <end-commit>
```
This would generate a text message (a request that they `git pull` your branch into their own master repo) that the contributor could manually email to the owner.
The owner would receive the email and there would be discussion back and forth on email, and perhaps on a mailing list, about the request. When the owner was ready to accept the contribution, they’d run
```bash
git pull # this would pull the contributors branch into the owner’s repo
git merge # this would merge the newly pulled branch into the owner’s main
```
*The essence of the process was the contributor doing a `request-pull`, and then the owner doing the requested `pull`… so this was called a “pull request”, or “PR”.* That's where the term "PR" comes from.
In the pure git world, a PR is just an email message—there’s no mechanical logic or history behind it, and the git repository itself doesn’t know anything about it. Indeed, there’s no reason the owner couldn’t do the `git pull` directly without the email from the contributor
## GitHub
GitHub combined this flow, including email discussions, into an integrated web UI. This is what made GitHub a big deal: not that it was a public repo, nor that it had a web UI, but that made collaboration easy by building infrastructure around PRs.
## What PRs Are
GitHub PRs are fundamentally a pointer to a branch on some repository, bundled with discussions about the PR, a web UI that allows diffs to be displayed, and “Merge” and “Close” buttons the owner can push.
Remember: Git itself doesn’t know anything about PRs—it doesn’t know they exist, who created them, when they’re closed, and certainly not anything about the discussion around them… these are all GitHub features, not Git features. Git itself only gets involved when the owner merges the branch the PR points to.
(Worth also remembering: a git branch is just a pointer to a commit, so a PR is really just a pointer to a commit in some repo, plus all the bundled metadata GitHub adds).
## Rebasing
(This can apply to any branch you're merging into; I’m assuming `main` here for clarity).
A typical workflow is that you create a branch off `main`, do a bunch of working making commits to your branch, then you want to merge your branch back into `main`. But what if other people have been making changes—merges of their own—into `main` while you've been working on your branch? Your branch can't be merged now, because `main` has moved on from when you created your branch. You need to get your branch up-to-date with all the changes made to `main`, first—that's what rebasing does: you rebase your branch first to pick up all the recent changes, then you can merge.
Of course, some of those changes you pick up might break things in your branch—yup: you have to fix those breaks, first... and if you take too long to do so, someone might move `main` while you're working, and then then you'll have to rebase *again*.
*Rebasing replays the change in `main` into your branch*, making it as though your branch started from the *current* state of `main`, after all the subsequent changes, instead of before the changes—it literally rewrites the history of your branch. You can think of it as merging `main` into your branch, instead of your branch into `main` (though that’s not what it’s really doing).
Why do this? To *limit the blast area of potential damage*, and *ensure a specific person has responsibility for fixing it*. When work is being done on code in independent branches, there’s a potential for conflict when those streams are merged—maybe branch A deleted code branch B needs; maybe branch B changed the name of a variable in a way branch A wasn’t aware of. **This is reality: merges with independent branches will break things. The question is: what gets broken, and who fixes it?**
- If you just merged into `main`, **you’d break the whole build**; CI/CD would fail, and every merge afterwards would make the mess worse. Because everyone’s code would be involved, responsibility for the repair is diffused.
- If a repo requires rebasing before merge, **only your rebased branch would break**; everyone else’s code would be safe and CI/CD would continue pushing to production normally. The only person inconvenienced is you, and responsibility for fixing the problem is clearly yours.
## Branch Protection
“Branch Protection” is a GitHub feature that makes it much harder to screw up the branch it’s applied to—usually your `main` branch. GitHub offers a bunch of different types of protection you can choose from; they mix-and-match, you can get any combination you want. The options include
- Only allow PRs to be merged (no direct merges of branches—PRs bundle them up nicely so it's easier to keep tracking of what changed, when)
- Require branches to be rebased before merging (see [[#Rebasing]], above)
- Only allow certain users to merge (e.g. the owner of the repo)
- Require review by certain users before merging
## Git Workflow
The usual workflow with PRs is:
1. Contributor fetches or clones a repository
2. They create a new branch (sometimes called a "feature branch") and work on their contributions locally in that new branch
3. They create a PR requesting to merge their feature branch with the original
4. If the PR is accepted, it’s squash-merged into the original
# Manipulating the Local Repo and Working Directory
## Git Clone
`git clone` is how you create a local repo on your machine from an existing remote repo. It's a full clone of a remote repository. It's not just a copy of the files—*it's the whole repository* with all history, etc. The working directory contains all the files in the HEAD node of the master branch. This also sets up your local repository to “track” the remote repository, so anytime you `get pull`, the latest updates in the original repository get pulled into your local copy.
```bash
git clone <remote repository>
git clone https://
[email protected]/fthiess/env-probe
```
## Get Fetch
`get fetch` downloads only metadata from the remote repo into your local repo—files, commits, branches, etc. It doesn't touch your working directory. This is used to update your local repo's knowledge of the state of the remote repo so you can see what other people have been doing in the remote.
## Git Pull
`get pull` fetches the repository *and* merges it with your local code. You frequently use `get pull` to update your local repo with any changes that have been made to the remote repository you originally cloned it from. `git pull` is equivalent to running `git fetch` followed by `git merge`.
Sync local copy to include changes in master (does a combination fetch and merge)
```bash
git pull
```
## Git Push
`git push` pushes the committed changes of the branch you're currently working on in your local repository into a remote repository. This works, but if you're working in any kind of team environment (or with an AI), people usually use create PRs instead of `git push` (indeed, if GitHub Branch Protection is turned on, `git push` might be completely blocked).
```bash
git push # pushes to default (usually upstream branch, if has same name)
```
If you use tags to name branches, be aware that *tags are not automatically pushed*—you have to push them explicitly:
```bash
git push origin <tagname>
```
## Git Switch
Sometimes you want to change the branch you're current working on—you want to change the state of the files in your working directory to match their state in another branch. That's what `git switch` does:
```bash
git switch <branch> # Replace your working dir with files from different branch
```
Note that there's a legacy command, `git checkout` that does the same thing as `git switch`... plus some other unrelated things. The features of the old `git checkout` command were split into several new commands (one of which is `git switch`) to reduce confusion. `git checkout` still works, but isn't recommended.
## Upstream
Setting an upstream sets the default branch that push/pull/fetch/merge/rebase/status will work with:
* If upstream is a remote tracking branch, git remote commands (fetch, clone, push, pull) will use it.
* If there’s no upstream, or if upstream is local, git remote commands will use `origin`.
*In most cases, git will set upstream automatically*, but there are some cases where it won’t—so you can use the `--set-upstream-to` option to set it manually.
## Worktrees
Sometimes, though, you actually want to have multiple working directories under the same local repo. That's where worktrees come in: each worktree is a separate working directory on your machine, all of which share a single local repo database.
The main reason to use a worktree is if you have *multiple users, and/or multiple AIs*, working on a code base at the same time. Each user could create a separate clone of the remote repo and then their changes wouldn’t conflict, but they’d have no visibility into changes committed by others. If they have their own worktrees they can work independently in their own working directories, but maintain visibility of commits others are making.
This ability makes worktrees the ideal mechanism for allowing multiple coding agents to work in the same code base at the same time.
```bash
git worktree add <path> <branch> # Create new dir and checkout branch inside it
git worktree list # List all linked worktrees and their branches
git worktree remove <path> # Deletes a worktree
```
## Rolling Back a Mistake
If you haven't merged the mistake yet—your changes are only in your working directory and you want to undo them, you can use `git restore`:
```bash
git restore <file> # Undo changes to one file
git restore . # Undo changes in all files
```
Rollback a merge that's only on your local machine:
```bash
git reset --hard HEAD # Rollback any local commits
git reset --hard HEAD-1 # Rollback local to state before last merge
```
Rollback a remote branch (e.g. `main`):
```bash
git log --oneline # Find merge commit hash you want to rollback to
git revert -m 1 <merge commit hash>
git push origin <branch name>
```
# Remote Repositories
`fetch`, `clone`, `pull`, and `push` are commands that tell git to cause some interaction between your local repository and a remote repositiory.
`origin` is a local alias for the URL of the remote repository the local one was cloned from.
To see all the remote repositories you’ve “connected” (origin plus collaborators you’re working with):
```basj
git remote -v
```
To add additional repositories:
```bash
git remote add <shortname> <url>
```
To see information about a remote repository and your working directory’s relationship to it (including which repository git push and git pull are configured to use):
```bash
git remote show <remote>
```
# **Automation**
`.git/hooks` can be used to implement things like pre-submit checks. That directory contains scripts that git will execute when it carries out certain actions—for example, running a pre-commit script.
# **Common Usage Patterns**
Most common for what I do (single user repository)...
Typical workflow (doesn't use PRs):
1. `git clone <Github repository URL>`
2. `<make some changes>`
3. `git commit -a -m “<commit message>”`
4. `git push'
5. {go back to step 2}
This is by far my most common situation: just editing and committing/pushing as I finish things.
A better practice is to make your changes in a branch instead of in main:
```bash
# 1. Start on main and make sure you have the latest updates
git checkout main
git pull
# 2. Create and switch to a new feature branch
git checkout -b feature/add-login-button
# 3. Do your work, commit it, and push this specific branch to GitHub
git add .
git commit -m "Add login button UI"
git push origin feature/add-login-button
```
Sync local copy to include changes in master (does a combination fetch and merge)
```bash
git pull
```
Add a file/directory to the repository
```bash
git add <name>
```
Remove a file/directory from the repository
```bash
git rm <name>
```
Move or rename a file/directory in the repository
```bash
git mv <from> <to>
```
## Git Workflows
TK: Add something here about Gitflow and GitHub Flow
# Useful Sources of Information
The official git book: [https://git-scm.com/doc](https://git-scm.com/doc)
[http://rogerdudler.github.io/git-guide/](http://rogerdudler.github.io/git-guide/)
[https://mukul-rathi.github.io/git-beginner-cheatsheet/](https://mukul-rathi.github.io/git-beginner-cheatsheet/)
[http://gitimmersion.com](http://gitimmersion.com)
Looks interesting, not read yet: [https://eagain.net/articles/git-for-computer-scientists/](https://eagain.net/articles/git-for-computer-scientists/)
[https://ohshitgit.com/](https://ohshitgit.com/)
[https://jwiegley.github.io/git-from-the-bottom-up/](https://jwiegley.github.io/git-from-the-bottom-up/)
[https://henrikwarne.com/2018/06/25/6-git-aha-moments/](https://henrikwarne.com/2018/06/25/6-git-aha-moments/)