A personal repository with notes about installing, configuring, and using Linux applications. This is all part of the learning and documenting process!
This guide will help you through understanding Git. I’m having difficulty wrapping my head around it, and I’m hoping this will help solidify my memory slightly. The best way to proceed is likely to cover what each command does. I will try to do so in a visual way. For a more interactive way of learning Git, I recommend checking out the following website: https://learngitbranching.js.org
First off, we should try to visualize Git as a working tree for your files and folders. It is a way to keep track of changes to your files in development.
So let’s say you want to take a “snapshot” of your files and folders in their current form. You’re going to use a command called git commit
.
git commit
takes a “snapshot” of your selected files and folders, and “saves” them in Git.
When you initialize Git, you start with a main
branch; an empty work tree. When you commit in a branch (such as main
), you create a new “snapshot” for that branch, which can then be referenced later.
C0 - git init (empty)
|
C1 - *main (after committing once)
If you type the option git commit --amend
, you can correct the most recent commit with the changes you need.
It’s generally bad practice to ALWAYS work from your main
branch. Let’s imagine for a moment that you’re working on a website or a piece of software, and main
is the “live” product. Do you want to “test” new features in the “live” version of your work? What would happen if your test breaks the “live” version? You can see how this can get messy.
For that reason, we can create new branches to our worktree. A branch creates a “copy” of our current commit in which you can play without affecting the other branches. You can achieve this by typing git branch <branch name> <location>
. If you want to create a development branch, you could create a branch called dev
with git branch dev
. You can also specify where to create the branch if you have multiple commits with the <location>
(such as git branch dev HEAD~^2~
) argument. I recommend reading chaining to learn more about navigating around the worktree.
C0
|
C1 - *main & dev
main
& dev
are now two branches, but they refer to the same commit. Next time you commit, they will both branch separately.
Once you understand references, they can be used to move branches around the worktree to previous commits. You can do so with the command git branch -f <branch name> <commit>
. The <commit>
can either be to another branch name, a hash, or use the caret or tilde commands. The -f
just means “force”.
Example #1 - Moving main up three commits Example #2 - Moving to branch
C0 C0
| |
C1 - main C1
| / \
C2 C2 C3
| | |
C3 main' - C4 C5
| |
C4 - *dev C6 - main & dev
git branch -f main HEAD~3 git branch -f main dev
In the first example, if we assume main
(and HEAD) was on the same commit as dev
, main will move up three parents from HEAD. In the second, we moved main, which was on a different branch, to our dev branch.
When you initialize a new Git repository (repo), you typically work within the main
branch. By creating a new branch (such as dev
), you can now jump between the two and separate your “live” product from your “development” work.
Let’s say that you created a new branch, and now want to start working IN that new branch. To do so, you’ll use the command git checkout <branch name> <location>
. For example, if your new branch is called dev
, then you’ll write git checkout dev
. You can also specify where you want to checkout a branch (such as a branch or commit). See References for details.
Example #1 - Checkout new branch Example #2 - Commit from branch
C0 C0
| |
C1 - main & *dev C1 - main
|
C2 - *dev
The *
denotes which branch you are currently working in.
You can actually create a new branch AND checkout that branch at the same time by using the command git checkout -b <branch name> <location>
.
If you git commit
while checked out in dev
, you’ll end up with a working tree like in Example #2.
Notice how the main
branch is now behind dev
. That’s because main
still refers to its respective commits even if you’re working in other branches. Your “live” product (in main
) is safe from any work you’re doing in dev
.
Each commit in a git repo has a unique identifier, called a hash
. It’s usually a long string of letters and numbers like (such as 472dadae3b2575fb313e12854efab28e2ebe92b3
). As they’re all unique however, you can refer to them fairly quickly by relatively identifying them by their first digits. For example, the relative reference for the previously mentioned hash would be 472dada
.
HEAD is just the way of referencing the currently checked out commit. Usually, HEAD will reside within the branch name, but we can change it to any commit we want. Let’s say our current working tree looks like this, and we’re currently checked out in dev
.
Example #1 - HEAD > dev > C4 Example #2 - HEAD > C4
C0 C0
| |
C1 C1
/ \ / \
main - C2 C3 main - C2 C3
| |
C4 - *dev HEAD - C4 - *dev
git checkout C4
If we want HEAD to refer to a particular commit, we’ll want to refer it to the appropriate hash. We’d do so with the git checkout <relative hash>
command.
As previously mentioned, HEAD usually resides in the same commit as the branch. In this example:
C0
|
C1
/ \
main - C2 C3
|
C4 - *dev
HEAD > dev > C4
With the caret ^
, we can move HEAD up the worktree to previous commits. The ^
will refer to the current commit’s parent.
Example #1 - One Caret Example #2 - Multiple Carets
C0 C0
| |
C1 C1 - HEAD
/ \ / \
main - C2 C3 - HEAD main - C2 C3
| |
C4 - *dev C4 - *dev
git checkout dev^ git checkout dev^^
The more ^
we add, the higher up it goes. This could also be achieved by typing git checkout HEAD^
twice.
Optionally, you can add a number to the caret ^<#>
, such as in git checkout dev^2
. In the examples above, it wouldn’t accomplish much. In the case where a commit references to another branch (in the case of a merge for example), then HEAD would follow the branch route, as opposed to the main
route. A ^3
would follow the third branch, and so on. See chaining for a more concrete example.
The Tilde ~
allows you to move up the worktree much quicker. Instead of typing multiple ^
, you can simply write a ~<#>
to specify how many commits to ascend.
Example #1 - Using the Tilde
C0 - HEAD
|
C1
|
C2
|
C3
|
C4 - main
git checkout HEAD~4
You can actually chain the caret ^
and the tilde ~
in a single command to move HEAD along the worktree. For example:
Example - Chaining ^ & ~
C0
/ \
C1 C3 - HEAD
| |
C2 C4
| |
| C5
| /
| /
| /
C6
|
C7 - *main
git checkout HEAD~^2~2
Let’s break down the command above by starting at main
. We move up a single commit w/ the first ~
, then move to the 2nd branch w/ ^2
(C5), then move up another two commits w/ ~2
. HEAD ends up at C3.
Let’s say you’ve done some work in a branch (like dev
), and are now ready to add them to your main
branch (your “live” product). Wouldn’t it be nice to merge them together? The command git merge <branch name>
allows us to do just that.
Merging can be a little complex to visualize and easy to “mess up”. For the sake of simplicity, we’ll only use two branches (main
and dev
).
C0
|
C1
/ \
*dev - C2 C3 - main
We have two branches with separate commits. While you were working on dev
(currently checked out), a colleague pushed some new work in the main
branch. Now it’s time to merge your work into main
.
Example #1 - Merging from branch Example #2 - Merging from main
C0 C0
| |
C1 C1
/ \ / \
C2 C3 dev - C2 C3
\ | \ |
\ | \ |
C4 - main & *dev C4 - *main
git merge main git checkout main
git merge dev
Since you were working in dev
and merged into main
, Git simply brings your current branch to a new commit along with main
. However, if you wanted to keep dev
in it’s current location, you can checkout in main
first and then merge dev
. In most cases, you’d likely want the first scenario to stay as up-to-date as possible w/ main.
It’s easy to forget that Git is actually a tool for version control, and a convenient way to keep track of changes and file history in development. When merging branches, the history of your changes will to its respective branches. But what if you wanted to “pull” the whole history from a working branch (such as dev
), and visually see them in a linear fashion in your “live” product (like main
)? This is when git rebase <branch name>
would come into play. For consistency and simplicity, let’s start with our previous example.
C0
|
C1
/ \
*dev - C2 C3 - main
Example #1 - Rebasing main Example #2 - Checkout main & rebasing
C0 C0
| |
C1 C1
/ \ / \
C2 C3 - main C2 C3
| |
C2' - *dev C2' - *main & dev
git rebase main git rebase dev main
Rebasing will take the commit from your working branch, make a copy and paste it to your main
branch. However, the main
branch is now behind the development branch. To fix this, you can type the second branch first (like dev
), followed by main
.
In the second example, you can think of the first branch as the reference point, and the next branch you want to bring TO the reference point. In this case, you’re bringing main
-> dev
.
You can also rebase “interactively”, which behaves much like git cherry-pick
(see git cherry-pick), but it will open up a dialog to ask which commits to rebase. Interactive rebase allows you to do lots of things, such as: Combining commits, amending commit messages, edit commits, reorder commits, keep or drop commits altogether.
Example #1 - Init Example #2 - Interactive Rebase
C0 C0
| |
C1 C1
| / \
C2 C2 C4'
| | |
C3 C3 C3' - *main
| |
C4 - *main C4
git rebase -i HEAD~3
In the example above, we’re interactively rebased the main
to omit C2, and change the order of C4 & C3. You would perform this through an interactive dialog box.
git reset
will move a branch back to a previous commit, and “erase” any commit up to that point. This is usually used at a local level.
Example #1 - Init Example #2 - Reset visualization
C0 C0
| |
C1 C1 - *main
|
C2 - *main
git reset HEAD~1
The main reference point returns to the first commit as though the second never happened.
Although git reset
works well on local git repos, it doesn’t really work when working through remote branches where there are multiple team members (if you push your work on Github or Gitlab). If this is the case, git revert
is a better option.
git revert
doesn’t roll back a commit like reset
. Instead, it will push a new commit which reverses the changes from the previous one.
Example #1 - Init Example #2 - Revert visualization
C0 C0
| |
C1 C1
| |
C2 - *main C2
|
C2' - *main
git revert HEAD
This way, anyone working on the project can see the revert
, and it will be logged in the repos history.
git cherry-pick
performs quite similarly to git rebase
. However, in the event where you want to selectively pick which commits to “rebase”, then you want to use git cherry-pick <commits...>
.
Example #1 - Init Example #2 - Using cherry-pick
C0 C0
| |
C1 C1
/ \ / \
C2 C3 - *main C2 C3
| | |
C3 C3 C2'
| | |
dev - C4 dev - C4 C4' - *main
git cherry-pick C2 C4
In this example, we want to copy the C2 & C4 commits from dev
into main
. Making sure we’re checked out in main
first, we git cherry-pick C2 C4
and it will “rebase” those commits into main
. In these examples, we only cherry-picked from two branches, but you can cherry-pick from as many branches as you’d like as you’re specifying each commit individually.
Tags are markers for Git repos. They permanently indicate a commit and can be referenced in a branch. It’s important to keep in mind however that tags cannot be checked out, you can’t work on them, and they don’t move as commits are created. They are “anchors” in the worktree. You can do so with the command git tag <tag name> <commit>
.
Tags are often used to indicate major releases or milestones in a git repo.
git describe <ref>
will indicate where you are in relation to the closest tag. The <ref>
, if left blank, will use HEAD as its reference point. Otherwise, you can refer to a commit, a branch or HEAD.
The output looks like:
<tag>_<numCommits>_g<hash>
<tag>
is the closest anchor in the history, <numCommits>
is how many commits away that tag is, and <hash>
is the associated hash for <ref>
.
Example #1 - Describe main Example #2 - Describe dev
C0(v1) C0(v1)
| |
C1 C1
/ \ / \
main - C2 C3(v2) C2 C3(v2)
| |
C4 - *dev C4 - *dev
git describe main git describe dev
The output for Example #1 would be v1_2_gC2
, while the output for Example #2 would be v2_1_gC4
.
So far, most of the commands we’ve seen are used at a local level to control the worktree on your own computer. However, the power of Git repositories can be found online through platforms such as Github & Gitlab. Remotes are online “backups” and allow git repositories to become more interactive among the online community.
git clone
allows you to create a local copy of a remote repository.
When you clone a repository, you end up with a new branch in your repository with a <remote name>/<branch name>
convention. More often than not, this looks like origin/main
. This is called a Remote branch. A remote branch will reflect the state of a remote repository (since you last referenced it). A remote branch is on your local version of the repository.
When you checkout a remote branch, you actually operate in a detached HEAD mode since you CANNOT work on the branches directly. You work on them elsewhere (such as a local PC), then share it with the remote.
Example #1 - Checking out a Remote Branch
C0
|
C1 - main & origin/main
|
(C2) - HEAD
git checkout origin/main
git commit
When we checkout origin/main and then commit, we are put in detached HEAD mode. main
& origin/main
are NOT affected by git commit
; Only HEAD is. origin/main
will only update once we update the remote repository.
When you clone a remote repository, main
and origin/main
become “connected”. By this, I mean that when you do a git pull
(see git pull), commits are downloaded on origin/main
and then merged on main
. On the flipside, when you git push
(see git push), work on your local main
is pushed onto the remote main
, and is then represented on the origin/main
locally.
At any point, you can point origin/main
to another local branch of your choice. For example, if you work on dev
locally and point it to origin/main
using the command git checkout -b dev origin/main
, then git push
to remote, it will push your work to the remote main
branch.
Example #1 - Init
(local) (remote)
C0 C0
| |
C1 - *main & origin/main C1
|
C2 - main
Example #2 - Pointing origin/main to dev, then pulling.
(local) (remote)
C0 C0
| |
C1 - main C1
| |
C2 - *dev & origin/main C2 - main
git checkout -b dev origin/main
git pull
In the example above, we create a new branch called dev
and point it to follow origin/main
. We then use the git pull
function to update origin/main
, and therefore dev
as well. The same is applicable for git push
.
In order to set remote tracking on a branch, we use the git branch -u origin/main <branch name>
command. If <branch name>
is blank, it will use the branch currently checked out.
git fetch
allows you to retrieve data from the remote repository. It will download the missing commits on our local repository FROM the remote repository. It will then update where our remote branch points to.
Example #1 - Init
(local) (remote)
C0 C0
| |
C1 - *main & origin/main C1
|
C2
|
C3 - main
Example #2 - Using git fetch
(local) (remote)
C0 C0
| |
C1 - *main C1
| |
C2 C2
| |
C3 - origin/main C3 - main
Slightly different layout in these examples. On the left-hand side, we have our local repositories, while on the right we have our remote repositories (hosted on Github for example).
If we only have our first two commits in our local repository, but the remote has two more (C2 & C3), then using git fetch
allows us to retrieve C2
& C3
from remote. This will consequently update origin/main
. However, main
will remain in place.
git fetch
DOES NOT change your local state. It does not update your main
branch or change your files. It simply downloads the missing ones.
Fetch can optionally take some options: git fetch <remote> <location>
. <remote>
refers to the local origin/<branch>
branch in your repository where everything will be downloaded. <location>
is the remote branch it will download from. It will download to origin/<branch>
as to not mess up the files on your current <branch>
, unless explicitly stated using the <source>:<destination>
refspec.
If you declare the <location>
, then Git ignores where we are currently checked out. In the case where you would like to set the source AND destination (such as if you want to push to a secondary remote branch, not main), you can turn <location>
into <source>:<destination>
. For example: git push origin feature:dev
. You can even use carets ^
and tilde ~
in the source, or create new branches on the remote! <source>
refers to the location on the remote, while <destination>
refers to the local place to put those commits.
In the case where <source>
is empty, then that will create a local branch. For example: git fetch origin :dev
will create a local dev
branch.
Fetching from the remote repository is crucial to having the most up-to-date files from remote. Once done, you can merge, rebase, cherry-pick, etc. origin/main
at any point. OR….
Use git pull
!
git pull
is the short-form way of using a git fetch
to update origin/main
, followed by git merge origin/main
. In both cases, you fetch the information from remote and update main
to reflect those changes.
Pull can take the same arguments as git fetch
. Doing so however will perform the fetch
-> merge
using the specified arguments. For example:
git pull origin dev
is the same as saying:
git fetch origin dev
git merge origin/dev
Just like git pull origin dev~1:feature
is the same as:
git fetch origin dev~1:feature
git merge feature
git push
allows you to upload your changes to the remote repository. In other words, it publishes your work from your local repository to the remote one. Keep in mind that the behaviour of git push
changes depending on your default settings.
When you use git push
, it updates the remote repository with any outstanding commits it does not have, and points your local remote branch (origin/main
) to the most current commit as well to ensure everything is synced.
Push can optionally take some options: git push <remote> <location>
. <remote>
refers to the remote repository where everything will be pushed. <location>
is where all the commits will come from. If you declare the <location>
, then Git ignores where we are currently checked out. In the case where you would like to set the source AND destination (such as if you want to push to a secondary remote branch, not main), you can turn <location>
into <source>:<destination>
. For example: git push origin feature:dev
. You can even use carets ^
and tilde ~
in the source, or create new branches on the remote!
In the case where <source>
is empty, then that will delete the remote branch. For example: git push origin :dev
will delete the remote dev
branch.
Keep in mind that when HEAD is in detached mode (or not on a remote-tracking branch), then git push
will fail.
When you’re working alone on a repository, you manage all your files both locally and remotely. For the most part, everything should be synced no matter the circumstance.
What happens when MULTIPLE people are working on the same project? What if you git pull
on a Monday, work on a feature throughout the week and try to git push
on the Friday? Chances are, the remote repository has received several pushes from other users in the meantime.
In these cases, git push
will not push your work to the remote repository.
One way we can accomplish the same with git merge
instead.
Example #1 - Init
(local) (remote)
C0 C0
| |
C1 - origin/main C1
| |
C3 - *main C2 - main
Example #2 - Rebasing origin/main
(local) (remote)
C0 C0
| |
C1 C1
/ \ / \
C3 C2 C2 C3
| / \ |
| / \ |
C4 - *main & origin/main C4 - main
git fetch
git rebase origin/main
git push
We fetch the outstanding commits, and merge it into a new commit (C4
), then applied the same to the remote. This is the default way of solving divergent history when you git pull; git push
Another way to resolve this kind of problem is to git fetch
and then git rebase
.
Example #1 - Init
(local) (remote)
C0 C0
| |
C1 - origin/main C1
| |
C3 - *main C2 - main
Example #2 - Rebasing origin/main
(local) (remote)
C0 C0
| |
C1 C1
/ \ |
C3 C2 C2
| |
C3' - *main & origin/main C3 - main
git fetch
git rebase origin/main
git push
When we rebase origin/main
, we “reflect” the changes in the remote repository, then we push our changes. No conflicts!
A shorthand form to accomplish this type of rebasing is git pull --rebase
.
In large collaborative projects, it’s important to protect branches, especially if one of them is your “live” product (like main
). You rarely ever want to push to main
directly in these scenarios. Instead, create a branch which will then be merged into main
. However, in the event you push to main
by mistake, you may be greeted with an error which requests you to “pull first”. To solve this:
Create another branch and push that to remote, and git reset
main
to be in sync with remote.