Introduction to Using git in Terminal

I use git via IDEs (i.e. applications with pretty user interfaces). Right now it’s GitHub Desktop. In the past I used Sourcetree. However I read that git in the terminal is more powerful and worth learning. So I’ve given it a shot whilst I’m learning Go.
Below are some of the standard workflows and terminal commands I find myself using over and over:
- Choose which parts to stage
- Enter a description with your commit message
- Push your local commits
- Save and retrieve a WIP commit
- Squash commits for a pull request
- Merge commits: adjacent and non-adjacent
Anything written <like this>
means that you should replace everything, including the <>
. Also a quick keyboard shortcut I use a lot: command
+ k
clears the screen.
Choose which parts to stage
When I code, I often play around with the code for quite a while before making a commit. This is because I’m not sure if what I’m doing will work, so I don’t want to commit code that adds no value.
This means when it comes time to make a commit, I want to commit my changes to a particular file in small parts, not all in one big block. These small parts are called “hunks”.
Choosing which parts to commit in a file is really easy in an IDE like GitHub Desktop or Sourcetree. It’s visual and you can highlight the lines really easily. However in Terminal, it’s a much more manual process. In the long term, I think I’ll probably continue using an IDE for staging the changes, but here’s how you do it in Terminal. Going through the process has given me the side bonus of learning how to use VIM.
Below are the steps to break down your commit into smaller hunks. Note to follow these steps, the file must already be committed, i.e. it is not a new file. If it is a new file, it’s a bit more tricky and I’m not going to go through it here as I found it was too much effort.
// Check what you have in your working directory
git status// Start the process to break down changes in a file
git add --patch <file name, e.g. fib.go>
This gives you some options in the Terminal. This article was very helpful to understand those options. You’ll want to use either s
(get git to split your code into smaller hunks for you) or e
(edit your code yourself). s
is not always an option as git needs old code in between new lines of code to split it out for you. e
will always be an option.
If you choose to edit it yourself, then you’ll be editing in vim (unless you have something else set up as your default editor). Here are the steps I take:
- Once you press
e
, you will enter vim in edit mode. Read the text at the bottom of the screen and make your changes (e.g. delete lines, change symbols, etc.) If you need to delete lines of code, the vim command isdd
. If you need to edit the text, you have to be in INSERT mode in vim. To enter INSERT mod, you pressesc
plusI
. - When you have finished your edits, type
esc
then:wq
. Press enter. This saves and exits vim. - Now git has staged that hunk for you. You can type
git status
and you’ll see that your file appears in two locations. One location is staged, and the other is unstaged. You can’t see what you staged, so before you forget, it’s best to commit it withgit commit -m "<enter your commit message here>"
. See the next section if you aren’t familiar with how to do this. - Once you have committed that hunk, you can check what you staged and committed with
git show
. - If for some reason, you made an error and want to undo that commit, use
git reset HEAD~
. Only use this command if you haven’t pushed your commit. Once you push your commit, this command will mess up the history for others. There are other ways to undo commits (in fact, I’ve even written about one of those ways here), but I won’t go into it here.
Done! Now it’s time to repeat the steps until your working directory is clean (i.e. there is nothing more to commit).
Enter a description with your commit message
Some commits are really straight forward and you don’t need a description. If this describes your situation, you can use git commit -m "<enter your commit message here>"
and skip this section.
However if you want to enter a description, and you want to do it using the terminal, there are multiple ways to do so. One is to set up your preferred text editor (e.g. Sublime) so that when you type git commit
it launches Sublime and you type it in there. I found this helpful YouTube clip that goes through the steps.
Another way is to stay in Terminal and use vim.
- Launch vim to type in your message and description. Do this by typing
git commit
into the command line. Nothing else. - Enter INSERT mode in vim: type
esc
theni
- Type your commit message and description. There are lots of guidelines out there on how to write a good commit message, best practices, and more. Here is one article that seems to be well referenced. One point I didn’t find in that article is formatting bullet points. The standard practice is to only indent the first line of a bullet point. (So it is unlike the bullet points you see here in Medium).
- Save your message and leave vim: type
esc
then:wq
Push your local commits
Some jargon before moving on:
- A remote repository is where you have set up your git tracking online. GitHub and Bitbucket are examples of remote repositories.
- Your local branch is the branch that exists on your physical laptop. It’s not in the cloud. If you lose your laptop, spill coffee on it, all the changes on your local branch will be lost forever. This is why it’s great practice to push your changes from your local branch to the remote repo. That way if anything happens to your laptop, you haven’t lost your work.
Realistically the most common reason to push from your local branch to your remote is because you want to work from home and you left your work laptop at work. If you forget to push the changes, you’ll need to redo everything again. Big waste of time! - An upstream branch is the branch that exists on your remote repository. For example, if you’re following gitflow, each repo will have a master branch. When you’re working as part of a team, the master branch on the remote repo will be changing all the time with your team mates’ work.
You’ll have a copy of the master branch locally, which only updates it if you tell it to. The master branch on the remote repository is called the upstream branch. They are the same branch, but the upstream one is the live version, and will be more up to date.
If this is the first time you are pushing commits on this branch to your remote repository, then you’ll need: git push -u origin HEAD
. This also tells git to track all the changes on the upstream branch. This means your local device will know if your local version of the branch is now out of date.
If the branch already exists on your remote repository, then you can type git push origin HEAD
.
Typing git push
will push all your local branches to remote. This is fine if that’s what you want to do. But often that is not the case for me, as I’m waiting for a response to a pull request on a different branch, so it’s likely that my local branch is no longer up to date with the remote.
Save and retrieve a WIP commit
Sometimes it’s the end of the day and you haven’t finished everything neatly. It’s still good practice to commit and push to the remote, otherwise you risk losing all your work if your laptop goes missing, or you need to work from home.
Then at home, you can pull down your progress and continue on your merry way. But what about that WIP commit? How to get rid of it, and still ensure lovely atomic commits that all build on each other?
I’ve researched for hours and read about lots of different options for git commit --amend
, git reset HEAD^
, git revert
, git rebase
and using a new temporary WIP branch. Finally I’ve landed on using git reset HEAD^
and then git push --force origin HEAD
. Force pushing overwrites the commit history. Really bad if this is a branch that is used by other people, as they could have already pulled your WIP commit, and now you’re changing the history. But it’s just lonely ol’ me on my branch.
Perhaps if others are hanging out on my branch with me, I’d use git revert
. But best practice with that would be to squash all my commits that say WIP before submitting a pull request. Shrug. Too complicated for a newbie like me! (Update: I learnt how to squash commits! Read on to see how.)
Thanks to this article which says “yeah, force pushing is bad but hey, if it’s just you out there, whatevs” (scroll to option 2). This gave me the confidence to go forth.
So here’s my workflow:
- Commit all the changes in one commit. Start the commit message with “WIP”, e.g.
git commit -m "WIP Fix up sorting with minimum value as last index"
. - Push to remote with
git push origin HEAD
- When I want to start again on a different computer, I use
git pull
to get my WIP commit. git reset HEAD^
. This resets my local computer to where I was just before step 1. All the changes are in my working directory, and it’s like the WIP commit never existed.- Do all my changes, commit like I want to, everything is fine. When I’m ready to push my changes to the remote, it’s going to be confused, because I’ve undone one of the commits. What I want to do is force the remote branch to look like my local branch. Do this with
git push --force origin HEAD
If you’re reading this and have a different method, please share! And please also share how you push new commits back onto the remote branch. I found this part to be missing from most of my research.
Squash commits for a pull request
After going to the effort of making all your atomic commits, most places ask you to squash them all into one or two commits before merging into master. Below is how I do it. You can also do it with git rebase -i <SHA of the previous commit>
. But I found it hard to find the correct SHA, because I had updated my feature branch with master so many times.
- Update the remote branch on GitHub with changes from master. You can do this on Github by pressing a button at the bottom of your pull request.
- Update your local branch. Use
git pull
. - Reset your local branch to the master branch by using
git reset --soft master
. All the changes will now be in your working directory, thanks to the--soft
. git status
to check that everything is as expected. All the changes will already be staged. Commit what you’d like. I separate out my tests into a different commit.- Once everything is committed, you’ll need to force push. You’ll be overwriting history here, but it’s your own local branch that no one else has pulled, so you’ll be fine.
git push origin <your branch name> --force
.
Done! Your pull request is updated.
Merge commits: adjacent and non-adjacent
So you’ve done your atomic commits, squashed them all into two nice commits (one for the tests, one for the functionality) and submitted a pull request. When you type in git log -n 3 --oneline
, your git commit history looks something like this, with the most recent commit at the top:
25155a4 (HEAD -> lab1, origin/lab1) Implement Fibonacci functione318138 Implement tests for Fib sequence40114a7 (upstream/master, master) Merge pull request #308 from me/lab2
A reviewer now asks you to make changes, to both your function and your tests. Ideally, you’d like to merge those changes straight into the existing commits, as they are so minor.
Merge adjacent commits
Let’s start with changing the most recent commit (SHA 25155a4
):
- Make the changes that you’d like to the file or files.
- Stage them with
git add .
- Now type in
git commit --amend --no-edit
. This will add your staged changes to the ones already inside the commit25155a4
. The--no-edit
flag means that the commit message will remain the same. If you want to change the commit message, drop that flag and usegit commit --amend
. - Now force push with
git push origin <branch> --force
. You have rewritten history. Note the new merged commit has a different SHA as well. As always with force push, note this is dangerous if anyone else is working with your branch and they have already downloaded the old history. But this seems really unlikely in an PR situation.
Merge non-adjacent commits
Now let’s say you want to make changes to a commit which is not the most recent. Perhaps you’ve updated the tests and want to change the commit e318138
. So save you scrolling around, here’s your commit history again:
25155a4 (HEAD -> lab1, origin/lab1) Implement Fibonacci functione318138 Implement tests for Fib sequence40114a7 (upstream/master, master) Merge pull request #308 from me/lab2
- Make a new commit with the new changes. Use the normal flow of
git add .
andgit commit -m "Cool commit message for tests"
. - Find the commit that happened before the one you want to change. We want to change
e318138
, so the commit that is before that is40114a7
. - Rebase: type in
git rebase -i 40114a7
. This will open up vim. Note vim displays the commits with the oldest commit at the top, and the newest at the bottom. So the commit you just made with “Cool commit message for tests” is at the bottom. It will look something like:
pick e318138 Implement tests for Fib sequence
pick 25155a4 Implement Fibonacci function
pick 1234567 Cool commit message for tests# Rebase 40114a7..25155a4 onto 40114a7 (2 commands)# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
- You want to merge your cool commit with
e318138
. To do this, you need to reorder the commits. You can cut a line withdd
and then paste it withp
(this pastes it one the line below the cursor). Rearrange your file so it looks like this:
pick e318138 Implement tests for Fib sequence
pick 1234567 Cool commit message for tests
pick 25155a4 Implement Fibonacci function
- Update the word
pick
tosquash
orfixup
. I usually want to keep the old commit message, so I usefixup
. To edit the text, typei
to enter insert mode. Your file now looks likes this:
pick e318138 Implement tests for Fib sequence
fixup 1234567 Cool commit message for tests
pick 25155a4 Implement Fibonacci function
- Save your changes and exit vim by typing
esc
then:wq
. Remember this will change the SHA and it rewrites your local history. - Force push your changes with
git push origin <branch> --force
. This forces the change into the remote branch.
Note that you can squash multiple commits into one. We have only squashed one more commit into e318138
, but you could squash 10 commits into it. You would need to reorder them all to be underneath e318138
.
All done! You can now merge commits all over the place.
Want more git related stuff? Here are some other articles I’ve written: