|
| 1 | +# Git |
| 2 | + |
| 3 | +This guide assumes Git version 2.28.0. |
| 4 | +You can install it from [Homebrew](https://brew.sh/) with `brew install git`. |
| 5 | +The version of Git that comes with macOS is older, so if some of these commands don't work, check `git --version`. |
| 6 | + |
| 7 | +## Core Concepts |
| 8 | + |
| 9 | +### Commits |
| 10 | + |
| 11 | +A commit is the core unit of change in Git. |
| 12 | +Each commit adds or removes lines in files. |
| 13 | + |
| 14 | +<img src="structure-of-a-commit.png" width="639" /> |
| 15 | + |
| 16 | +Every commit has a hexadecimal hash (`36ee8abf2`...) that uniquely identifies it among all the other commits in the repository. |
| 17 | +`git log` shows the full 40-character hash for each commit. |
| 18 | +Since 40-character hashes are cumbersome, GitHub often shows just the first 6-9 characters of the hash (`36ee8ab`), just enough to identify it uniquely, and hides the rest. |
| 19 | + |
| 20 | +Commits point to their parent commit. |
| 21 | +For example, if I make three commits in a repository, A ← B ← C, C would be the most recent commit, B its parent, and C its grandparent. |
| 22 | +`git log` shows the most recent commit and all of its parents. |
| 23 | + |
| 24 | +Git can reconstruct the repository at any point in time by applying the sequence of added and removed lines in the commit chain. |
| 25 | +When we make changes by adding commits or merging pull requests, Git looks at those commits to know which lines to add and remove. |
| 26 | + |
| 27 | +### `HEAD` |
| 28 | + |
| 29 | +In Git, `HEAD` is a special alias for the most recent commit. |
| 30 | +It's whichever commit `git log` would show first. |
| 31 | +When we create a new commit, the previous `HEAD` commit is the new commit's parent, and then `HEAD` is updated to point to the new commit. |
| 32 | + |
| 33 | +### Branches |
| 34 | + |
| 35 | +Branches are special labels that point to commits. |
| 36 | +`master` is a regular branch whose label is "master". |
| 37 | +Sometimes repositories choose to call it `main` instead. |
| 38 | +Because we start work by branching from `master`, our branches look like a tree with `master` as the trunk. |
| 39 | +We can follow commits' parents in a chain and eventually end up back in the `master` trunk. |
| 40 | + |
| 41 | +<img src="branches.png" width="620" /> |
| 42 | + |
| 43 | +We can create our own branches with `git branch new-branch-name`. |
| 44 | +This will create a new label pointing to the most recent commit. |
| 45 | + |
| 46 | +We can switch between branches with `git switch other-branch`. |
| 47 | + |
| 48 | +When we create a new commit, the current branch's label automatically updates from the previous commit to the new commit. |
| 49 | + |
| 50 | +### Pushing and pulling |
| 51 | + |
| 52 | +Our laptops, GitHub, and each Heroku environment all have their own copy of our repository. |
| 53 | +These repositories keep track of branches and commits separately. |
| 54 | +We use `git push` and `git pull` to copy commits and branch labels between repositories. |
| 55 | + |
| 56 | +`git remote --verbose` lists all of the other repositories that Git knows about. |
| 57 | +GitHub is typically called `origin`, and `production` and `staging` might be names for Heroku's repositories. |
| 58 | + |
| 59 | +When we push a new branch to GitHub, it pushes all of the commits we've made locally and then tells GitHub to create its own copy of the branch label pointing to the most recent commit. |
| 60 | + |
| 61 | +When we push new commits to an existing branch in GitHub, Git sends GitHub all the new commits that are in our laptop's branch but not yet in GitHub's branch and updates GitHub's branch label to point to the newest commit. |
| 62 | + |
| 63 | +Pulling works in reverse. |
| 64 | +When we pull a branch from GitHub, Git retrieves all the commits that are in GitHub but not yet on our laptop and updates our laptop's branch label to point to the newest commit. |
| 65 | + |
| 66 | +## Working on code |
| 67 | + |
| 68 | +The `master` branch should be the authoritative source for what exists in production. |
| 69 | +Changes should only reach `master` when the tests are passing and they're ready for production. |
| 70 | + |
| 71 | +### Starting a feature or bug fix |
| 72 | + |
| 73 | +We work on in-progress changes in "feature branches" off of `master`. |
| 74 | +We feature branch might implement a new feature, add a test, change some copy, or fix a bug. |
| 75 | + |
| 76 | +```sh |
| 77 | +# Make sure we're starting our branch off of master. |
| 78 | +$ git switch master |
| 79 | +# And make sure we have the latest changes from master on GitHub. |
| 80 | +$ git pull |
| 81 | +# Create the feature branch from master. Name it something descriptive, like |
| 82 | +# `very-cool-feature` or `fix-issue-123`. |
| 83 | +$ git branch new-branch-name |
| 84 | +# Switch to the new branch. |
| 85 | +$ git switch new-branch-name |
| 86 | +``` |
| 87 | + |
| 88 | +At this point, we're working on the `new-branch-name` branch. |
| 89 | +For now, `HEAD`, `new-branch-name`, and `master` all point to the same most recent commit. |
| 90 | +Any new commits will be added to `new-branch-name` but not `master`. |
| 91 | + |
| 92 | +### Committing changes |
| 93 | + |
| 94 | +Once we've made a change, we use `git add` to prepare the added and removed lines to be turned into a commit. |
| 95 | +When we `git add` a file, it goes into a "staging" area, which is like a waiting room for the lines that will be in a commit. |
| 96 | + |
| 97 | +```sh |
| 98 | +# Show what files have been changed since the last commit. |
| 99 | +$ git status |
| 100 | +# Add a file. |
| 101 | +$ git add hello-world.js |
| 102 | +# Show that hello-world.js has been added to the commit staging area. |
| 103 | +$ git status |
| 104 | +# Turn the staged files into a commit. |
| 105 | +$ git commit |
| 106 | +``` |
| 107 | + |
| 108 | +The `git commit` command will open a text editor where you can write a title and message for your commit. |
| 109 | +The title is a short description in a few words. |
| 110 | +The message contains a more detailed description of what changes we made and why. |
| 111 | +If we're adding a new feature, we describe the feature's design and how it's built. |
| 112 | +If we're fixing a bug, we describe what caused the bug and why this is the right way to fix it. |
| 113 | + |
| 114 | +### Opening a pull request |
| 115 | + |
| 116 | +Congrats! |
| 117 | +We've implemented a new feature or fixed a bug. |
| 118 | +All the steps in the change are nicely packaged as line changes in commits. |
| 119 | +It's time to open a pull request in GitHub. |
| 120 | + |
| 121 | +```sh |
| 122 | +# Tell GitHub about our new commits and branch. |
| 123 | +$ git push --set-upstream origin my-new-branch |
| 124 | +``` |
| 125 | + |
| 126 | +`git push` copies all of the new commits to GitHub and creates a copy of our branch label. |
| 127 | +`origin` is Git's name for GitHub's copy of our repository. |
| 128 | +`--set-upstream` tells Git to remember that we pushed `my-new-branch` to GitHub, so in the future, just `git push` will automatically go to GitHub. |
| 129 | +Now go to GitHub and fill out your pull request. |
| 130 | + |
| 131 | +#### Review feedback |
| 132 | + |
| 133 | +We've received review feedback on the pull request and need to make changes. |
| 134 | +Make commits just like when we were working on the change initially. |
| 135 | + |
| 136 | +```sh |
| 137 | +# Show the files we've updated in response to feedback. |
| 138 | +$ git status |
| 139 | +# Add them to the staging area for the next commit. |
| 140 | +$ git add hello-world.js |
| 141 | +# Commit our review fixes. |
| 142 | +$ git commit |
| 143 | +# Push the fixed commits to GitHub. |
| 144 | +$ git push |
| 145 | +``` |
| 146 | + |
| 147 | +Git will send our review feedback changes to GitHub, update GitHub's `my-new-branch` branch label to point to the new commits, and show them in the pull request. |
| 148 | +In the `push` step, since we used `--set-upstream` when we first pushed the branch to GitHub, we don't need to specify `origin my-new-branch` again because Git remembers those settings. |
| 149 | +It's the same as running `git push origin my-new-branch`, just with less typing. |
| 150 | + |
| 151 | +#### Suggested changes |
| 152 | + |
| 153 | +Sometimes reviewers leave small suggested changes in review comments. |
| 154 | +Committing these suggestions to the pull request is quick and easy without ever leaving GitHub. |
| 155 | +After you commit review suggestions from GitHub, be sure to update your local repository so it knows about the new commits. |
| 156 | + |
| 157 | +```sh |
| 158 | +# Switch to the pull request's branch. |
| 159 | +$ git switch my-new-branch |
| 160 | +# Update the local branch with the suggested change commits from GitHub. |
| 161 | +$ git pull |
| 162 | +``` |
| 163 | + |
| 164 | +#### Rejected push |
| 165 | + |
| 166 | +If we forget to update our local branch when there are new commits in GitHub's copy of the branch, Git will reject our next push to the pull request. |
| 167 | +**Never** run `git push` with `--force`. |
| 168 | +That would overwrite the suggested change commits from GitHub, losing work. |
| 169 | +Git rejected the push because there are commits in GitHub's copy of `my-new-branch` that are missing from our local copy of `my-new-branch`. |
| 170 | +We want to combine the commits from all versions of the branch. |
| 171 | +Run `git pull --rebase` to take the suggested change commits from GitHub and then replay our local commits after them. |
| 172 | + |
| 173 | +<img src="rebase-suggested-change.png" width="798" /> |
| 174 | + |
| 175 | +```sh |
| 176 | +# Git rejects our push because our local branch is missing some commits. |
| 177 | +$ git push |
| 178 | +To https://github.com/drivecapital/git-workflow.git |
| 179 | + ! [rejected] my-new-branch -> my-new-branch (non-fast-forward) |
| 180 | +error: failed to push some refs to 'https://github.com/drivecapital/git-workflow.git' |
| 181 | +hint: Updates were rejected because the tip of your current branch is behind |
| 182 | +hint: its remote counterpart. Integrate the remote changes (e.g. |
| 183 | +hint: 'git pull ...') before pushing again. |
| 184 | +hint: See the 'Note about fast-forwards' in 'git push --help' for details. |
| 185 | +# Pull the newer commits from GitHub and replay our local commits after them. |
| 186 | +$ git pull --rebase |
| 187 | +# Now we can push the combined commits to GitHub. |
| 188 | +$ git push |
| 189 | +``` |
| 190 | + |
| 191 | +### Merge conflicts |
| 192 | + |
| 193 | +Sometimes GitHub says we can't merge a pull request because we have merge conflicts. |
| 194 | +This happens when a teammate worked on the same lines in a different branch. |
| 195 | +Git doesn't know which version of the new lines to take - ours or our teammate's, so it asks us to decide. |
| 196 | + |
| 197 | +First, we need to pull the latest changes from GitHub's `master` branch label to our laptop. |
| 198 | + |
| 199 | +```sh |
| 200 | +# Switch to our local master branch. |
| 201 | +$ git switch master |
| 202 | +# Retrieve the new commits from GitHub. |
| 203 | +$ git pull |
| 204 | +``` |
| 205 | + |
| 206 | +_TODO: Finish this section._ |
| 207 | + |
| 208 | +## Git etiquette |
| 209 | + |
| 210 | +_TODO: A commit should change one thing, and pull requests should only include related commits. How to make things easier for reviewers._ |
| 211 | + |
| 212 | +## Advanced usage |
| 213 | + |
| 214 | +### Reverting a pull request |
| 215 | + |
| 216 | +If we just merged and deployed a pull request that broke everything and we can't fix it in a hurry, the revert button is the nuclear option. |
| 217 | +Clicking the "revert" button on the offending pull request will reverse all of its changes, removing the lines it added and adding back the lines it removed. |
| 218 | +If we do a good job submitting pull requests that make logically grouped changes and splitting unrelated changes into their own pull requests, we should be able to revert just the pull request that caused the breakage. |
| 219 | +We shouldn't need to use the `git revert` command locally. |
| 220 | + |
| 221 | +### Committing only part of a file |
| 222 | + |
| 223 | +Each commit should do one thing. |
| 224 | +If we've fixed two unrelated bugs, we'd like to create two commits, one to fix each bug. |
| 225 | +We don't have to `git add` an entire file at once. Instead, we can add only part of a file. |
| 226 | + |
| 227 | +```sh |
| 228 | +# Start an interactive session to stage lines for the commit a chunk at a time. |
| 229 | +$ git add --patch |
| 230 | +``` |
| 231 | + |
| 232 | +`git add --patch` will go through the changes one chunk of lines at a time. |
| 233 | +Typing `?` at the prompt will show help instructions. |
| 234 | +`y` will stage this chunk of lines for inclusion in the next commit. |
| 235 | +`n` will leave this chunk of lines unstaged for a later commit. |
| 236 | +If the chunk of lines is too big, split it into even smaller chunks with `s`, then stage or skip the sub-chunks with `y` or `n`. |
| 237 | + |
| 238 | +### Un-staging changes |
| 239 | + |
| 240 | +If you've added a file to the commit staging area and don't want it to be part of the commit, you can remove it from the staging area before committing. |
| 241 | + |
| 242 | +```sh |
| 243 | +# Show the files in the staging area for the next commit. Whoops, hello-world.js |
| 244 | +# isn't supposed to be part of this commit! Let's leave it for a later commit. |
| 245 | +$ git status |
| 246 | +# Remove hello-world.js from the staging area. |
| 247 | +$ git unstage |
| 248 | +# Show that hello-world.js has been removed from the commit staging area. A new |
| 249 | +# commit now would not include the added and removed lines in hello-world.js. |
| 250 | +$ git status |
| 251 | +``` |
| 252 | + |
| 253 | +### Interactive rebase |
| 254 | + |
| 255 | +_TODO_ |
| 256 | + |
| 257 | +## Configuration |
| 258 | + |
| 259 | +When pulling from GitHub, only allow "fast forwards". |
| 260 | +When "fast forwarding", if your local branch contains commits A, B, C, and GitHub has A, B, C, D, E, pulling will add D and E to your local branch. |
| 261 | + |
| 262 | +```sh |
| 263 | +$ git config --global pull.ff only |
| 264 | +``` |
| 265 | + |
| 266 | +Enable branch protection in your GitHub repository's settings: |
| 267 | + |
| 268 | +- `master` |
| 269 | + - Don't allow force pushes |
| 270 | + - This only applies to `master`. |
| 271 | + - We can still `--force-with-lease` to feature branches in advanced uses. |
| 272 | + - Don't allow deletions |
| 273 | + - Include administrators |
| 274 | + - Prevents even organization owners from accidentally force pushing to `master`. |
| 275 | + - If 💩 hits the fan and you really need to, you can temporarily un-check this. |
| 276 | + - Optionally, require pull request reviews before merging |
| 277 | + - Changes must go through code review. |
| 278 | + - Prevents accidentally pushing directly to `master`. |
| 279 | + - Optionally, require status checks to pass before merging |
| 280 | + - If your automated tests aren't flaky. |
| 281 | + - Optionally, require linear history |
| 282 | + - Forces people to resolve conflicts via rebasing their feature branch on `master` instead of merge commits. |
0 commit comments