I’m teaching a Startup Systems Design and Engineering class at Cornell Tech this year. As a first homework assignment we’ve asked students to fork our github repository and add a page with their own info and maybe a photo, linked from the README. A good pull request is done off a feature branch and includes 3 files: the README change, a personal page and a picture named the same way as the personal page file. Here’s a good one from one of our TAs, minus the feature branch.
Since most students use git for the very first time they often make some kind of mess. Everyone successfully forks the repository, clones it locally, makes changes (usually on master), commits and pushes, but that’s where the issues begin. They try to update from the upstream repository, squash commits, rebase things and make a huge mess. This is normal, git is very powerful and can be very confusing. Eventually it becomes muscle memory and you can only really get there by doing it yourself.
After helping a handful of students I decided to write this up. Here’s a quick tutorial on getting out of a git mess.
Make a Clean-ish Slate
The first goal is to make both the local computer’s and our Github fork’s master branches look like the upstream, the repository that was forked, master branch, without losing changes already made.
Checkout the master branch.
(big-mess)$ git checkout master Switched to branch 'master' Your branch is up-to-date with 'origin/master'.
Make sure there’re no changes currently on master.
(master)$ git status On branch master Your branch is up-to-date with 'origin/master'. nothing to commit, working directory clean
If there’re any changes here,
git stash them away. If you were in the middle of a merge, run
git merge --abort. If you were in the middle of a rebase, run
git rebase --abort. Run
git status again and make sure everything is clean.
Save Your Work
Create a temporary branch with all your changes.
(master)$ git checkout -b tmp Switched to a new branch 'tmp' (tmp)$
And go back to the master branch.
(tmp)$ git checkout master Switched to branch 'master' Your branch is up-to-date with 'origin/master'. (master)$
Configure the Upstream Remote
This is our link to the parent or forked repository called the upstream.
(master)$ git remote -v origin firstname.lastname@example.org:dblock/cs5356.git (fetch) origin email@example.com:dblock/cs5356.git (push)
Add the upstream remote.
(master)$ git remote add upstream firstname.lastname@example.org:Cornell-CS5356-Fall2015/cs5356.git
git remote -v command would show the upstream remote now.
(master)$ git remote -v origin email@example.com:dblock/cs5356.git (fetch) origin firstname.lastname@example.org:dblock/cs5356.git (push) upstream email@example.com:Cornell-CS5356-Fall2015/cs5356.git (fetch) upstream firstname.lastname@example.org:Cornell-CS5356-Fall2015/cs5356.git (push)
Rewind Master a Dozen Commits
Go back in time, losing any changes with
git reset --hard.
(master)$ git reset HEAD~12 --hard HEAD is now at ee933e2 Smaller logo.
This just undid a dozen changes, including ours, on master. Fortunately we saved everything in that tmp branch.
Pull from upstream with
git pull upstream master.
(master)$ git pull upstream master remote: Counting objects: 215, done. remote: Compressing objects: 100% (39/39), done. remote: Total 215 (delta 92), reused 74 (delta 74), pack-reused 102 Receiving objects: 100% (215/215), 6.25 MiB | 810.00 KiB/s, done. Resolving deltas: 100% (113/113), completed with 12 local objects. From github.com:Cornell-CS5356-Fall2015/cs5356 * branch master -> FETCH_HEAD * [new branch] master -> upstream/master Updating ee933e2..abaecc6 Fast-forward ...
You may get a “Your branch and ‘origin/master’ have diverged, and have 2 and 8 different commits each, respectively.” error message, which is normal, because your cloned local copy is not the same as your Github fork, they have diverged.
Push the master branch to Github, overwriting all changes with
git push origin master -f. The force flag,
-f is required because we’re overwriting changes on Github.
(master)$ git push origin master -f Counting objects: 289, done. Delta compression using up to 4 threads. Compressing objects: 100% (152/152), done. Writing objects: 100% (289/289), 12.90 MiB | 2.32 MiB/s, done. Total 289 (delta 149), reused 272 (delta 136) To email@example.com:dblock/cs5356.git dd3a085..abaecc6 master -> master
Now master is in sync between upstream’s master, your fork’s master on Github and your computer’s master, we’re back to a state after we’ve forked the repository, and our changes are on that tmp branch.
Apply Changes to a New Branch
We saved our work to tmp. Create a new branch called students-list and merge all changes from tmp to it, minus the history with
git merge --squash. That flag applies the changes but doesn’t replay the commits.
(master)$ git checkout -b students-list Switched to a new branch 'students-list' (students-list)$ git merge tmp --squash Updating dd3a085..abaecc6 Fast-forward Squash commit -- not updating HEAD ...
git status to see changes. They aren’t committed, it’s as if you just made them with a text editor.
(students-list)$ git status On branch students-list Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: ...
Examine Changes, Commit and Push
Add any changed files with
git add, commit changes with
git commit and push the branch to Github.
(students-list)$ git push origin student-list
Make a pull request from the student-list branch to the upstream’s master branch on Github. You’re all set.
Need More Changes?
If you need to make more small changes to the student-list branch, edit the files,
git add them and commit using
git commit --amend, which updates the last commit instead of adding a new one, then
git push origin student-list -f to push the changes to Github. This way there’s only one commit in the pull request, no need to squash.
There’s going to be other changes on the upstream repository by other students. Update your fork.
(tmp)$ git checkout master Switched to branch 'master' Your branch is up-to-date with 'origin/master'. (master)$ git pull upstream master ... Fast-forward ...
Rebase the student-list branch.
(tmp)$ git checkout student-list Switched to branch 'student-list' (master)$ git rebase master ...
When a conflict is shown during the rebase, fix it by editing the file in conflict with a text editor, then continue the rebase with
git rebase --continue.
Push the branch to Github.
git push origin student-list -f
No need to update an existing pull request from the student-list branch, it will be automatically updated with the new changes!
After the pull request has been accepted upstream (ie. merged), checkout the master branch,
git pull upstream master, then cleanup local branches.
(master)$ git branch -D tmp (master)$ git branch -d student-list
-D force-deletes unmerged branches with changes and
-d deletes all merged branches.
You may get a “error: unable to delete ‘branch-name’: remote ref does not exist” when trying to delete a branch. Prune any branches that were deleted remotely.
(master)$ git remote prune origin
You can see the remote branch list with
git branch -r.
(master)$ git branch -r origin/student-list
Delete the merged remote branches.
(master)$ git push origin :student-list
The biggest issue was that you started without a topic or feature branch, student-list in our example. That makes it hard to have a local baseline (master) to rebase or update against. Always keep all the master branches in sync, and use feature branches for development.