git commit; git push; error :(
A note on my learning journey with Git from this great source https://learngitbranching.js.org/
Basics of Git
These following sections are the basics of Git that is enough for a majority of all workflows. It includes concepts like components of a working tree (commit, branch, HEAD), branching and merging, and how to reverse changes in Git.
Git Commits
Git commits: snapshots of all the tracked files in your working directory.
- Git can (when possible) compress a commit as a set of changes, or a “delta”, from one commit to the next.
- Git can maintain a history of these changes.
- Git can synchronize changes across multiple people.
Demonstration:
git init
echo "Hello World" > file.txt
git add file.txt
git commit -m "Initial commit"
echo "How are you?" >> file.txt
git commit -am "Second commit"
Git Branches
Branches in Git are incredibly lightweight as they are just pointers to a specific commit. A branch essentially says: “I want to include the work of this commit and all parent commits”.
Demonstration:
git branch bugFix # create a new branch called bugFix
git checkout bugFix # switch to the bugFix branch
echo "Bug fixed" >> file.txt # make changes to the file
git commit -am "Bug fixed" # commit the changes in the bugFix branch
git checkout main # switch to the main branch
echo "New feature" >> file.txt
git commit -am "New feature"
In the above example, bugFix
and main
are two branches. bugFix
branch has a commit that fixes a bug, while main
branch has a commit that adds a new feature.
Demonstration 2:
git init
echo "Hello World" > file.txt
git add file.txt
git commit -m "Initial commit"
git branch bugFix
echo "Bug fixed" >> file.txt
git commit -am "Bug fixed"
-
git checkout bugFix
: switch to thebugFix
branch -
git checkout -b bugFix
: create a new branch calledbugFix
and switch to it -
git checkout main
: switch to themain
branch
Braches and Merging
Merging in Git creates a special commit that has two unique parents. A merge commit is necessary because a repository can have multiple branches with different commits. Essentially, a merge commit is to unify the changes of two branches.
In the above example, C4
is a merge commit of C2
- branch bugFix
and C3
- branch main
. Therefore, C4
has two parents: C2
and C3
, which means C4
contains all the changes/works of both C2
and C3
.
Moreover, in the above example, we merge bugFix
into main
branch (illustrated by the same color). However, we can also merge main
into bugFix
branch.
Demonstration:
git init
git commit -m "C1"
git checkout -b bugFix; git commit -am "C2" # create a new branch bugFix and switch to it and then commit
git checkout main; git commit -am "C3" # switch to main and commit
git merge bugFix # merge bugFix into main
Rebase
Rebasing is the process of moving or combining a sequence of commits to a new base commit. Rebasing essentially takes a set of commits, “copies” them, and plops them down somewhere else. The advantage of rebasing is that it can be used to make a nice linear sequence of commits.
-
git rebase <branchA> <branchB>
: rebasebranchA
ontobranchB
. This means that all the commits inbranchA
will be “copied” and “pasted” ontobranchB
. HEAD will be at the tip ofbranchA
after the rebase. -
git rebase <branch>
: rebase the current branch ontobranch
. This is equivalent togit rebase HEAD <branch>
.
Demonstration:
git init
git commit -m "C1"
git branch bugFix
git checkout bugFix
git commit -m "C2"
git checkout main
git commit -m "C3"
git checkout bugFix
git rebase main # rebase bugFix onto main
HEAD
HEAD is the symbolic name for the currently checked out commit. It is essentially the “current branch”. When you switch branches with git checkout
, the HEAD revision changes to point to the tip of the new branch.
Detaching HEAD just means attaching it to a commit instead of a branch. For example, git checkout C1
will detach HEAD and attach it to commit C1
.
Demonstration:
git init
git commit -m "C1" # HEAD -> main -> C1
git branch bugFix # HEAD -> main -> C1
git checkout bugFix # HEAD -> bugFix -> C1
git commit -m "C2" # HEAD -> bugFix -> C2
git checkout main # HEAD -> main -> C1
git commit -m "C3" # HEAD -> main -> C3
git merge bugFix # HEAD -> main -> C3 -> C2
git commit -m "C4" # HEAD -> main -> C4 -> C3 -> C2
git checkout C4 # HEAD -> C4
Relative Refs
Relative refs are another powerful way to specify commits. They are almost exclusively used for moving around in history and are great for “walking back in time”.
-
^
: indicates the parent commit.^^
indicates the grandparent commit. -
~
: indicates the first parent commit.~2
indicates the first parent’s parent commit.~n
indicates the nth parent commit. -
^2
: indicates the second parent commit. This is useful for merge commits where one commit has two parents. - You can also use HEAD as a relative ref. For example,
HEAD^
is the parent commit of the current commit.
Demonstration:
git branch -f main C6 # move the main branch to commit C6
git checkout HEAD~1 # move HEAD to the parent commit of the current commit
git branch -f bugFix HEAD~1 # move the bugFix branch to the parent commit of the current commit
In the above example, git branch -f source des_commit
forces the source
branch to point to a specific destination commit des_commit
, while git checkout HEAD~1
moves HEAD to the parent commit of the current commit.
Comparing to git reset
, git branch -f
is used to move branches around, while git reset
is used to move HEAD and the current branch around.
Advanced: We can also combine relative refs such as HEAD~^2~2
which moves HEAD to the grandparent of the second parent of the parent of the parent of the current commit as shown in the illustration below.
Reversing Changes in Git
There are many ways to reverse changes in Git. Low-level approach like staging
individual files or high-level approach like git revert
or git reset
.
-
git reset
reverts changes by moving a branch reference backward in time to an older commit. In this way, it can be used to “undo” commits or “rewriting history”. All commits after the reset commit will be lost. -
git revert
creates a new commit that reverts the changes of a previous commit. It is a safe way to undo and can be shared with others unlikegit reset
.
Demonstration: Given the current working directory, reverse the most recent commit on both local and pushed (one per branch).
git reset HEAD~1 # reverse the most recent commit on the current branch
git checkout pushed # switch to the pushed branch
git revert HEAD # reverse the most recent commit on the pushed branch
Moving Work Around
This section covers more advanced topics in Git such as cherry-picking, interactive rebase, and tags.
Cherry-pick
Cherry-picking in Git means to choose a specific commit from one branch and apply it onto another. This is in contrast with other ways such as merge
or rebase
which apply many commits onto another branch.
For example, git cherry-pick C3 C4
will apply the changes of commits C3
and C4
onto the current branch.
Interactive Rebase
Interactive rebase is a powerful tool to re-write history. It allows you to change the order of commits, combine multiple commits into one, or even delete commits.
Situations where interactive rebase is useful:
- Cleaning up a messy history before merging a feature branch.
- Squashing commits together to make them more readable.
- Splitting a commit into smaller commits.
- Removing a commit that introduced a bug.
- Reordering commits to make them more logical.
- Changing commit messages.
Practical scenario: You have some changes (newImage) and another set of changes (caption) that are related, so they are stacked on top of each other in your repository. And you need to make a small modification to an earlier commit (newImage).
There are several approaches to solve this problem:
-
Approach 1: Use
git rebase -i
to reorder the commits and then usegit commit --amend
to modify the commit. Then usegit rebase -i
to reorder the commits back to the original order. -
Approach 2: Use
git cherry-pick
to apply the changes of the commit to a new commit. Then usegit rebase -i
to delete the original commit and reorder the new commit to the original position.
Demonstration: Given the current working directory, modify the commit C3 twice and the commit C2 three times, then reorder the commits back to the original order (C1, C2’’’, C3’’) on the main branch.
git rebase -i HEAD~2 # rebasing the last two commits. Assuming the modifications are made. Reorder the commits as HEAD -> C2' -> C3' -> C1
git rebase -i HEAD~1 # rebasing the last commit. Assuming the modifications are made. HEAD -> C2'' -> C3' -> C1
git rebase -i HEAD~2 # reorder the commits back to the original order. HEAD -> C3'' -> C2''' -> C1
git checkout main # switch to the main branch
git merge caption # merge the caption branch into the main branch, fast-forward
Example with git cherry-pick
:
git checkout C1
git cherry-pick C2 # apply the changes of commit C2 onto the current branch
git checkout main # switch to the main branch
git cherry-pick C2^ # apply the changes of commit C2' onto the main branch
git cherry-pick C3 # apply the changes of commit C3 onto the main branch
Git Tags
Git tags are a way to mark specific points in history as being important. They are often used to mark release points (v1.0, and so on).
-
git tag <tagname>
: create a new tag -
git tag <tagname> <commit>
: create a new tag that points to a specific commit -
git tag -a <tagname> -m "message"
: create an annotated tag -
git tag -d <tagname>
: delete a tag
git tag v1 side~1 # create a tag v1 that points to the commit that is one before recent commit of the branch `side`
git tag v0 main~2 # create a tag v0 that points to the commit that is two before recent commit of the branch `main`
Git Describe
Git describe is a way to get a human-readable description of a commit. It is often used to provide a more user-friendly way to identify a commit.
git describe <ref>
: describe the most recent commit reachable from the ref
. If the ref
is omitted, it defaults to HEAD.
The output of git describe
is in the format <tag>_<numCommits>_g<hash>
. For example, v1_2_ga6f4
means that the most recent tag is v1
, there are 2 commits since the tag, and the hash of the commit being described is a6f4
.
Exercises
Example 1: Given the current working history, arrange the commits as shown in the goal.
Solution:
git rebase main bugFix # rebase bugFix onto main. HEAD -> bugFix
git rebase bugFix side # rebase side onto bugFix, HEAD -> side
git rebase side another # rebase another onto side, HEAD -> another
git rebase another main # rebase main onto another, HEAD -> main
Example 2: given the current working history, create a new branch bugWork
at the specified destination as shown in the goal.
It can simply be done by git branch bugWork C2
. However, the goal is to use relative refs to specify the destination commit.
Solution:
git branch bugWork HEAD~^2~ # Current HEAD at C7. HEAD~ is C6. HEAD~^2 is the second parent of C6 which is C5. HEAD~^2~ is the parent of C5 which is C2.
Example 3: given the current working history, where the main
branch is some commits ahead of the three branches one
, two
, and three
. The goal is to modify the three branches as shown in the goal.
Analysis: The simplest task is for the branch three
where it is just move forward one commit to the commit C2
. The branch one
can be done by cherry-pick
or rebase
while branch two
is the copy of branch one
with an additional commit C5
.
Solution:
git checkout three; git reset C2 # move the three branch to the commit C2
git checkout one; git cherry-pick C4 C3 C2 # apply the changes of commit C4, C3, and C2 onto the one branch
git checkout two; git cherry-pick C5; # apply the changes of commit C5 onto the two branch
git cherry-pick C4^ C3^ C2^ # apply the changes of the parent commit of C4, C3, and C2 onto the two branch
-
git checkout three; git reset C2
: move the branch three to the commitC2
. Alternatively,git branch -f three C2
can be used.
Working Remotely
This section covers the basics of working with remote repositories in Git. It includes concepts like cloning, fetching, pulling, pushing, and remotes.
Enjoy Reading This Article?
Here are some more articles you might like to read next: