Hello friends!
This time we'll be talking about some various ways that we can use to do common tasks with Git on the terminal.
We'll understand more about Git functionality and also add some new tricks to our toolbox when using it to manage changes in our local workspace and in GitHub (or GitLab, Bitbucket, etc.)
So if you are ready to get started, let's dive right into it!
The basics
For most of us, we are already familiar with the common workflow of git which is something like...
Pull latest changes from remote repo, work on the code, add our changes, commit them and push them to the remote repo
Since normally the bulk of the work doesn't happen in the master branch, we use feature branches which can be created with the command
git branch {branchName}
and then we can move to the recently created branch, or we could use everyone's favorite shortcut
git checkout -b {branchName}
Using branches it's when Git becomes more interesting. The branches can exist both in the remote repo and also in our local copy of it
Normally, we create the branches locally and then push them to the remote repo although we can also create them the other way (i.e. from the remote and them pull them locally.)
Now, how do we know what branches exist and whether or not they have a matching remote branch?
First let's make sure that we actually have a remote repo linked to our local one. We can check that with the git remote
command.
Although it's more useful when we use the --verbose
flag or -v
for short like so
git remote -v
Once we know we have a remote repo, we can see the existing branches on our local with git branch
which will list all branches created.
We can get more info about them with the -v
flag that will show us the id and name of the last commit made on that branch.
And we can get even more info on the branches with the -vv
flag that will show us not only the info of the last commit but also the name of the remote branch the local one is pointing to (if it has one.)
Taking things up a notch
Now, we know how to create branches and move to them once created.
In order to navigate between different branches we would have to use the git checkout
command and the name of the branch.
Though there's a nifty little trick to navigate back and forth between two branches without having to write their names every time. To do so, use the command
git checkout -
Once we make changes to a file, we can save it to the working directory, add it to the staging area and then make a commit before pushing it up to the remote.
There are several things we can do once we have changes committed to the Git history (like being able to recover changes if something unwanted happens for instance)
The standard command for that is git commit
which if used alone will open the Git default editor so that we can introduce a commit message.
You can also tell Git which editor you want it to use as a default by running the command
git config --global core.editor "your_editor_of_choice"
In my case I use Vim (the grandfather of all text editors) but you can use nano, emacs and even VSCode.
Now, let's assume we created a commit with a generic message (because we were trying out the editor config) and now we want to change that message for something more descriptive. To do that, run the command
git commit --amend -m "More descriptive commit message"
And now a new commit will be created with the message we provided, we could now be ready to push our changes to the remote repo...
But... did you know that the amend flag also works to add additional changes that we may have forgotten and should've been part of the previous commit?
In that case we have to add the changes before amending the commit with git add .
or git add -A
which is the shorthand for --all
.
Difference between git add .
& git add -A
The difference relies in that git add .
only adds files in the current directory where you're located whereas git add -A
add the files in the current and in any subdirectories as well, hence the "all".
Ok, we added the necessary files, commited them and now we're ready to push them to the remote. We do so with the command
git push origin {branchName}
If it's the first time we're pushing this branch to the remote, we can pass in the --set-upstream
flag or -u
for short like so
git push -u origin {branchName}
And now, every time we want to push new changes from that same branch we can just run git push
and Git will know what remote we're talking about.
We can now see the commit history in the remote repo navigating to the URL we see when running the git remote -v
command.
Undoing changes pushed to a remote
Now let's assume that we pushed the commit to the wrong branch (maybe because we were so busy paying attention to the difference between the flags to pass to git add...)
Don't worry we can fix that issue with the git revert
command.
Suppose we have the following history in our git log
And we want to remove that last commit from the branch that is on and maybe move it to a different one.
In that case we would run the revert command like this.
git revert 0a995a8
And now Git will create a revert commit with the changes of the previous commit undone.
(You could also have used 'HEAD' instead of the id of the commit with the same result, but I think it's easier to understand what Git is doing using ids instead of pointers to commits.)
What's left to do now is to push this new revert commit to the remote (and if you work with others, tell them to do a pull so they can get the fix.)
That wasn't too bad was it? although if we want to be more cautious for next time, we can avoid doing a revert commit and handle the commits locally.
Moving a commit from one branch to another
Let's see how we can move a commit. There are a couple of ways of doing it, like most things in Git.
Let's imagine that we are in the 'feature/about' branch and we found something to fix in the contact section and committed it there.
We'll now want to move that commit to a different branch like 'feature/contact'. For that let's run the command
git checkout feature/contact
Now we're in the contact branch and if we inspect the log, we'll see that the commit is now in both branches and what's left is to remove it from the first branch.
So now let's go back to the previous branch (with git checkout -
remember?) and we'll take the id of the commit that's before the one we want to remove and run the command
git reset --hard {commitId}
We'll get a message from Git saying that 'HEAD is now at {previousCommitId} {previousCommitName}'.
So essentially the HEAD pointer moved back one commit and it's like that latest commit was never created in the first place, but it still exists in the branch that we want it to be.
Another option to do it is to use the git cherry-pick
command.
For instance, if we're in the 'feature/about' branch, make note of the commit id (the one we want to copy) and then move to the wanted branch and run the command
git cherry-pick {commitId}
Now the changes of the commit from the other branch will be applied to the branch that we're on.
Note that cherry-pick applies the changes of the selected commit by creating a new commit on the branch instead of copying the entire commit.
We'll now have to go back to the other branch and remove the commit from there like we did before.
Also note that git cherry-pick
works if we want to take one commit from a branch and copy the changes into another branch whereas with git checkout
we're taking all the recent commits and moving them to the other branch.
With that, we have moved a commit to it's corresponding branch before pushing to the remote which makes things easier to deal with locally.
Getting into more advanced stuff
We'll now be looking into how to handle commits across different branches and remotes with some of the extra firepower that Git gives us.
But you know what? I just looked through the whole article and realized that's it's getting quite long and unwieldy.
So I'll be saving the advanced stuff for a Git tricks part 2.
In that one we'll be looking at things like...
- The different options that
git reset
has. - How to recover commits that may seem lost with
git reflog
. - Doing forced pushes with caution using
--force-with-lease
instead of--force
. - And how to manage changes, commit messages and more with
git rebase
.
So, there you have it, some extra tricks that your probably didn't know when using Git for the traditional software development workflow.
I hope this has been useful, I know a couple of years back I'd be using Git without understanding most of its power and just limiting myself to the most used commands and nothing more.
And now that was it for this week's post!
T Hanks for reading so far and I hope to see you the next week for part 2!
Top comments (2)
Interesting article, thanks, though I believe this is not quite right:
I think this is true iff:
git checkout -b feature/contact
But I don't think it would be true if feature/contact is an existing branch split off earlier, is that right?
I've mostly ignored cherry picking, so I greatly appreciate your simple, clear explanation, I'll be looking forward to part 2.
Hey Jerry,
Thanks for the comment. You are right, that example would only work if the 'feature/contact' branch was created off of the one we're currently in and not if it already existed.
I think that it's not obvious reading that part again, so appreciate the clarification.
I'm making a small project to use as an example for the followup of this article so it took me longer than expected to writing it 😅