Detailed Table of Contents
Guidance for the item(s) below:
Let's learn about a few more Git techniques, starting with branching. Although these techniques are not really needed for the iP, we require you to use them in the iP so that you have more time to practice them before they are really needed in the tP.
Target Usage: To make use of multiple timelines of work in a local repository.
Motivation: At times, you need to do multiple parallel changes to files (e.g., to try two alternative implementations of the same feature).
Lesson plan:
T6L1. Creating Branches covers that part.
T6L2. Merging Branches covers that part.
T6L3. Resolving Merge Conflicts covers that part.
T6L4. Renaming Branches covers that part.
T6L5. Deleting Branches covers that part.
To work in parallel timelines, you can use Git branches.
Git branches let you develop multiple versions of your work in parallel — effectively creating diverged timelines of your repository’s history. For example, one team member can create a new branch to experiment with a change, while the rest of the team continues working on another branch. Branches can have meaningful names, such as master
, release
, or draft
.
A Git branch is simply a ref (a named label) that points to a commit and automatically moves forward as you add new commits to that branch. As you’ve seen before, the HEAD
ref indicates which branch you’re currently working on, by pointing to the corresponding branch ref.
When you add a commit, it goes into the branch you are currently on, and the branch ref (together with the HEAD
ref) moves to the new commit.
Git creates a branch named master
by default (Git can be configured to use a different name e.g., main
).
Given below is an illustration of how branch refs move as branches evolve. Refer to the text below it for explanations of each stage.
master
) and there is only one commit on it. The HEAD
ref is pointing to the master
branch (as we are currently on that branch).master
and the HEAD
refs have moved to the new commit.fix1
has been added. The repo has switched to the new branch too (hence, the HEAD
ref is attached to the fix1
branch).c
) has been added. The current branch ref fix1
moves to the new commit, together with the HEAD
ref.master
branch. Hence, the HEAD
has moved back to master
branch's .b
(not c
).d
) has been added. The master
and the HEAD
refs have moved to that commit.fix1
branch and added a new commit e
to it. Note how the branch ref fix1
(together with HEAD
) has moved to the new commit e
while the branch ref master
still points to d
.Note that appearance of the revision graph (colors, positioning, orientation etc.) varies based on the Git client you use, and might not match the exact diagrams given above.
Preparation Fork the samplerepo-things repo, and clone it onto your computer.
1 Observe that you are on the branch called master
.
$ git status
on branch master
2 Start a branch named feature1
and switch to the new branch.
You can use the branch
command to create a new branch and the checkout
command to switch to a specific branch.
$ git branch feature1
$ git checkout feature1
One-step shortcut to create a branch and switch to it at the same time:
$ git checkout –b feature1
The new switch
command
Git recently introduced a switch
command that you can use instead of the checkout
command given above.
To create a new branch and switch to it:
$ git branch feature1
$ git switch feature1
One-step shortcut (by using -c
or --create
flag):
$ git switch –c feature1
Click on the Branch
button on the main menu. In the next dialog, enter the branch name and click Create Branch
.
Note how the feature1
is indicated as the current branch (reason: Sourcetree automatically switches to the new branch when you create a new branch, if the Checkout New Branch
was selected in the previous dialog).
3 Create some commits in the new branch, as follows.
numbers.txt
, stage it, commit it.feature
branch will becomes part of that branch.master
ref and the HEAD
ref move to the new commit.As before, you can use the git log --oneline --decorate
command for this.
At times, the HEAD
ref of the local repo is represented as in Sourcetree, as illustrated in the screenshot below
.
The HEAD
ref is not shown in the UI if it is already pointing at the active branch.
numbers.txt
, stage the changes, and commit it. This commit too will be added to the feature1
branch.4 Switch to the master
branch. Note how the changes you made in the feature1
branch are no longer in the working directory.
$ git switch master
Double-click the master
branch.
Revisiting master
vs origin/master
In the screenshot above, you see a master
ref and a origin/master
ref for the same commit. The former identifies the of the local master
branch while the latter identifies the tip of the master
branch at the remote repo named origin
. The fact that both refs point to the same commit means the local master
branch and its remote counterpart are with each other.
Similarly, origin/HEAD
ref appearing against the same commit indicates that of the remote repo is pointing to this commit as well.
5 Add a commit to the master branch. Let’s imagine it’s a bug fix.
To keep things simple for the time being, this commit should not involve the numbers.txt
file that you changed in the feature1
branch. Of course, this is easily done, as the numbers.txt
file you added in the feature
branch is not even visible when you are in the master
branch.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch feature1 commit id: "f1" commit id: "[feature1] f2" checkout master commit id: "[HEAD → master] m3" checkout feature1
6 Switch between the two branches and see how the working directory changes accordingly. That is, now you have two parallel timelines that you can freely switch between.
done!
You can also start a branch from an earlier commit, instead of the latest commit in the current branch. For that, simply check out the commit you wish to start from.
In the samplerepo-things
repo that you used above, let's create a new branch that starts from the same commit the feature1
branch started from. Let's pretend this branch will contain an alternative version of the content we added in the feature1
branch.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch feature1 branch feature1-alt checkout feature1 commit id: "f1" commit id: "[feature1] f2" checkout master commit id: "[HEAD → master] m3" checkout feature1-alt commit id: "[HEAD → feature1-alt] a1"
Avoid this rookie mistake!
Always remember to switch back to the master
branch before creating a new branch. If not, your new branch will be created on top of the current branch.
master
branch.feature1
branch diverged from the master
branch (e.g. git checkout HEAD~1
). This will create a detached HEAD
.feature1-alt
. The HEAD
will now point to this new branch (i.e., no longer 'detached').PRO-TIP: Creating a branch based on another branch in one shot
Suppose you are currently on branch b2
and you want to create a new branch b3
that starts from b1
. Normally, you can do that in two steps:
git switch b1 # switch to the intended base branch first
git switch -c b3 # create the new branch and switch to it
This can be done in one shot using the git switch -c <new-branch> <base-branch>
command:
git switch -c b3 b1
done!
EXERCISE: side-track
Most work done in branches eventually gets merged together.
Merging combines the changes from one branch into another, bringing their diverged timelines back together.
When you merge, Git looks at the two branches and figures out how their histories have diverged since their merge base (i.e., the most recent common ancestor commit of two branches). It then applies the changes from the other branch onto your current branch, creating a new commit. The new commit created when merging is called a merge commit — it records the result of combining both sets of changes.
Given below is an illustration of how such a merge looks like in the revision graph:
fix1
branch (as indicated by HEAD
).master
branch (thus, HEAD
is now pointing to master
ref).fix1
branch has been merged into the master
branch, creating a merge commit f
. The repo is still on the master
branch.A merge commit has two parent commits e.g., in the above example, the merge commit f
has both d
and e
as parent commits. The parent commit on the receiving branch is considered the first parent and the other is considered the second parent e.g., in the example above, fix1
branch is being merged into the master
branch (i.e., the receiving branch) -- accordingly, d
is the first parent and e
is the second parent.
Preparation We continue with the samplerepo-things
repo from earlier, which should look like the following. Note that we are ignoring the feature1-alt
branch, for simplicity.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch feature1 commit id: "f1" commit id: "[feature1] f2" checkout master commit id: "[HEAD → master] m3" checkout feature1
1 Switch back to the feature1
branch.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch feature1 commit id: "f1" commit id: "[HEAD → feature1] f2" checkout master commit id: "[master] m3" checkout feature1
2 Merge the master
branch to the feature1
branch, giving an end-result like the following. Also note how Git has created a merge commit (shown as mc1
in the diagram below).
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch feature1 commit id: "f1" commit id: "f2" checkout master commit id: "[master] m3" checkout feature1 merge master id: "[HEAD → feature1] mc1"
$ git merge master
Right-click on the master
branch and choose merge master into the current branch
. Click OK
in the next dialog.
The revision graph should look like this now (colours and line alignment might vary but the graph structure should be the same):
Observe how the changes you made in the master
branch (i.e., the imaginary bug fix in m3
) is now available even when you are in the feature1
branch.
Furthermore, observe (e.g., git show HEAD
) how the merge commit contains the sum of changes done in commits m3
, f1
, and f2
.
3 Add another commit to the feature1
branch, in which you do some further changes to the numbers.txt
.
Switch to the master
branch and add one more commit.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch feature1 commit id: "f1" commit id: "f2" checkout master commit id: "m3" checkout feature1 merge master id: "mc1" commit id: "[feature1] f3" checkout master commit id: "[HEAD → master] m4"
4 Merge feature1
to the master branch, giving an end-result like this:
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch feature1 commit id: "f1" commit id: "f2" checkout master commit id: "m3" checkout feature1 merge master id: "mc1" commit id: "[feature1] f3" checkout master commit id: "m4" merge feature1 id: "[HEAD → master] mc2"
git merge feature1
Right-click on the feature1
branch and choose Merge...
. The resulting revision graph should look like this:
Now, any changes you made in feature1
branch are available in the master branch.
done!
When the branch you're merging into hasn't diverged — meaning it hasn't had any new commits since the merge base — Git simply moves the branch pointer forward to include all the new commits, keeping the history clean and linear. This is called a fast-forward merge because Git simply "fast-forwards" the branch pointer to the tip of the other branch. The result looks as if all the changes had been made directly on one branch, without any branching at all.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "[HEAD → master] m2" branch bug-fix commit id: "b1" commit id: "[bug-fix] b2" checkout master
→
[merge bug-fix
]
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" commit id: "b1" commit id: "[HEAD → master][bug-fix] b2" checkout master
In the example above, the master
branch has not changed since the merge base (i.e., m2
). Hence, merging the branch bug-fix
onto master
can be done by fast-forwarding the master
branch ref to the tip of the bug-fix
branch (i.e., b2
).
Preparation Let's continue with the same samplerepo-things
repo we used above, and do a fast-forward merge this time.
1 Create a new branch called add-countries
, and some commits to it as follows:
Switch to the new branch, add a file named countries.txt
, stage it, and commit it.
Do some changes to countries.txt
, and commit those changes.
You should have something like this now:
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "[master] mc2" branch add-countries commit id: "a1" commit id: "[HEAD → add-countries] a2"
2 Go back to the master
branch.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "[HEAD → master] mc2" branch add-countries commit id: "a1" commit id: "add-countries] a2"
3 Merge the add-countries
branch onto the master
branch. Observe that there is no merge commit. The master
branch ref (and the HEAD
ref along with it) moved to the tip of the add-countries
branch (i.e., a2
) and both branches now point to a2
.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master (and add-countries)'}} }%% commit id: "mc2" commit id: "a1" commit id: "[HEAD → master][add-countries] a2"
done!
It is possible to force Git to create a merge commit even if fast forwarding is possible. This is useful if you prefer the revision graph to visually show when each branch was merged to the main timeline.
To prevent Git from fast-forwarding, use the --no-ff
switch when merging. Example:
git merge --no-ff add-countries
Windows: Tick the box shown below when you merge a branch:
Mac:
Trigger the branch operation using the following menu button:
In the next dialog, tick the following option:
To permanently prevent fast-forwarding:
Settings
.Git
section.Do not fast-forward when merging, always create commit
.A squash merge combines all the changes from a branch into a single commit on the receiving branch, without preserving the full commit history of the branch being merged. This is especially useful when the feature branch contains many small or experimental commits that would clutter the main branch’s history. By squashing, you retain the final state of the changes while presenting them as one cohesive unit, making the project history easier to read and manage. It also helps maintain a linear, simplified commit log on the main branch.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "[HEAD → master] m1" branch feature checkout feature commit id: "f1" commit id: "[feature] f2"
→
[squash merge...]
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch feature checkout feature commit id: "f1" commit id: "[feature] f2" checkout master commit id: "[HEAD → master] s1 (same as f1+f2)" type: HIGHLIGHT
In the example above, the branch feature
has been squash merged onto the master
branch, creating a single 'squashed' commit s1
that combines all the commits in feature
branch.
EXERCISE: branch-bender
DETOUR: Undoing a Merge
In the example below, you merged master
to feature1
.
If you want to undo that merge,
feature1
branch (because that's the branch that received the merge).feature1
branch to the commit highlighted (in yellow) in the screenshot above (because that was the tip of the feature1
branch before you merged the master
branch to it).DETOUR: Comparing Branches
Comparing branches in Git is useful when you want to understand how two lines of development differ — for example, before merging a branch, you might want to review what changes it introduces to the main branch.
Here are two ways to compare two branches:
git diff branchA..branchB
: Changes based on commits in branchB
but not in branchA
. This is the one used more commonly.git diff branchA...branchB
: This shows changes in all the commits that are reachable by either of two references but not by both of them.branchA
or branchB
.DETOUR: Doing a Squash Merge
To do a squash merge, you can use the --squash
switch. It will prepare the squashed merge commit but will stop short of actually finalising the commit.
git merge --squash feature-1
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
At that point, you can go ahead and make the commit yourself, with the commit message you want.
When merging branches, you need to guide Git on how to resolve conflicting changes in different branches.
A merge conflict happens when Git can't automatically combine changes from two branches because the same parts of a file were modified differently in each branch. When this happens, Git pauses the merge and marks the conflicting sections in the affected files so you can resolve them yourself. Once you've reviewed and fixed the conflicts, you can tell Git they're resolved and complete the merge.
More generally, a conflict occurs when Git cannot automatically reconcile different changes made to the same part of a file -- branch merge conflicts is just one example.
Target To simulate a merge conflict and use it to learn how to resolve merge conflicts.
Preparation You can use any repo with at least one commit in the master
branch.
1 Start a branch named fix1
in the repo. Create a commit that adds a line with some text to one of the files.
2 Switch back to master
branch. Create a commit with a conflicting change i.e., it adds a line with some different text in the exact location the previous line was added.
3 Try to merge the fix1
branch onto the master
branch. Git will pause mid-way during the merge and report a merge conflict. If you open the conflicted file, you will see something like this:
COLORS
------
blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red
white
4 Observe how the conflicted part is marked between a line starting with <<<<<<
and a line starting with >>>>>>
, separated by another line starting with =======
.
Highlighted below is the conflicting part that is coming from the master
branch:
blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red
This is the conflicting part that is coming from the fix1
branch:
blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red
5 Resolve the conflict by editing the file. Let us assume you want to keep both lines in the merged version. You can modify the file to be like this:
COLORS
------
blue
black
green
red
white
6 Stage the changes, and commit. You have now successfully resolved the merge conflict.
done!
EXERCISE: conflict-mediator
Branches can be renamed, for example, to fix a mistake in the branch name.
Local branches can be renamed easily. Renaming a branch simply changes the branch reference (i.e., the name used to identify the branch) — it is just a cosmetic change.
Preparation First, create the repo samplerepo-books
for this hands-on practical, by running the following commands in your terminal.
mkdir samplerepo-books
cd samplerepo-books
git init
echo "Horror Stories" >> horror.txt
git add .
git commit -m "Add horror.txt"
git switch -c textbooks
echo "Textbooks" >> textbooks.txt
git add .
git commit -m "Add textbooks.txt"
git switch master
git switch -c fantasy
echo "Fantasy Books" >> fantasy.txt
git add .
git commit -m "Add fantasy.txt"
git switch master
git merge --no-ff -m "Merge branch textbooks" textbooks
The above should give you a repo similar to the revision graph given below, on the left.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch textbooks checkout textbooks commit id: "[textbooks] t1" checkout master branch fantasy checkout fantasy commit id: "[fantasy] f1" checkout master merge textbooks id: "[HEAD → master] mc1"
→
[rename branches]
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch study-books checkout study-books commit id: "[study-books] t1" checkout master branch fantasy-books checkout fantasy-books commit id: "[fantasy-books] f1" checkout master merge study-books id: "[HEAD → master] mc1"
Target Rename the fantasy
branch to fantasy-books
. Similarly, rename textbooks
branch to study-books
. The outcome should be similar to the revision graph above, on the right.
steps:
To rename a branch, use the git branch -m <current-name> <new-name>
command (-m
stands for 'move'):
git branch -m fantasy fantasy-books
git branch -m textbooks study-books
git log --oneline --decorate --graph --all # verify the changes
* 443132a (HEAD -> master) Merge branch textbooks
|\
| * 4969163 (study-books) Add textbooks.txt
|/
| * 0586ee1 (fantasy-books) Add fantasy.txt
|/
* 7f28f0e Add horror.txt
Note these additional switches to the log
command:
--all
: Shows all branches, not just the current branch.--graph
: Shows a graph-like visualisation (notice how *
is used to indicate a commit, and branches are indicated using vertical lines).Right-click on the branch name and choose Rename...
. Provide the new branch name in the next dialog.
done!
SIDEBAR: Branch naming conventions
Branch names can contain lowercase letters, numbers, /
, dashes (-
), underscores (_
), and dots (.
).
You can use uppercase letters too, but many teams avoid them for consistency.
A common branch naming convention is to prefix it with <category>/
. Some examples:
feature/login-form
— for new features (origin/feature/login-form
could be the matching remote-tracking branch)bugfix/profile-photo
— for fixing bugshotfix/payment-crash
— for urgent production fixesrelease/2.0
— for prepping a releaseexperiment/ai-chatbot
— for “just trying stuff”Although forward-slash (/
) in the prefix doesn't mean folders, some tools treat it kind of like a path so you can group related branches when you run git branch. Shown below is an example of how Sourcetree groups branches with the same prefix.
EXERCISE: branch-rename
Branches can be deleted to get rid of them when they are no longer needed.
Deleting a branch deletes the corresponding branch ref from the revision history (it does not delete any commits). The impact of the loss of the branch ref depends on whether the branch has been merged.
When you delete a branch that has been merged, the commits of the branch will still exist in the history and will be safe. Only the branch ref is lost.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch bug-fix checkout bug-fix commit id: "[bug-fix] b1" checkout master merge bug-fix id: "[HEAD → master] mc1"
→
[delete branch bug-fix
]
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch _ checkout _ commit id: "b1" checkout master merge _ id: "[HEAD → master] mc1"
In the above example, the only impact of the deletion is the loss of the branch ref bug-fix
. All commits remain reachable (via the master
branch), and there is no other impact on the revision history.
In fact, some prefer to delete the branch soon after merging it, to reduce branch references cluttering up the revision history.
When you delete a branch that has not been merged, the loss of the branch ref can render some commits unreachable (unless you know their commit IDs or they are reachable through other refs), putting them at risk of being lost eventually.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "[HEAD → master] m1" branch bug-fix checkout bug-fix commit id: "[bug-fix] b1" checkout master
→
[delete branch bug-fix
]
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "[HEAD → master] m1" branch _ checkout _ commit id: "b1" checkout master
In the above example, the commit b1
is no longer reachable, unless we know its commit ID (i.e., the SHA
).
SIDEBAR: What makes a commit 'unreachable'?
Recall that a commit only has a pointer to its parent commit (not its descendent commits).
A commit is considered reachable if you can get to it by starting at a branch, tag, or other ref and walking backward through its parent commits. This is the normal state for commits — they are part of the visible history of a branch or tag.
When no branch, tag, or ref points to a commit (directly or indirectly), it becomes unreachable. This often happens when you delete a branch or rewrite history (e.g., with reset or rebase), leaving some commits "orphaned" (or "dangling") without a ref pointing to them.
In the example below, C4
is unreachable (i.e., cannot be reached by starting at any of the three refs: v1.0 or master or ←HEAD), but the other three are all reachable.
Unreachable commits are not deleted immediately — Git keeps them for a while before cleaning them up. By default, Git retains unreachable commits for at least 30 days, during which they can still be recovered if you know their SHA. After that, they will be garbage-collected, and will be lost for good.
Preparation First, create the repo samplerepo-books-2
for this hands-on practical, by running the following commands in your terminal.
mkdir samplerepo-books-2
cd samplerepo-books-2
git init
echo "Horror Stories" >> horror.txt
git add .
git commit -m "Add horror.txt"
git switch -c textbooks
echo "Textbooks" >> textbooks.txt
git add .
git commit -m "Add textbooks.txt"
git switch master
git switch -c fantasy
echo "Fantasy Books" >> fantasy.txt
git add .
git commit -m "Add fantasy.txt"
git switch master
git merge --no-ff -m "Merge branch textbooks" textbooks
1 Delete the (the merged) textbooks
branch.
Use the git branch -d <branch>
command to delete a local branch 'safely' -- this command will fail if the branch has unmerged changes.
git branch -d textbooks
git log --oneline --decorate --graph --all # check the current revision graph
* 443132a (HEAD -> master) Merge branch textbooks
|\
| * 4969163 Add textbooks.txt
|/
| * 0586ee1 (fantasy) Add fantasy.txt
|/
* 7f28f0e Add horror.txt
Right-click on the branch name and choose Delete <branch>
:
In the next dialog, click OK
:
Observe that all commits remain. The only missing thing is the textbook
ref.
2 Make a copy of the SHA
of the tip of the (unmerged) fantasy
branch.
3 Delete the fantasy
branch.
Attempt to delete the branch. It should fail, as shown below:
git branch -d fantasy
error: the branch 'fantasy' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D fantasy'
As also hinted by the error message, you can replace the -d
with -D
to 'force' the deletion.
git branch -D fantasy
Now, check the revision graph:
git log --oneline --decorate --graph --all
* 443132a (HEAD -> master) Merge branch textbooks
|\
| * 4969163 Add textbooks.txt
|/
* 7f28f0e Add horror.txt
Attempt to delete the branch as you did before. It will fail because the branch has unmerged commits.
Try again but this time, tick the Force delete
option, which will force Git to delete the unmerged branch:
Observe how the branch ref fantasy
is gone, together with any unmerged commits on it.
4 Attempt to view the 'unreachable' commit whose SHA
you noted in step 2.
e.g., git show 32b34fb
(use the SHA
you copied earlier)
Observe how the commit still exists and still is reachable using the commit ID, although it is not reachable by other means, and not visible in the revision graph.
done!
EXERCISE: branch-delete
At this point: Now you can create, maintain, and merge multiple parallel branches in a local repo. This tour covered only the basic use of Git branches. More advanced usage will be covered in other tours.
What's next: Tour 7: Keeping Branches in Sync
Target Usage: To keep branches in a local repository synchronised with each other, as needed.
Motivation: While working on one branch, you might want to have access to changes introduced in another branch (e.g., to take advantage of a bug fix introduced in another branch).
Lesson plan:
T7L1. Merging to Sync Branches covers that part.
T7L2. Rebasing to Sync Branches covers that part.
T7L3. Copying Specific Commits covers that part.
Merging is one way to keep one branch synchronised with another.
When working in parallel branches, you’ll often need to sync (short for synchronise) one branch with another. For example, while developing a feature in one branch, you might want to bring in a recent bug fix from another branch that your branch doesn’t yet have.
The simplest way to sync branches is to merge — that is, to sync a branch b1
with changes from another branch b2
, you merge b2
into b1
. In fact, you can merge them periodically to keep one branch up to date with the other.
gitGraph %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch bug-fix branch feature commit id: "f1" checkout master checkout bug-fix commit id: "b1" checkout master merge bug-fix checkout feature merge master id: "mc1" commit id: "f2" checkout master commit id: "m2" checkout feature merge master id: "mc2" checkout master commit id: "m3" checkout feature commit id: "[feature] f3" checkout master commit id: "[HEAD → master] m4"
In the example above, you can see how the feature
branch is merging the master
branch periodically to keep itself in sync with the changes being introduced to the master
branch.
Rebasing is another way to synchronise one branch with another.
Rebasing is another way to synchronise one branch with another, while keeping the history cleaner and more linear. Instead of creating a merge commit to combine the branches, rebasing moves the entire sequence of commits from your branch and "replays" them on top of another branch. This effectively moves the base of your branch to the tip of the other branch (i.e., it 're-bases' it — hence the name), as if you had started your work from there in the first place.
Rebasing is especially useful when you want to update your branch with the latest changes from a main branch, but you prefer an uncluttered history with fewer merge commits.
Suppose we have the following revision graph, and we want to sync the feature
branch with master
, so that changes in commit m2
become visible to the feature
branch.
gitGraph %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch feature checkout feature commit id: "f1" checkout master commit id: "[master] m2" checkout feature commit id: "[HEAD → feature] f2"
If we merge the master
branch to the feature
branch as given below, m2
becomes visible to the feature
branch. However, it creates a merge commit.
gitGraph %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch feature checkout feature commit id: "f1" checkout master commit id: "[master] m2" checkout feature commit id: "f2" merge master id: "[HEAD → feature] mc1"
Instead of merging, if we rebased the feature
branch on the master
branch, we would get the following.
gitGraph %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" checkout master commit id: "[branch: master] m2" branch feature checkout feature commit id: "f1a" commit id: "[HEAD → feature] f2a"
Note how the rebasing changed the base of the feature
branch from m1
to m2
. As a result, changes done in m2
are now visible to the feature
branch. But there is no merge commit, and the revision graph is simpler.
Also note how the first commit in the feature branch, previously shown as f1
, is now shown as f1a
after the rebase. Although both commits contain the same changes, other details -- such as the parent commit -- are different, making them two distinct Git objects with different SHA values. Similarly, f2
and f2a
are also different. Thus, the history of the entire feature
branch has changed after the rebase.
Because rebasing rewrites the commit history of your branch, you should avoid rebasing branches that you’ve already published, and are potentially used by others -- rewriting published history can cause confusion and conflicts for those using the previous version of the commits.
Cherry-picking is a Git operation that copies over a specific commit from one branch to another.
Cherry-picking is another way to synchronise branches, by applying specific commits from one branch onto another.
Unlike merging or rebasing — which bring over all changes since the branches diverged — cherry-picking lets you choose individual commits and apply just those, one at a time, to your current branch. This is useful when you want to bring over a bug fix or a small feature from another branch without merging the entire branch history.
Because cherry-picking copies only the chosen commits, it creates new commits on your branch with the same changes but different SHA values.
Suppose we have the following revision graph, and we want to bring over the changes introduced in m3
(in the master
branch) onto the feature
branch.
gitGraph %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch feature checkout feature commit id: "f1" checkout master commit id: "m2" commit id: "m3" type: HIGHLIGHT commit id: "[master] m4" checkout feature commit id: "[HEAD → feature] f2"
After cherry-picking m3
onto the feature
branch, the revision graph should look like the following:
gitGraph %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch feature checkout feature commit id: "f1" checkout master commit id: "m2" commit id: "m3" type: HIGHLIGHT commit id: "[master] m4" checkout feature commit id: "f3" commit id: "[HEAD → feature] m3a" type: HIGHLIGHT
Note how it makes the changes done in m3
available (from now on) in the feature
branch, with minimal changes to the revision graph. Also note that the new commit m3a
contains the same changes as m3
, but it will be a different Git object with a different SHA value.
Cherry-picking is another Git operation that can result in conflicts i.e., if the changes in the cherry-picked commit conflict with the changes in the receiving branch.
At this point: You should now be able to bring changes from one branch to another in your local repository.
What's next: Tour 8: Working with Remote Branches
Target Usage: To synchronise branches in the local repo with a remote repo's branches.
Motivation: It is useful to be able to have another copy of branches in a remote repo.
Lesson plan:
T8L1. Pushing Branches to a Remote covers that part.
T8L2. Pulling Branches from a Remote covers that part.
T8L3. Deleting Branches from a Remote covers that part.
T8L4. Renaming Branches in a Remote covers that part.
Local branches can be replicated in a remote.
Pushing a copy of local branches to the corresponding remote repo makes those branches available remotely.
In a previous lesson, we saw how to push the default branch to a remote repository and have Git set up tracking between the local and remote branches using a remote-tracking reference. Pushing any other local branch to a remote works the same way as pushing the default branch — you simply specify the target branch instead of the default branch. Pushing any new commits in any local branch to a corresponding remote branch is done similarly as well.
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch bug-fix checkout master commit id: "[origin/master][HEAD → master] m2" checkout bug-fix commit id: "[bug-fix] b1" checkout master
[bug-fix
branch does not exist in the remote origin
]
→
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" branch bug-fix checkout master commit id: "[origin/master][HEAD → master] m2" checkout bug-fix commit id: "[origin/bug-fix][bug-fix] b1" checkout master
[after pushing bug-fix
branch to origin,
and setting up a remote-tracking branch]
Preparation Fork the samplerepo-company to your GitHub account. When doing so, un-tick the Copy the master branch only
option.
After forking, go to the fork and ensure both branches (master
, and track-sales
) are in there.
Clone the fork to your computer. It should look something like this:
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch track-sales checkout track-sales commit id: "[origin/track-sales] s1" checkout master commit id: "[origin/master][origin/HEAD][HEAD → master] m3"
The origin/HEAD
remote-tracking ref indicates where the HEAD
ref is in the remote origin
.
1 Create a new branch called hiring
, and add a commit to that branch. The commit can contain any changes you want.
Here are the commands you can run in the terminal to do this step in one shot:
git switch -c hiring
echo "Receptionist: Pam" >> employees.txt
git commit -am "Add Pam to employees.txt"
gitGraph BT: %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%% commit id: "m1" commit id: "m2" branch track-sales checkout track-sales commit id: "[origin/track-sales] s1" checkout master commit id: "[origin/master][origin/HEAD][master] m3" branch hiring checkout hiring commit id: "[HEAD → hiring] h1"
The resulting revision graph should look like the one above.
2 Push the hiring
branch to the remote.
You can use the usual git push <remote> -u <branch>
command to push the branch to the remote, and set up a remote-tracking branch at the same time.
git push origin -u hiring
3 Verify that the branch has been pushed to the remote by visiting the fork on GitHub, and looking for the origin/hiring
remote-tracking ref in the local repo.
done!
Branches in a remote can be replicated in the local repo, and maintained in sync with each other.
Sometimes we need to create a local copy of a branch from a remote repository, make further changes to it, and keep it synchronised with the remote branch. Let's explore how to handle this in a few common use cases:
Use case 1: Working with branches that already existed in the remote repo when you cloned it to your computer.
When you clone a repository,
Preparation Use the same samplerepo-company repo you used in Lesson T8L1. Pushing Branches to a Remote. Fork and clone it if you haven't done that already.
1 Verify that the remote-tracking branch origin/track-sales
exists in the local repo, but there is no local copy of it.
You can use the git branch -a
command to list all local and tracking branches.
git branch -a
* hiring
master
remotes/origin/HEAD -> origin/master
remotes/origin/hiring
remotes/origin/master
remotes/origin/track-sales
The *
in the output above indicates the currently active branch.
Note how there is no track-sales
in the list of branches (i.e., no local branch named track-sales
), but there is a remotes/origin/track-sales
(i.e., the remote-tracking branch)
Observe how the branch track-sales
appear under REMOTES
→ origin
but not under BRANCHES
.
2 Create a local copy of the remote branch origin/track-sales
.
You can use the git switch -c <branch> <remote-branch>
command for this e.g.,
git switch -c track-sales origin/track-sales
Locate the track-sales
remote-tracking branch (look under REMOTES
→ origin
), right-click, and choose Checkout...
.
In the next dialog, choose as follows:
The above command/action does several things:
track-sales
.origin/track-sales
, which means the local branch ref track-sales
will also move to where the origin/track-sales
is.3 Add a commit to the track-sales
branch and push to the remote, to verify that the local branch is tracking the remote branch.
Commands to perform this step in one shot:
echo "5 reams of paper" >> sales.txt
git commit -am "Update sales.txt"
git push origin track-sales
done!
Use case 2: Working with branches that were added to the remote repository after you cloned it e.g., a branch someone else pushed to the remote after you cloned.
Simply fetch to update your local repository with information about the new branch. After that, you can create a local copy of it and work with it just as you did in Use Case 1.
Fetching was covered in Lesson T3L3. Downloading Data Into a Local Repo.
Often, you'll need to delete a branch in a remote repo after it has served its purpose.
To delete a branch in a remote repository, you simply tell Git to remove the reference to that branch from the remote. This does not delete the branch from your local repository — it only removes it from the remote, so others won’t see it anymore. This is useful for cleaning up clutter in the remote repo e.g., delete old or merged branches that are no longer needed on the remote.
Preparation Fork the samplerepo-books to your GitHub account. When doing so, un-tick the Copy the master branch only
option.
After forking, go to the fork and ensure all three branches are in there.
Clone the fork to your computer.
1 Create a local copy of the fantasy
branch in your clone.
Follow instructions in Lesson T8L2. Pulling Branches from a Remote.
2 Delete the remote branch fantasy
.
You can use the git push <remote> --delete <branch>
command to delete a branch in a remote. This is like pushing changes in a branch to a remote, except we request the branch to be deleted instead, by adding the --delete
switch.
git push origin --delete fantasy
Locate the remote branch under REMOTES
→ origin
, right-click on the branch name, and choose Delete...
:
3 Verify that the branch was deleted from the remote, by going to the fork on GitHub and checking the branches page https://github.com/{YOUR_USERNAME}/samplerepo-books/branches
e.g., https://github.com/johndoe/samplerepo-books/branches
.
Also verify that the local copy has not been deleted.
4 Restore the remote branch from the local copy.
Push the local branch to the remote, while enabling the tracking option (as if pushing the branch to the remote for the first time), as covered in Lesson T8L1. Pushing Branches to a Remote.
In the above steps, we first created a local copy of the branch before deleting it in the remote repo. Doing so is optional. You can delete a remote branch without ever checking it out locally — you just need to know its name on the remote. Deleting the remote branch directly without creating a local copy is recommended if you simply want to clean up a remote branch you no longer need.
done!
Occasionally, you might need to rename a branch in a remote repo.
Git does not have a way to rename remote branches in place. Instead, you create a new branch with the desired name and delete the old one. This involves renaming your local branch to the new name, pushing it to the remote (which effectively creates a new remote branch), and then removing the old branch from the remote. This ensures the remote reflects the updated name while preserving the commit history and any work already done on the branch.
While Git cannot rename a remote branch in place, GitHub allows you to rename a branch in a remote repo. If you use this approach, the local repo still needs to be updated to reflect the change.
Preparation You can use the fork and the clone of the samplerepo-books that you created in Lesson T8L3. Deleting Branches from a Remote.
Target Rename the branch fantasy
in the remote (i.e., your fork) to fantasy-books
.
Steps
master
branch.origin/fantasy
.fantasy-books
.git switch master # ensure you are on the master branch
git switch -c fantasy origin/fantasy # create a local copy, tracking the remote branch
git branch -m fantasy fantasy-books # rename local branch
git push -u origin fantasy-books # push the new branch to remote, and set it to track
git push origin --delete fantasy # delete the old branch
You can run the git log --oneline --decorate --graph --all
to check the revision graph after each step. The final outcome should be something like the below:
* 355915c (HEAD -> fantasy-books, origin/fantasy-books) Add fantasy.txt
| * 027b2b0 (origin/master, origin/HEAD, master) Merge branch textbooks
|/|
| * a6ebaec (origin/textbooks) Add textbooks.txt
|/
* d462638 Add horror.txt
Perform the above steps (each step was covered in a previous lesson).
done!
At this point: You should now be able to work with branches in a remote repo, and keep them synchronised with branches in the local repo.
What's next: More trails to be added in the future.
Guidance for the item(s) below:
Let's learn how to create a pull request (PRs) on GitHub; you need to create one for your project this week.
A pull request (PR for short) is a mechanism for contributing code to a remote repo i.e., "I'm requesting you to pull my proposed changes to your repo". It's feature provided by RCS platforms such as GitHub. For this to work, the two repos must have a shared history. The most common case is sending PRs from a fork to its repo.
Suppose you want to propose some changes to a GitHub repo (e.g., samplerepo-pr-practice) as a pull request (PR).
samplerepo-pr-practice is an unmonitored repo you can use to practice working with PRs. Feel free to send PRs to it.
Given below is a scenario you can try in order to learn how to create PRs:
1. Fork the repo onto your GitHub account.
2. Clone it onto your computer.
3. Commit your changes e.g., add a new file with some contents and commit it.
master
branchadd-intro
(remember to switch to the master
branch before creating a new branch) and add your commit to it.4. Push the branch you updated (i.e., master
branch or the new branch) to your fork, as explained here.
5. Initiate the PR creation:
Go to your fork.
Click on the Pull requests tab followed by the New pull request button. This will bring you to the Compare changes
page.
Set the appropriate target repo and the branch that should receive your PR, using the base repository
and base
dropdowns. e.g.,
base repository: se-edu/samplerepo-pr-practice base: master
Normally, the default value shown in the dropdown is what you want but in case your fork has , the default may not be what you want.
Indicate which repo:branch contains your proposed code, using the head repository
and compare
dropdowns. e.g.,
head repository: myrepo/samplerepo-pr-practice compare: master
6. Verify the proposed code: Verify that the diff view in the page shows the exact change you intend to propose. If it doesn't, as necessary.
7. Submit the PR:
Click the Create pull request button.
Fill in the PR name and description e.g.,
Name: Add an introduction to the README.md
Description:
Add some paragraph to the README.md to explain ...
Also add a heading ...
If you want to indicate that the PR you are about to create is 'still work in progress, not yet ready', click on the dropdown arrow in the Create pull request button and choose Create draft pull request
option.
Click the Create pull request button to create the PR.
Go to the receiving repo to verify that your PR appears there in the Pull requests
tab.
The next step of the PR lifecycle is the PR review. The members of the repo that received your PR can now review your proposed changes.
You can update the PR along the way too. Suppose PR reviewers suggested a certain improvement to your proposed code. To update your PR as per the suggestion, you can simply modify the code in your local repo, commit the updated code to the same branch as before, and push to your fork as you did earlier. The PR will auto-update accordingly.
Sending PRs using the master
branch is less common than sending PRs using separate branches. For example, suppose you wanted to propose two bug fixes that are not related to each other. In that case, it is more appropriate to send two separate PRs so that each fix can be reviewed, refined, and merged independently. But if you send PRs using the master
branch only, both fixes (and any other change you do in the master
branch) will appear in the PRs you create from it.
To create another PR while the current PR is still under review, create a new branch (remember to switch back to the master
branch first), add your new proposed change in that branch, and create a new PR following the steps given above.
It is possible to create PRs within the same repo e.g., you can create a PR from branch feature-x
to the master
branch, within the same repo. Doing so will allow the code to be reviewed by other developers (using PR review mechanism) before it is merged.
Problem: merge conflicts in ongoing PRs, indicated by the message This branch has conflicts that must be resolved. That means the upstream repo's master
branch has been updated in a way that the PR code conflicts with that master
branch. Here is the standard way to fix this problem:
master
branch from the upstream repo to your local repo.git checkout master
git pull upstream master
master
branch (that you updated in the previous step) onto the PR branch, in order to bring over the new code in the master
branch to your PR branch.git checkout pr-branch # assuming pr-branch is the name of branch in the PR
git merge master
master
branch.
Resolve the conflict manually (this topic is covered elsewhere), and complete the merge.master
branch, the merge conflict alert in the PR will go away automatically. Guidance for the item(s) below:
As your project gets bigger and changes become more frequent, it's natural to look for ways to automate the many steps involved in going from the code you write in the editor to an executable product. This is a good time to start learning about that aspect too.
Build automation tools automate the steps of the build process, usually by means of build scripts.
In a non-trivial project, building a product from its source code can be a complex multistep process. For example, it can include steps such as: pull code from the revision control system, compile, link, run automated tests, automatically update release documents (e.g., build number), package into a distributable, push to repo, deploy to a server, delete temporary files created during building/testing, email developers of the new build, and so on. Furthermore, this build process can be done ‘on demand’, it can be scheduled (e.g., every day at midnight) or it can be triggered by various events (e.g., triggered by a code push to the revision control system).
Some of these build steps such as compiling, linking and packaging, are already automated in most modern IDEs. For example, several steps happen automatically when the ‘build’ button of the IDE is clicked. Some IDEs even allow customization of this build process to some extent.
However, most big projects use specialized build tools to automate complex build processes.
Some popular build tools relevant to Java developers: Gradle, Maven, Apache Ant, GNU Make
Some other build tools: Grunt (JavaScript), Rake (Ruby)
Some build tools also serve as dependency management tools. Modern software projects often depend on third party libraries that evolve constantly. That means developers need to download the correct version of the required libraries and update them regularly. Therefore, dependency management is an important part of build automation. Dependency management tools can automate that aspect of a project.
Maven and Gradle, in addition to managing the build process, can play the role of dependency management tools too.
An extreme application of build automation is called continuous integration (CI) in which integration, building, and testing happens automatically after each code change.
A natural extension of CI is Continuous Deployment (CD) where the changes are not only integrated continuously, but also deployed to end-users at the same time.
Some examples of CI/CD tools: Travis, Jenkins, Appveyor, CircleCI, GitHub Actions
Guidance for the item(s) below:
Next, we have a few more Java topics that you need as you move from a 'programming exercise' mode to a 'production code' mode.
JavaDoc is a tool for generating API documentation in HTML format from comments in the source code. In addition, modern IDEs use JavaDoc comments to generate explanatory tooltips.
An example method header comment in JavaDoc format:
/**
* Returns an Image object that can then be painted on the screen.
* The url argument must specify an absolute {@link URL}. The name
* argument is a specifier that is relative to the url argument.
* <p>
* This method always returns immediately, whether or not the
* image exists. When this applet attempts to draw the image on
* the screen, the data will be loaded. The graphics primitives
* that draw the image will incrementally paint on the screen.
*
* @param url An absolute URL giving the base location of the image.
* @param name The location of the image, relative to the url argument.
* @return The Image at the specified URL.
* @see Image
*/
public Image getImage(URL url, String name) {
try {
return getImage(new URL(url, name));
} catch (MalformedURLException e) {
return null;
}
}
Generated HTML documentation:
Tooltip generated by IntelliJ IDE:
In the absence of more extensive guidelines (e.g., given in a coding standard adopted by your project), you can follow the two examples below in your code.
A minimal JavaDoc comment example for methods:
/**
* Returns lateral location of the specified position.
* If the position is unset, NaN is returned.
*
* @param x X coordinate of position.
* @param y Y coordinate of position.
* @param zone Zone of position.
* @return Lateral location.
* @throws IllegalArgumentException If zone is <= 0.
*/
public double computeLocation(double x, double y, int zone)
throws IllegalArgumentException {
// ...
}
A minimal JavaDoc comment example for classes:
package ...
import ...
/**
* Represents a location in a 2D space. A <code>Point</code> object corresponds to
* a coordinate represented by two integers e.g., <code>3,6</code>
*/
public class Point {
// ...
}
You can use the java.io.File
class to represent a file object. It can be used to access properties of the file object.
This code creates a File
object to represent a file fruits.txt
that exists in the data
directory relative to the current working directory and uses that object to print some properties of the file.
import java.io.File;
public class FileClassDemo {
public static void main(String[] args) {
File f = new File("data/fruits.txt");
System.out.println("full path: " + f.getAbsolutePath());
System.out.println("file exists?: " + f.exists());
System.out.println("is Directory?: " + f.isDirectory());
}
}
full path: C:\sample-code\data\fruits.txt
file exists?: true
is Directory?: false
If you use backslash to specify the file path in a Windows computer, you need to use an additional backslash as an escape character because the backslash by itself has a special meaning. e.g., use "data\\fruits.txt"
, not "data\fruits.txt"
. Alternatively, you can use forward slash "data/fruits.txt"
(even on Windows).
You can read from a file using a Scanner
object that uses a File
object as the source of data.
This code uses a Scanner
object to read (and print) contents of a text file line-by-line:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class FileReadingDemo {
private static void printFileContents(String filePath) throws FileNotFoundException {
File f = new File(filePath); // create a File for the given file path
Scanner s = new Scanner(f); // create a Scanner using the File as the source
while (s.hasNext()) {
System.out.println(s.nextLine());
}
}
public static void main(String[] args) {
try {
printFileContents("data/fruits.txt");
} catch (FileNotFoundException e) {
System.out.println("File not found");
}
}
}
i.e., contents of the data/fruits.txt
5 Apples
3 Bananas
6 Cherries
You can use a java.io.FileWriter
object to write to a file.
The writeToFile
method below uses a FileWriter
object to write to a file. The method is being used to write two lines to the file temp/lines.txt
.
import java.io.FileWriter;
import java.io.IOException;
public class FileWritingDemo {
private static void writeToFile(String filePath, String textToAdd) throws IOException {
FileWriter fw = new FileWriter(filePath);
fw.write(textToAdd);
fw.close();
}
public static void main(String[] args) {
String file2 = "temp/lines.txt";
try {
writeToFile(file2, "first line" + System.lineSeparator() + "second line");
} catch (IOException e) {
System.out.println("Something went wrong: " + e.getMessage());
}
}
}
Contents of the temp/lines.txt
:
first line
second line
Note that you need to call the close()
method of the FileWriter
object for the writing operation to be completed.
You can create a FileWriter
object that appends to the file (instead of overwriting the current content) by specifying an additional boolean parameter to the constructor.
The method below appends to the file rather than overwrites.
private static void appendToFile(String filePath, String textToAppend) throws IOException {
FileWriter fw = new FileWriter(filePath, true); // create a FileWriter in append mode
fw.write(textToAppend);
fw.close();
}
The java.nio.file.Files
is a utility class that provides several useful file operations. It relies on the java.nio.file.Paths
file to generate Path
objects that represent file paths.
This example uses the Files
class to copy a file and delete a file.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class FilesClassDemo {
public static void main(String[] args) throws IOException{
Files.copy(Paths.get("data/fruits.txt"), Paths.get("temp/fruits2.txt"));
Files.delete(Paths.get("temp/fruits2.txt"));
}
}
The techniques above are good enough to manipulate simple text files. Note that it is also possible to perform file I/O operations using other classes.
You can organize your types (i.e., classes, interfaces, enumerations, etc.) into packages for easier management (among other benefits).
To create a package, you put a package statement at the very top of every source file in that package. The package statement must be the first line in the source file and there can be no more than one package statement in each source file. Furthermore, the package of a type should match the folder path of the source file. Similarly, the compiler will put the .class
files in a folder structure that matches the package names.
The Formatter
class below (in <source folder>/seedu/tojava/util/Formatter.java
file) is in the package seedu.tojava.util
. When it is compiled, the Formatter.class
file will be in the location <compiler output folder>/seedu/tojava/util
:
package seedu.tojava.util;
public class Formatter {
public static final String PREFIX = ">>";
public static String format(String s){
return PREFIX + s;
}
}
Package names are written in all lower case (not camelCase), using the dot as a separator. Packages in the Java language itself begin with java
. or javax
. Companies use their reversed Internet domain name to begin their package names.
For example, com.foobar.doohickey.util
can be the name of a package created by a company with a domain name foobar.com
To use a public from outside its package, you must do one of the following:
The Main
class below has two import statements:
import seedu.tojava.util.StringParser
: imports the class StringParser
in the seedu.tojava.util
packageimport seedu.tojava.frontend.*
: imports all the classes in the seedu.tojava.frontend
packagepackage seedu.tojava;
import seedu.tojava.util.StringParser;
import seedu.tojava.frontend.*;
public class Main {
public static void main(String[] args) {
// Using the fully qualified name to access the Processor class
String status = seedu.tojava.logic.Processor.getStatus();
// Using the StringParser previously imported
StringParser sp = new StringParser();
// Using classes from the tojava.frontend package
Ui ui = new Ui();
Message m = new Message();
}
}
Note how the class can still use the Processor
without importing it first, by using its fully qualified name seedu.tojava.logic.Processor
Importing a package does not import its sub-packages, as packages do not behave as hierarchies despite appearances.
import seedu.tojava.frontend.*
does not import the classes in the sub-package seedu.tojava.frontend.widget
.
If you do not use a package statement, your type doesn't have a package -- a practice not recommended (except for small code examples) as it is not possible for a type in a package to import a type that is not in a package.
Optionally, a static import can be used to import static members of a type so that the imported members can be used without specifying the type name.
The class below uses static imports to import the constant PREFIX
and the method format()
from the seedu.tojava.util.Formatter
class.
import static seedu.tojava.util.Formatter.PREFIX;
import static seedu.tojava.util.Formatter.format;
public class Main {
public static void main(String[] args) {
String formatted = format("Hello");
boolean isFormatted = formatted.startsWith(PREFIX);
System.out.println(formatted);
}
}
Formatter
class
Note how the class can use PREFIX
and format()
(instead of Formatter.PREFIX
and Formatter.format()
).
When using the command line to compile/run Java, you should take the package into account.
If the seedu.tojava.Main
class is defined in the file Main.java
,
<source folder>
, the command is:javac seedu/tojava/Main.java
<compiler output folder>
, the command is:java seedu.tojava.Main
Java applications are typically delivered as JAR (short for Java Archive) files. A JAR contains Java classes and other resources (icons, media files, etc.).
An executable JAR file can be launched using the java -jar
command e.g., java -jar foo.jar
launches the foo.jar
file.
The IDE or build tools such as Gradle can help you to package your application as a JAR file.
See the tutorial Working with JAR files @se-edu/guides to learn how to create and use JAR files.
Guidance for the item(s) below:
As you know, one of the objectives of the iP is to raise the quality of your code. We'll be learning about various ways to improve the code quality in the next few weeks, starting with coding standards.
Guidance for the item(s) below:
In-video quizzes
The Q+ icon indicates that the video has an in-video quiz. Submitting the in-video quiz can earn you bonus participation marks.
In-video quizzes are in only a small number of pre-recorded videos that are more important than the rest. They are a very light way to engage you with the video a bit more than just passively watching.
Please watch the video given below as it has some extra points not given in the text version.
Always code as if the person who ends up maintaining your code will be a violent psychopath who knows where you live. -- Martin Golding
Production code needs to be of high quality. Given how the world is becoming increasingly dependent on software, poor quality code is something no one can afford to tolerate.
One essential way to improve code quality is to follow a consistent style. That is why software engineers usually follow a strict coding standard (aka style guide).
The aim of a coding standard is to make the entire codebase look like it was written by one person. A coding standard is usually specific to a programming language and specifies guidelines such as the locations of opening and closing braces, indentation styles and naming styles (e.g., whether to use Hungarian style, Pascal casing, Camel casing, etc.). It is important that the whole team/company uses the same coding standard and that the standard is generally not inconsistent with typical industry practices. If a company's coding standard is very different from what is typically used in the industry, new recruits will take longer to get used to the company's coding style.
IDEs can help to enforce some parts of a coding standard e.g., indentation rules.
Guidance for the item(s) below:
As promised last week, let's learn some more sophisticated ways of testing.
Delaying testing until the full product is complete has a number of disadvantages:
Therefore, it is better to do early testing, as hinted by the popular rule of thumb given below, also illustrated by the graph below it.
The earlier a bug is found, the easier and cheaper to have it fixed.
Such early testing software is usually, and often by necessity, done by the developers themselves i.e., developer testing.
A test driver is the code that ‘drives’ the for the purpose of testing i.e., invoking the SUT with test inputs and verifying if the behavior is as expected.
PayrollTest
‘drives’ the Payroll
class by sending it test inputs and verifies if the output is as expected.
public class PayrollTest {
public static void main(String[] args) throws Exception {
// test setup
Payroll p = new Payroll();
// test case 1
p.setEmployees(new String[]{"E001", "E002"});
// automatically verify the response
if (p.totalSalary() != 6400) {
throw new Error("case 1 failed ");
}
// test case 2
p.setEmployees(new String[]{"E001"});
if (p.totalSalary() != 2300) {
throw new Error("case 2 failed ");
}
// more tests...
System.out.println("All tests passed");
}
}
JUnit is a tool for automated testing of Java programs. Similar tools are available for other languages and for automating different types of testing.
This is an automated test for a Payroll
class, written using JUnit libraries.
// other test methods
@Test
public void testTotalSalary() {
Payroll p = new Payroll();
// test case 1
p.setEmployees(new String[]{"E001", "E002"});
assertEquals(6400, p.totalSalary());
// test case 2
p.setEmployees(new String[]{"E001"});
assertEquals(2300, p.totalSalary());
// more tests...
}
Most modern IDEs have integrated support for testing tools. The figure below shows the JUnit output when running some JUnit tests using the Eclipse IDE.
Unit testing: testing individual units (methods, classes, subsystems, ...) to ensure each piece works correctly.
In OOP code, it is common to write one or more unit tests for each public method of a class.
Here are the code skeletons for a Foo
class containing two methods and a FooTest
class that contains unit tests for those two methods.
class Foo {
String read() {
// ...
}
void write(String input) {
// ...
}
}
class FooTest {
@Test
void read() {
// a unit test for Foo#read() method
}
@Test
void write_emptyInput_exceptionThrown() {
// a unit tests for Foo#write(String) method
}
@Test
void write_normalInput_writtenCorrectly() {
// another unit tests for Foo#write(String) method
}
}
import unittest
class Foo:
def read(self):
# ...
def write(self, input):
# ...
class FooTest(unittest.TestCase):
def test_read(self):
# a unit test for read() method
def test_write_emptyIntput_ignored(self):
# a unit test for write(string) method
def test_write_normalInput_writtenCorrectly(self):
# another unit test for write(string) method
When writing JUnit tests for a class Foo
, the common practice is to create a FooTest
class, which will contain various test methods for testing methods of the Foo
class.
Suppose we want to write tests for the IntPair
class below.
public class IntPair {
int first;
int second;
public IntPair(int first, int second) {
this.first = first;
this.second = second;
}
/**
* Returns The result of applying integer division first/second.
* @throws Exception if second is 0.
*/
public int intDivision() throws Exception {
if (second == 0){
throw new Exception("Divisor is zero");
}
return first/second;
}
@Override
public String toString() {
return first + "," + second;
}
}
Here's a IntPairTest
class to match (using JUnit 5).
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class IntPairTest {
@Test
public void intDivision_nonZeroDivisor_success() throws Exception {
// normal division results in an integer answer 2
assertEquals(2, new IntPair(4, 2).intDivision());
// normal division results in a decimal answer 1.9
assertEquals(1, new IntPair(19, 10).intDivision());
// dividend is zero but divisor is not
assertEquals(0, new IntPair(0, 5).intDivision());
}
@Test
public void intDivision_zeroDivisor_exceptionThrown() {
try {
assertEquals(0, new IntPair(1, 0).intDivision());
fail(); // the test should not reach this line
} catch (Exception e) {
assertEquals("Divisor is zero", e.getMessage());
}
}
@Test
public void testStringConversion() {
assertEquals("4,7", new IntPair(4, 7).toString());
}
}
@Test
annotation.assertEquals(expected, actual)
methods (provided by JUnit) to compare the expected output with the actual output. If they do not match, the test will fail.assertNull
, assertNotNull
, assertTrue
, assertFalse
etc. [more ...]testStringConversion
but when writing test methods, sometimes another convention is used:unitBeingTested_descriptionOfTestInputs_expectedOutcome
intDivision_zeroDivisor_exceptionThrown
catch
block. But if it is not thrown as expected, the test will reach fail()
line and will fail as a result.What to test for when writing tests? While test case design techniques is a separate topic altogether, it should be noted that the goal of these tests is to catch bugs in the code. Therefore, test using inputs that can trigger a potentially buggy path in the code. Another way to approach this is, to write tests such that if a future developer modified the method to unintentionally introduce a bug into it, at least one of the test should fail (thus alerting that developer to the mistake immediately).
In the example above, the IntPairTest
class tests the IntPair#intDivision(int, int)
method using several inputs, some even seemingly attempting to 'trick' the method into producing a wrong result. If the method still produces the correct output for such 'tricky' inputs (as well as 'normal' outputs), we can have a higher confidence on the method being correctly implemented.
However, also note that the current test cases do not (but probably should) test for the inputs (0, 0
), to confirm that it throws the expected exception.
A proper unit test requires the unit to be tested in isolation so that bugs in the cannot influence the test i.e., bugs outside of the unit should not affect the unit tests.
If a Logic
class depends on a Storage
class, unit testing the Logic
class requires isolating the Logic
class from the Storage
class.
Stubs can isolate the from its dependencies.
Stub: A stub has the same interface as the component it replaces, but its implementation is so simple that it is unlikely to have any bugs. It mimics the responses of the component, but only for a limited set of predetermined inputs. That is, it does not know how to respond to any other inputs. Typically, these mimicked responses are hard-coded in the stub rather than computed or retrieved from elsewhere, e.g., from a database.
Consider the code below:
class Logic {
Storage s;
Logic(Storage s) {
this.s = s;
}
String getName(int index) {
return "Name: " + s.getName(index);
}
}
interface Storage {
String getName(int index);
}
class DatabaseStorage implements Storage {
@Override
public String getName(int index) {
return readValueFromDatabase(index);
}
private String readValueFromDatabase(int index) {
// retrieve name from the database
}
}
Normally, you would use the Logic
class as follows (note how the Logic
object depends on a DatabaseStorage
object to perform the getName()
operation):
Logic logic = new Logic(new DatabaseStorage());
String name = logic.getName(23);
You can test it like this:
@Test
void getName() {
Logic logic = new Logic(new DatabaseStorage());
assertEquals("Name: John", logic.getName(5));
}
However, this logic
object being tested is making use of a DataBaseStorage
object which means a bug in the DatabaseStorage
class can affect the test. Therefore, this test is not testing Logic
in isolation from its dependencies and hence it is not a pure unit test.
Here is a stub class you can use in place of DatabaseStorage
:
class StorageStub implements Storage {
@Override
public String getName(int index) {
if (index == 5) {
return "Adam";
} else {
throw new UnsupportedOperationException();
}
}
}
Note how the StorageStub
has the same interface as DatabaseStorage
, but is so simple that it is unlikely to contain bugs, and is pre-configured to respond with a hard-coded response, presumably, the correct response DatabaseStorage
is expected to return for the given test input.
Here is how you can use the stub to write a unit test. This test is not affected by any bugs in the DatabaseStorage
class and hence is a pure unit test.
@Test
void getName() {
Logic logic = new Logic(new StorageStub());
assertEquals("Name: Adam", logic.getName(5));
}
In addition to Stubs, there are other type of replacements you can use during testing, e.g., Mocks, Fakes, Dummies, Spies.
Guidance for the item(s) below:
While the JUnit concepts mentioned in the topic below are not strictly needed for the course projects, it is good to be aware of them so that you try some of them when applicable.