9 Version Control - Notes
9 Version Control - Notes
Version Control
Winter 2023
Outline
Contents
1 Review 1
2 Merge Conflicts 2
3 Commit Etiquette 5
4 GitHub 8
1 Review
Overview
Git is Confusing
• Git is confusing!
• Ask as many questions as you need, and don’t let me move on if you don’t yet understand something.
1
Terminology
Rebasing
2 Merge Conflicts
Merge Conflicts
Definition 2.1 (merge conflict). A merge conflict is what happens when you try to combine two contradic-
tory branches. Git can’t always figure out how to resolve the contradiction, so it’ll ask the user (you).
• Git normally resolves merge conflicts automatically.
• Some conflicts have multiple valid resolutions (e.g., what if one person edited a file that another person
deleted?).
• If Git doesn’t know what to do, it’ll ask you to resolve the conflict.
Merge Conflicts
Git will tell you which files conflicted, and tell you to resolve the commits and commit the results:
Auto-merging hello.txt
CONFLICT (content): Merge conflict in hello.txt
Automatic merge failed; fix conflicts and then commit the result.
2
Merge Conflicts
Conflict Markers
Git will also add conflict markers to the files:
Merge Conflicts
Conflict Markers: The Base Branch
The top part (labeled HEAD ) are the changes in the base branch (the branch you’re currently on):
Merge Conflicts
Conflict Markers: The Incoming Branch
The top part (labeled with a branch name or commit message) are the changes in the incoming branch (the
one you’re merging):
When you see these conflict markers, all you have to do is make the files look the way you want them to look
at the end. In this case, I added the text “I’m doing my PhD in the Stanford CS department.” on main ,
but I added the text “I am a PhD student studying CS at Stanford.” on the branch add-major . When I
tried to merge add-major into main , Git didn’t know what to do, so it’s asking me. Now I can choose
either of the two sentences to keep, and delete the other (or I could keep both of them, if I wanted).
As an aside, you might see the name HEAD pop up in Git. This basically just means “what commit you’re
currently looking at”.
3
Merge Conflicts
Resolving a Conflict
Pick how you want to resolve the conflict (i.e., decide what the “correct” result of the merge is), and make
the file look that way!
In this case, I mixed together both versions. The “correct” answer often depends on what exactly you’re
doing, which is why Git can’t figure it out for you.
Merge Conflicts
Commiting the Merge
Resolve all the conflicts in all the files however you want, then:
1. git add your changes to track them
Save the file in your editor and close it ( :wq in Vim), and Git will save the merge commit. That’s it—the
merge conflict is gone!
That’s all you have to do—make the files look “correct”, then commit! A merge conflict really isn’t as bad
as people sometimes make it sound; all it means is that there are multiple ways to merge the two branches,
and Git wants you to pick one.
If you decide to use rebase instead, the process is pretty much the same—just run git rebase --continue
instead of git commit at the end.
Merge Conflicts
Rebase Conflicts
Resolve all the conflicts in all the files however you want, then:
1. git add your changes to tell Git you fixed them
Since rebasing doesn’t create a merge commit, you don’t run git commit ; use git rebase --continue
instead!
Remember, rebasing happens backwards; the base branch (the one onto which you’re rebasing) becomes
HEAD, and the “feature” branch becomes the incoming branch.
4
2. Look at the files in conflict (run git status to see what’s going on).
3. Fix each conflict, one-by-one.
4. When you’re done, git add all the fixed files and git commit .
Let’s practice!
Merge conflicts usually happen in shared repos, so let’s clone one of my repos onto your computer:
We’ll go into more detail about how shared repositories work in the last section of this lecture, but for now:
• You can “clone” a shared repository using git clone , which makes a local copy.
• You can “fetch” commits from the shared repository into yours using git fetch . The commits will
go into a separate branch so they don’t conflict with yours; by convention, the branch names have the
prefix “origin/” prepended to them, so main goes into origin/main .
Pulling Changes
You might have seen references to the git pull command before. This is a combination of two commands,
but the exact two depends on your Git version and configuration:
git pull --ff-only : git fetch and git merge --ff-only (Default)
git pull --no-rebase : git fetch and git merge (Old Default)
Depending on your preferences, you can configure git pull to do any of these.
I personally use git pull --rebase the most often, since I don’t like having merge commits in my repo
history.
3 Commit Etiquette
Commit Messages
Git only saves work that we’ve committed, so we want to commit as often as possible, but…
• Other people will also look at your commit history to see what you did.
• Your commit messages in the history should be short and specific, but descriptive enough that someone
new can understand what they do.
• Similarly, each of your commits should do a single thing, so a single message can describe it easily.
• Good commits are bisectable; you should be able to checkout any commit in main and get a valid
(e.g., compilable) state of your repo.
Writing good commit messages is part of being a good programmer!
These goals might seem contradictory; how do we commit as-often-as-possible, but still make sure each of
our commits are meaningful and discrete? The answer is: we don’t! While we’re developing, we commit as
often as we want. Then, when we’re ready to share our work with others, we edit our commit history to
make it look like we made nice, easy-to-understand, discrete commits.
5
Squashing Commits
We can commit often locally but still have meaningful commits in the end by squashing commits together
with interactive rebase.
Editing History
Interactive rebasing edits history! Don’t do this on a branch you share with other people (like main ). In gen-
eral, only do this on commits you have not pushed. Otherwise, you’ll have to force-push ( git push --force )
your changes, which will destroy everyone else’s changes.
You can start an interactive rebase using the command git rebase --interactive <base> ; for example,
git rebase --interactive main will let you edit every commit that’s in your branch but not in main .
Interactive Rebasing
Git will open $EDITOR with a list of actions (which you can edit!).
Each line represents one commit. The first word is a “command”; pick cherry-picks (i.e., includes) the
commit in the new history, reword lets you edit the commit message, edit lets you change the commit
contents, squash and fixup both squash the commit into the previous one, and drop removes the
commit.
You might notice that the default behavior here is to cherry-pick every commit. This is the exact same as
a normal (non-interactive) rebase! What we called “rebasing” earlier is actually a special case of editing
history, but you can go far beyond that with interactive rebasing.
Note that we could also use fixup here, the only difference is whether the original message gets saved or
thrown away. In this case, we’re using reword on the first commit anyway, so it’s a moot point.
6
Rewording Commits
When you want to reword a commit, Git will open $EDITOR and ask you for a new commit message. Enter
the message you want, save, and quit.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Fri Feb 3 22:34:02 2023 -0800
Amending Commits
If you want to edit the most recent commit you made (i.e., HEAD), you can skip the rebase and just amend
it, using git commit --amend .
This will also let you edit the commit message of the last commit.
If you want to add to an earlier commit but don’t want to do the full interactive rebase yet, you can use
git commit --fixup <hash> to mark a commit as being a fixup commit of an earlier commit.
You can then use git rebase --interactive --autosquash <base> to automatically absorb your fixup
commits into the original commits.
Again, only do this if no one else is using your branch.
These commit messages aren’t perfect, but they’re short, descriptive, and make it clear what one thing each
commit does. They generally follow the format verb-object (although the implied subject isn’t always
consistent), and they describe which part of the codebase they’re touching.
These commits are also bisectable; that means, if I notice a bug, I can binary search to figure out which
commit introduced the bug. Git actually has a tool for this built-in, called git bisect —you give it a start
and end commit (the start commit definitely doesn’t have the bug, and the end commit definitely does), and
it’ll checkout commits in between to help you figure out where the bug was introduced.
7
a527839 minor 44e7773 minor 2e67fd8 minor
adaf72e minor 571c20b minor e530f6e minor
c9c6193 minor 059cb3f minor 70387f2 minor
d64a6ef minor eaa75ae minor e3d971e minor
ff2636e minor ebbe9db minor 91b236e minor
4a988f2 minor 13570e0 minor de176a8 minor
cb901d5 minor 3e51470 minor 461e76a minor
8d4e80a minor 95a0fad minor 48cd0ff minor
53b5e84 minor 5d2c780 minor 0543316 minor
0321f79 minor d5caf55 minor 40b48f6 minor
4126899 minor c26b868 minor fb0ec84 minor
f1d7231 minor 080ddf2 minor 3a124af added basic files.
cefba82 minor f492a3f minor
These commit messages are useless; if you try to look back at your commit history, you’re going to have
no idea what’s going on. If one of these commits had a bug, it’s hopeless to try and figure out which
one introduced it. Additionally, messages like this are often a symptom of poorly-separated commits; it’s
impossible to describe what a commit does because each commit does way too many things (or, alternatively,
a single discrete change is scattered across many commits, so there’s no meaningful description of what a
single commit did).
For the record, these are both real sequences of commit messages from projects I’ve worked on. You’ll
probably run into both ends of this as you work with different groups of people, but whenever you can, try
to make your commit messages more like the first example.
4 GitHub
GitHub
• You fetch while inside a clone, which copies the remote main branch into a branch called origin/main .
• You push your new main back to the remote, which updates its main and your origin/main .
Remember, you can combine fetch and merge (or rebase ) using pull , if you want to.
You can actually have multiple remote repos for a single local repo. For example, you might have origin
as your copy of the repo on GitHub, and upstream as someone else’s copy of the same repo from which
you want to cherry-pick changes.
GitHub Demo
8
2. Click “Create repository” to continue.
3. Run git clone with the URL of your new repo.
4. Run gh auth login from inside your new clone. Tell gh that you want to use it to authenticate
with git .
5. Make some changes (add a file), and run git push to upload them!
GitHub Demo
Pull Requests
• It’s dangerous to give access to the main branch on your repo to everyone; someone might start
messing with it!
• In “Settings/Branches”, you can enable branch protection for main . Specifically, you can enable
“Require a pull request before merging”.
• A pull request2 is a way to review a change before merging it. You (the repo owner/maintainer)
can choose whether to approve or reject the request.
• To create a pull request: create a new branch, make your changes, push your new branch, then run
gh pr create .
1 Hint: you might have to use the --set-upstream flag; Git will tell you exactly what to do.
2 This is misleadingly named, it’s really a “merge request”
9
alias ga="git add"
alias gc="git commit"
alias gc!="git commit --amend"
alias gcmsg="git commit --message"
alias gca="git commit --all"
alias gcam="git commit --all --message"
alias gca!="git commit --all --amend"
alias gp="git push"
alias gf="git fetch"
alias gfm="git pull --no-rebase"
alias gfr="git pull --rebase"
alias gff="git merge --ff-only"
alias gst="git status"
alias gr="git rebase"
alias gri="git rebase --interactive"
alias glog="git log"
alias gco="git checkout"
alias gb="git branch"
[alias]
co = checkout
c = commit
st = status
b = branch
hist = log --pretty=format:"%Cred%h%x09%Cgreen%cs%x09%Creset%s%x20%Cblue[%an]%Creset"
uncommit = reset --soft HEAD^
amend = commit --amend
histedit = rebase -i origin/main
unstash = stash pop
unadd = restore --staged
skip = update-index --skip-worktree
unskip = update-index --no-skip-worktree
skipped = ! git ls-files -v | grep '^S' | cut -d' ' -f2
list = ls-files -v
ff = merge --ff-only
delete-remote-branch = push origin --delete
I wouldn’t recommend copying all of these, since some of them are particular to my use case, but they might
give you ideas of ways you can make your own Git usage more convenient. A git alias can be run as a git
subcommand, so I can run git unadd hello.txt instead of git restore --staged hello.txt .
10