Detailed Table of Contents
Guidance for the item(s) below:
As you know, OOP is a core part of this course. Let's start learning the OOP paradigm this week.
Object-Oriented Programming (OOP) is a programming paradigm. A programming paradigm guides programmers to analyze programming problems, and structure programming solutions, in a specific way.
Programming languages have traditionally divided the world into two parts—data and operations on data. Data is static and immutable, except as the operations may change it. The procedures and functions that operate on data have no lasting state of their own; they’re useful only in their ability to affect data.
This division is, of course, grounded in the way computers work, so it’s not one that you can easily ignore or push aside. Like the equally pervasive distinctions between matter and energy and between nouns and verbs, it forms the background against which you work. At some point, all programmers—even object-oriented programmers—must lay out the data structures that their programs will use and define the functions that will act on the data.
With a procedural programming language like C, that’s about all there is to it. The language may offer various kinds of support for organizing data and functions, but it won’t divide the world any differently. Functions and data structures are the basic elements of design.
Object-oriented programming doesn’t so much dispute this view of the world as restructure it at a higher level. It groups operations and data into modular units called objects and lets you combine objects into structured networks to form a complete program. In an object-oriented programming language, objects and object interactions are the basic elements of design.
Some other examples of programming paradigms are:
| Paradigm | Programming Languages |
|---|---|
| Procedural Programming paradigm | C |
| Functional Programming paradigm | F#, Haskell, Scala |
| Logic Programming paradigm | Prolog |
Some programming languages support multiple paradigms.
Java is primarily an OOP language but it supports limited forms of functional programming and it can be used to (although not recommended to) write procedural code. e.g., se-edu/addressbook-level1
JavaScript and Python support functional, procedural, and OOP programming.
An object in Object-Oriented Programming (OOP) has state and behavior, similar to objects in the real world.
Every object has both state (data) and behavior (operations on data). In that, they’re not much different from ordinary physical objects. It’s easy to see how a mechanical device, such as a pocket watch or a piano, embodies both state and behavior. But almost anything that’s designed to do a job does, too. Even simple things with no moving parts such as an ordinary bottle combine state (how full the bottle is, whether or not it’s open, how warm its contents are) with behavior (the ability to dispense its contents at various flow rates, to be opened or closed, to withstand high or low temperatures).
It’s this resemblance to real things that gives objects much of their power and appeal. They can not only model components of real systems, but equally as well fulfill assigned roles as components in software systems.
OOP views the world as a network of interacting objects.
A real world scenario viewed as a network of interacting objects:
You are asked to find out the average age of a group of people Adam, Beth, Charlie, and Daisy. You take a piece of paper and pen, go to each person, ask for their age, and note it down. After collecting the age of all four, you enter it into a calculator to find the total. And then, use the same calculator to divide the total by four, to get the average age. This can be viewed as the objects You, Pen, Paper, Calculator, Adam, Beth, Charlie, and Daisy interacting to accomplish the end result of calculating the average age of the four persons. These objects can be considered as connected in a certain network of certain structure that dictates how these objects can interact. For example, You object is connected to the Pen object, and hence You can use the Pen object to write.
OOP solutions try to create a similar object network inside the computer’s memory – a sort of virtual simulation of the corresponding real world scenario – so that a similar result can be achieved programmatically.
OOP does not demand that the virtual world object network follow the real world exactly.
Our previous example can be tweaked a bit as follows:
Main to represent your role in the scenario.Pen and Paper with an object called AgeList that is able to keep a list of ages.Every object has both state (data) and behavior (operations on data).
The state and behavior of our running example are as follows:
| Object | Real World? | Virtual World? | Example of State (i.e., Data) | Examples of Behavior (i.e., Operations) |
|---|---|---|---|---|
| Adam | Name, Date of Birth | Calculate age based on birthday | ||
| Pen | - | Ink color, Amount of ink remaining | Write | |
| AgeList | - | Recorded ages | Give the number of entries, Accept an entry to record | |
| Calculator | Numbers already entered | Calculate the sum, divide | ||
| You/Main | Average age, Sum of ages | Use other objects to calculate |
Every object has an interface and an implementation.
Every real world object has,
The interface and implementation of some real-world objects in our example:
Similarly, every object in the virtual world has an interface and an implementation.
The interface and implementation of some virtual-world objects in our example:
Adam: the interface might have a method getAge(Date asAt); the implementation of that method is not visible to other objects.Objects interact by sending messages. Both real world and virtual world object interactions can be viewed as objects sending messages to each other. The message can result in the sender object receiving a response and/or the receiver object’s state being changed. Furthermore, the result can vary based on which object received the message, even if the message is identical (see rows 1 and 2 in the example below).
Same messages and responses from our running example:
| World | Sender | Receiver | Message | Response | State Change |
|---|---|---|---|---|---|
| Real | You | Adam | "What is your name?" | "Adam" | - |
| Real | as above | Beth | as above | "Beth" | - |
| Real | You | Pen | Put nib on paper and apply pressure | Makes a mark on your paper | Ink level goes down |
| Virtual | Main | Calculator (current total is 50) | add(int i): int i = 23 | 73 | total = total + 23 |
Guidance for the item(s) below:
OOP is built upon four core concepts. The firs two are:
They are explained in the sections below.
The concept of Objects in OOP is an abstraction mechanism because it allows us to abstract away the lower level details and work with bigger granularity entities i.e., ignore details of data formats and the method implementation details and work at the level of objects.
You can deal with a Person object that represents the person Adam and query the object for Adam's age instead of dealing with details such as Adam’s date of birth (DoB), in what format the DoB is stored, the algorithm used to calculate the age from the DoB, etc.
Encapsulation protects an implementation from unintended actions and from inadvertent access.
-- Object-Oriented Programming with Objective-C, Apple
An object is an encapsulation of some data and related behavior in terms of two aspects:
1. The packaging aspect: An object packages data and related behavior together into one self-contained unit.
2. The information hiding aspect: The data in an object is hidden from the outside world and are only accessible using the object's interface.
Writing an OOP program is essentially writing instructions that the computer will use to,
A class contains instructions for creating a specific kind of objects. It turns out sometimes multiple objects keep the same type of data and have the same behavior because they are of the same kind. Instructions for creating a 'kind' (or ‘class’) of objects can be done once and those same instructions can be used to objects of that kind. We call such instructions a Class.
Classes and objects in an example scenario
Consider the example of writing an OOP program to calculate the average age of Adam, Beth, Charlie, and Daisy.
Instructions for creating objects Adam, Beth, Charlie, and Daisy will be very similar because they are all of the same kind: they all represent ‘persons’ with the same interface, the same kind of data (i.e., name, dateOfBirth, etc.), and the same kind of behavior (i.e., getAge(Date), getName(), etc.). Therefore, you can have a class called Person containing instructions on how to create Person objects and use that class to instantiate objects Adam, Beth, Charlie, and Daisy.
Similarly, you need classes AgeList, Calculator, and Main classes to instantiate one each of AgeList, Calculator, and Main objects.
| Class | Objects |
|---|---|
Person | objects representing Adam, Beth, Charlie, Daisy |
AgeList | an object to represent the age list |
Calculator | an object to do the calculations |
Main | an object to represent you (i.e., the one who manages the whole operation) |
Guidance for the item(s) below:
Now that you know what objects are, let's see how they are used in Java, which happens to be an OOP language.
Java is an "object-oriented" language, which means that it uses objects to represent data and provide methods related to them. Object types are called classes e.g., you can use String objects in Java and those objects belong to the String class.
Java comes with many inbuilt classes which are organized into packages. Here are some examples:
| package | Some example classes in the package |
|---|---|
java.lang | String, Math, System |
Before using a class in your code, you need to import the class. import statements appear at the top of the code.
This example imports the java.awt.Point class (i.e., the Point class in the java.awt package) -- which can be used to represent the coordinates of a location in a Cartesian plane -- and use it in the main method.
import java.awt.Point;
public class Main{
public static void main(String[] args) {
Point spot = new Point(3, 4);
int x = spot.x;
System.out.println(x);
}
}
You might wonder why we can use the System class without importing it. System belongs to the java.lang package, which is imported automatically.
new operatorTo create a new object, you have to use the new operator
This line shows how to create a new Point object using the new operator:
Point spot = new Point(3, 4);
Update the code below to create a new Rectangle object as described in the code comments, to produce the given output.
Rectangle class is found in the java.awt package.Rectangle objects are (int x, int y, int width, int height).public class Main {
public static void main(String[] args) {
Rectangle r;
// TODO: create a Rectangle object that has the
// properties x=0, y=0, width=5, height=10
// and assign it to r
System.out.println(r);
}
}
java.awt.Rectangle[x=0,y=0,width=5,height=10]
Hint
Variables that belong to an object are called attributes (or fields).
To access an attribute of an object, Java uses dot notation.
The code below uses spot.x which means "go to the object spot refers to, and get the value of the attribute x."
Point spot = new Point(3, 4);
int sum = spot.x * spot.x + spot.y * spot.y;
System.out.println(spot.x + ", " + spot.y + ", " + sum);
3, 4, 25
You can an object by assigning a different value to its attributes.
This example changes the x value of the Point object to 5.
Point spot = new Point(3, 4);
spot.x = 5;
System.out.println(spot.x + ", " + spot.y);
5, 4
Java uses the dot notation to invoke methods on an object too.
This example invokes the translate method on a Point object so that it moves to a different location.
Point spot = new Point(3, 4);
System.out.println(spot.x + ", " + spot.y);
spot.translate(5,5);
System.out.println(spot.x + ", " + spot.y);
3, 4
8, 9
Update the code below as described in code comments, to produce the given output.
import java.awt.Rectangle;
public class Main {
public static void main(String[] args) {
Rectangle r = new Rectangle(0, 0, 4, 6);
System.out.println(r);
int area;
//TODO: add a line below to calculate the area using
// width and height properties of r
// and assign it to the variable area
System.out.println("Area: " + area);
//TODO: add a line here to set the size of r to
// 8x10 (width x height)
//Recommended: use the setSize(int width, int height)
// method of the Rectangle object
System.out.println(r);
}
}
java.awt.Rectangle[x=0,y=0,width=4,height=6]
Area: 24
java.awt.Rectangle[x=0,y=0,width=8,height=10]
Hint
You can pass objects as parameters to a method in the usual way.
The printPoint method below takes a Point object as an argument and displays its attributes in (x,y) format.
public static void printPoint(Point p) {
System.out.println("(" + p.x + ", " + p.y + ")");
}
public static void main(String[] args) {
Point spot = new Point(3, 4);
printPoint(spot);
}
(3, 4)
You can return an object from a method too.
The java.awt package also provides a class called Rectangle. Rectangle objects are similar to points, but they have four attributes: x, y, width, and height. The findCenter method below takes a Rectangle as an argument and returns a Point that corresponds to the center of the rectangle:
public static Point findCenter(Rectangle box) {
int x = box.x + box.width / 2;
int y = box.y + box.height / 2;
return new Point(x, y);
}
The return type of this method is Point. The last line creates a new Point object and returns a reference to it.
null and NullPointerExceptionnull is a special value that means "no object". You can assign null to a variable to indicate that the variable is 'empty' at the moment. However, if you try to use a null value, either by accessing an attribute or invoking a method, Java throws a NullPointerException.
In this example, the variable spot is assigned a null value. As a result, trying to access spot.x attribute or invoking the spot.translate method results in a NullPointerException.
Point spot = null;
int x = spot.x; // NullPointerException
spot.translate(50, 50); // NullPointerException
On the other hand, it is legal to return null from a method or to pass a null reference as an argument to a method.
Returning null from a method.
public static Point createCopy(Point p) {
if (p == null) {
return null; // return null if p is null
}
// create a new object with same x,y values
return new Point(p.x, p.y);
}
Passing null as the argument.
Point result = createCopy(null);
System.out.println(result);
null
It is possible to have multiple variables that refer to the same object.
Notice how p1 and p2 are aliases for the same object. When the object is changed using the variable p1, the changes are visible via p2 as well (and vice versa), because they both point to the same Point object.
Point p1 = new Point(0,0);
Point p2 = p1;
System.out.println("p1: " + p1.x + ", " + p1.y);
System.out.println("p2: " + p2.x + ", " + p2.y);
p1.x = 1;
p2.y = 2;
System.out.println("p1: " + p1.x + ", " + p1.y);
System.out.println("p2: " + p2.x + ", " + p2.y);
p1: 0, 0
p2: 0, 0
p1: 1, 2
p2: 1, 2
Java does not have explicit pointers (and other related things such as pointer de-referencing and pointer arithmetic). When an object is passed into a method as an argument, the method gains access to the original object. If the method changes the object it received, the changes are retained in the object even after the method has completed.
Note how p3 retains changes done to it by the method swapCoordinates even after the method has completed executing.
public static void swapCoordinates(Point p){
int temp = p.x;
p.x = p.y;
p.y = temp;
}
public static void main(String[] args) {
Point p3 = new Point(2,3);
System.out.println("p3: " + p3.x + ", " + p3.y);
swapCoordinates(p3);
System.out.println("p3: " + p3.x + ", " + p3.y);
}
p3: 2, 3
p3: 3, 2
Add a method move(Point p, Rectangle r) to the code below, to produce the given output. The behavior of the method is as follows:
null and does nothing if either p or r is nullPoint object that has attributes x and y that match those of rpr so that its attributes x and y match those of pimport java.awt.Point;
import java.awt.Rectangle;
public class Main {
//TODO add your method here
public static void main(String[] args) {
Point p1 = new Point(0, 0);
Rectangle r1 = new Rectangle(2, 3, 5, 6);
System.out.println("arguments: " + p1 + ", " + r1);
Point p2 = move(p1, r1);
System.out.println(
"argument point after method call: " + p1);
System.out.println(
"argument rectangle after method call: " + r1);
System.out.println(
"returned point: " + p2);
System.out.println(move(null, null));
}
}
arguments: java.awt.Point[x=0,y=0], java.awt.Rectangle[x=2,y=3,width=5,height=6]
argument point after method call: java.awt.Point[x=0,y=0]
argument rectangle after method call: java.awt.Rectangle[x=0,y=0,width=5,height=6]
returned point: java.awt.Point[x=2,y=3]
null
Hint
What happens when no variables refer to an object?
Point spot = new Point(3, 4);
spot = null;
The first line creates a new Point object and makes spot refer to it. The second line changes spot so that instead of referring to the object, it refers to nothing. If there are no references to an object, there is no way to access its attributes or invoke a method on it. From the programmer’s view, it ceases to exist. However, it’s still present in the computer’s memory, taking up space.
In Java, you don’t have to delete objects you create when they are no longer needed. As your program runs, the system automatically looks for stranded objects and reclaims them; then the space can be reused for new objects. This process is called garbage collection. You don’t have to do anything to make garbage collection happen, and in general don’t have to be aware of it. But in high-performance applications, you may notice a slight delay every now and then when Java reclaims space from discarded objects.
Guidance for the item(s) below:
Having seen how to use objects in Java, the next step is learn how to define new kinds of objects (aka classes) in Java.
As you know,
new operator instantiates objects, that is, it creates new instances of a class. Here's a class called Time, intended to represent a moment in time. It has three attributes and no methods.
public class Time {
private int hour;
private int minute;
private int second;
}
You can give a class any name you like. The Java convention is to use format for class names.
The code is placed in a file whose name matches the class e.g., the Time class should be in a file named Time.java.
When a class is public (e.g., the Time class in the above example) it can be used in other classes. But the that are private (e.g., the hour, minute and second attributes of the Time class) can only be accessed from inside the Time class.
The syntax for is similar to that of other methods, except:
static is omitted.When you invoke new, Java creates the object and calls your constructor to initialize the instance variables. When the constructor is done, it returns a reference to the new object.
Here is an example constructor for the Time class:
public Time() {
hour = 0;
minute = 0;
second = 0;
}
This constructor does not take any arguments. Each line initializes an instance variable to 0 (which in this example means midnight).
Now you can create Time objects.
Time time = new Time();
Like other methods, constructors can be .
You can add another constructor to the Time class to allow creating Time objects that are initialized to a specific time:
public Time(int h, int m, int s) {
hour = h;
minute = m;
second = s;
}
Here's how you can invoke the new constructor:
Time justBeforeMidnight = new Time(11, 59, 59);
this keywordThe this keyword is a reference variable in Java that refers to the . You can use this the same way you use the name of any other object. For example, you can read and write the instance variables of this, and you can pass this as an argument to other methods. But you do not declare this, and you can’t make an assignment to it.
In the following version of the constructor, the names and types of the parameters are the same as the instance variables (parameters don’t have to use the same names, but that’s a common style). As a result, the parameters shadow (or hide) the instance variables, so the keyword this is necessary to tell them apart.
public Time(int hour, int minute, int second) {
this.hour = hour;
this.minute = minute;
this.second = second;
}
this can be used to refer to a constructor of a class within the same class too.
In this example the constructor Time() uses the this keyword to call its own constructor Time(int, int, int)
public Time() {
this(0, 0, 0); // call the overloaded constructor
}
public Time(int hour, int minute, int second) {
// ...
}
You can add methods to a class which can then be used from the objects of that class. These instance methods do not have the static keyword in their method signature. Instance methods can access attributes of the class.
Here's how you can add a method to the Time class to get the number of seconds passed since midnight.
public int secondsSinceMidnight() {
return hour*60*60 + minute*60 + second;
}
Here's how you can use that method.
Time t = new Time(0, 2, 5);
System.out.println(t.secondsSinceMidnight() + " seconds since midnight!");
Define a Circle class so that the code given below produces the given output. The nature of the class is as follows:
private):
int x, int y: represents the location of the circledouble radius: the radius of the circleCircle(): initializes x, y, radius to 0Circle(int x, int y, double radius): initializes the attributes to the given valuesgetArea(): intint value (not double). Calculated as Pi * (radius)2double to an int using (int) e.g., x = (int)2.25 gives x the value 2.Math.PI to get the value of PiMath.pow() to raise a number to a specific power e.g., Math.pow(3, 2) calculates 32public class Main {
public static void main(String[] args) {
Circle c = new Circle();
System.out.println(c.getArea());
c = new Circle(1, 2, 5);
System.out.println(c.getArea());
}
}
0
78
Hint
As the instance variables of Time are private, you can access them from within the Time class only. To compensate, you can provide methods to access attributes:
public int getHour() {
return hour;
}
public int getMinute() {
return minute;
}
public int getSecond() {
return second;
}
Methods like these are formally called “accessors”, but more commonly referred to as getters. By convention, the method that gets a variable named something is called getSomething.
Similarly, you can provide setter methods to modify attributes of a Time object:
public void setHour(int hour) {
this.hour = hour;
}
public void setMinute(int minute) {
this.minute = minute;
}
public void setSecond(int second) {
this.second = second;
}
Consider the Circle class below:
public class Circle {
private int x;
private int y;
private double radius;
public Circle(){
this(0, 0, 0);
}
public Circle(int x, int y, double radius){
this.x = x;
this.y = y;
this.radius = radius;
}
public int getArea(){
double area = Math.PI * Math.pow(radius, 2);
return (int)area;
}
}
Update it as follows so that code given below produces the given output.
public class Main {
public static void main(String[] args) {
Circle c = new Circle(1,2, 5);
c.setX(4);
c.setY(5);
c.setRadius(6);
System.out.println("x : " + c.getX());
System.out.println("y : " + c.getY());
System.out.println("radius : " + c.getRadius());
System.out.println("area : " + c.getArea());
c.setRadius(-5);
System.out.println("radius : " + c.getRadius());
c = new Circle(1, 1, -4);
System.out.println("radius : " + c.getRadius());
}
}
x : 4
y : 5
radius : 6.0
area : 113
radius : 0.0
radius : 0.0
Hint
Guidance for the item(s) below:
Now that we know about classes, let's learn what class-level members are and how to use them in Java.
While all objects of a class have the same attributes, each object has its own copy of the attribute value.
All Person objects have the name attribute but the value of that attribute varies between Person objects.
However, some attributes are not suitable to be maintained by individual objects. Instead, they should be maintained centrally, shared by all objects of the class. They are like ‘global variables’ but attached to a specific class. Such variables whose value is shared by all instances of a class are called class-level attributes.
The attribute totalPersons should be maintained centrally and shared by all Person objects rather than copied at each Person object.
Similarly, when a normal method is being called, a message is being sent to the receiving object and the result may depend on the receiving object.
Sending the getName() message to the Adam object results in the response "Adam" while sending the same message to the Beth object results in the response "Beth".
However, there can be methods related to a specific class but not suitable for sending messages to a specific object of that class. Such methods that are called using the class instead of a specific instance are called class-level methods.
The method getTotalPersons() is not suitable to send to a specific Person object because a specific object of the Person class should not have to know about the total number of Person objects.
Class-level attributes and methods are collectively called class-level members (also called static members sometimes because some programming languages use the keyword static to identify class-level members). They are to be accessed using the class name rather than an instance of the class.
The content below is an extract from -- Java Tutorial, with slight adaptations.
When a number of objects are created from the same class blueprint, they each have their own distinct copies of instance variables. In the case of a Bicycle class, the instance variables are gear, and speed. Each Bicycle object has its own values for these variables, stored in different memory locations.
Sometimes, you want to have variables that are common to all objects. This is accomplished with the static modifier. Fields that have the static modifier in their declaration are called static fields or class variables. They are associated with the class, rather than with any object. Every instance of the class shares a class variable, which is in one fixed location in memory. Any object can change the value of a class variable, but class variables can also be manipulated without creating an instance of the class.
Suppose you want to create a number of Bicycle objects and assign each a serial number, beginning with 1 for the first object. This ID number is unique to each object and is therefore an instance variable. At the same time, you need a field to keep track of how many Bicycle objects have been created so that you know what ID to assign to the next one. Such a field is not related to any individual object, but to the class as a whole. For this you need a class variable, numberOfBicycles, as follows:
public class Bicycle {
private int gear;
private int speed;
// an instance variable for the object ID
private int id;
// a class variable for the number of Bicycle
// objects instantiated
private static int numberOfBicycles = 0;
...
}
Class variables are referenced by the class name itself, as in Bicycle.numberOfBicycles This makes it clear that they are class variables.
The Java programming language supports static methods as well as static variables. Static methods, which have the static modifier in their declarations, should be invoked with the class name, without the need for creating an instance of the class, as in ClassName.methodName(args)
The static modifier, in combination with the final modifier, is also used to define constants. The final modifier indicates that the value of this field cannot change. For example, the following variable declaration defines a constant named PI, whose value is an approximation of pi (the ratio of the circumference of a circle to its diameter):
static final double PI = 3.141592653589793;
Here is an example with class-level variables and class-level methods:
public class Bicycle {
private int gear;
private int speed;
private int id;
private static int numberOfBicycles = 0;
public Bicycle(int startSpeed, int startGear) {
gear = startGear;
speed = startSpeed;
numberOfBicycles++;
id = numberOfBicycles;
}
public int getID() {
return id;
}
public static int getNumberOfBicycles() {
return numberOfBicycles;
}
public int getGear(){
return gear;
}
public void setGear(int newValue) {
gear = newValue;
}
public int getSpeed() {
return speed;
}
// ...
}
Explanation of System.out.println(...):
out is a class-level public attribute of the System class.println is an instance level method of the out object.Consider the Circle class below:
public class Circle {
private int x;
private int y;
private double radius;
public Circle(){
this(0, 0, 0);
}
public Circle(int x, int y, double radius){
setX(x);
setY(y);
setRadius(radius);
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = Math.max(radius, 0);
}
//TODO: Add your getMaxRadius() method here
}
Update it as follows so that code given below produces the given output.
maxRadius variable to store the maximum radius value of the Circle objects in existence thus far.getMaxRadius() method that uses the above-mentioned variable to return the maximum radius value of the Circle objects in existence thus far.setRadius method to ensure maxRadius value is updated (if needed) when the radius of an existing Circle object is changed.public class Main {
public static void main(String[] args) {
Circle c = new Circle();
System.out.println("max radius used so far : " + Circle.getMaxRadius());
c = new Circle(0, 0, 10);
System.out.println("max radius used so far : " + Circle.getMaxRadius());
c = new Circle(0, 0, -15);
System.out.println("max radius used so far : " + Circle.getMaxRadius());
c.setRadius(12);
System.out.println("max radius used so far : " + Circle.getMaxRadius());
}
}
max radius used so far : 0.0
max radius used so far : 10.0
max radius used so far : 10.0
max radius used so far : 12.0
Hint
Guidance for the item(s) below:
We've packaged our Git (and GitHub) lessons as 'Learning Trail' which is divided into several 'tours'. Each tour consists of a series of lessons and covers the Git knowledge required for a specific usage.
Git Learning Tour
This week, we start with first three tours, to get started with Git and GitHub.
Target Usage: To use Git to systematically record the history of a folder in your own computer. More specifically, to use Git to save a snapshot of the folder at specific points of time.
Motivation: Recording the history of files in a folder (e.g, code files of a software project, case notes, files related to an article/book that you are authoring) can be useful in case you need to refer to past versions.
Lesson plan:
T1L1. Introduction to Revision Control covers that part.
T1L2. Preparing to Use Git covers that part.
T1L3. Putting a Folder Under Git's Control covers that part.
T1L4. Specifying What to Include in a Snapshot covers that part.
T1L5. Saving a Snapshot covers that part.
T1L6. Examining the Revision History covers that part.
Before learning about Git, let us first understand what revision control is.
Given below is a general introduction to revision control, adapted from bryan-mercurial-guide:
Revision control is the process of managing multiple versions of a piece of information. In its simplest form, this is something that many people do by hand: every time you modify a file, save it under a new name that contains a number, each one higher than the number of the preceding version.
Manually managing multiple versions of even a single file is an error-prone task, though, so software tools to help automate this process have long been available. The earliest automated revision control tools were intended to help a single user to manage revisions of a single file. Over the past few decades, the scope of revision control tools has expanded greatly; they now manage multiple files, and help multiple people to work together. The best modern revision control tools have no problem coping with thousands of people working together on projects that consist of hundreds of thousands of files.
There are a number of reasons why you or your team might want to use an automated revision control tool for a project.
Most of these reasons are equally valid, at least in theory, whether you're working on a project by yourself, or with a hundred other people.
A revision is the state of a piece of information at a specific point in time, resulting from changes made to it e.g., if you modify the code and save the file, you have a new revision (or a new version) of that file. Some seem to use this term interchangeably with version while others seem to distinguish the two -- here, let us treat them as the same, for simplicity.
Revision Control Software (RCS) are the software tools that automate the process of Revision Control i.e., managing revisions of software . RCS are also known as Version Control Software (VCS), and by a few other names.
Git is the most widely used RCS today. Other RCS tools include Mercurial, Subversion (SVN), Perforce, CVS (Concurrent Versions System), Bazaar, TFS (Team Foundation Server), and Clearcase.
Github is a web-based project hosting platform for projects using Git for revision control. Other similar services include GitLab, BitBucket, and SourceForge.
Before you start learning Git, you need to install some tools on your computer.
Git is a free and open source software used for revision control. To use Git, you need to install Git on your computer.
PREPARATION: Install Git
Download the Git installer from the official Git website.
Run the installer and make sure to select the option to install Git Bash when prompted.
Screenshots given below provide some guidance on the dialogs you might encounter when installing Git. In other cases, go with the default option.




When running Git commands, we recommend Windows users to use the Git Bash terminal that comes with Git. To open Git Bash terminal, hit the key and type git-bash.
It may be possible that the installation didn't add a shortcut to the Start Menu.
You can navigate to the directory where git-bash.exe is (most likely C:\Program Files\Git\git-bash.exe), double click git-bash.exe to open Git Bash.
You can also right-click it and choose Pin to Start or Pin to taskbar.
SIDEBAR: Git Bash Terminal
Git Bash is a terminal application that lets you use Git from the command line on Windows. Since Git was originally developed for Unix-like systems (like Linux and macOS), Windows does not come with a native shell that supports all the commands and utilities commonly used with Git.
Git Bash provides a Unix-like command-line environment on Windows. It includes:
ls, cat, ssh, etc.) that are useful when working with Git and scripting.When copy-pasting text onto a Git Bash terminal, you will not be able to use the familiar Ctrl+V key combo to paste. Instead, right-click on the terminal and use the Paste menu option.
On Windows, you might need to close and open the terminal again for it to recognise changes done elsewhere in the computer (e.g., newly-installed software, changes to system variables, etc.).
Use your Linux distribution's package manager to install Git. Examples:
Debian/Ubuntu, run sudo apt-get update and then sudo apt-get install git.
Fedora: run sudo dnf update and then sudo dnf install git.
Verify Git is installed, by running the following command in a terminal.
git --version
git version 2._._
The output should display the version number.
user.name and user.emailGit needs to know who you are to record changes properly. When you save a snapshot of your work in Git, it records your name and email as the author of that change. This ensures everyone working on the project can see who made which changes. Accordingly, you should set the config settings user.name and user.email before you start Git for revision control.
PREPARATION: Set user.name and user.email
To set the two config settings, run the following commands in your terminal window:
git config --global user.name "<your-name>"
git config --global user.email "<your-email@example.com>"
To check if they are set as intended, you can use the following two commands:
git config --global user.name
git config --global user.email
Git is fundamentally a command-line tool. You primarily interact with it through its by typing commands. This gives you full control over its features and helps you understand what’s really happening under the hood.
clients for Git also exist, such as Sourcetree, GitKraken, and the built-in Git support in editors like Intellij IDEA and VS Code. These tools provide a more visual way to perform some Git operations.
If you're new to Git, it's best to learn the CLI first. The CLI is universal, always available (even on servers), and helps you build a solid understanding of Git’s concepts. You can use GUI clients as a supplement — for example, to visualise complex history structures.
Mastering the CLI gives you confidence and flexibility, while GUI tools can serve as helpful companions.
PREPARATION: [Optional] Install a GUI client
Optionally, you can install a Git GUI client. e.g., Sourcetree (installation instructions).
Our Git lessons show how to perform Git operations in Git CLI, and in Sourcetree -- the latter just to illustrate how Git GUIs work. It is perfectly fine for you to learn the CLI only.

[image credit: https://www.sourcetreeapp.com]
In these lessons, we are piloting a new companion app called Git-Mastery that we have been developing to help Git learners. Specifically, it provides exercises that you can do to self-test your Git knowledge, and the app will also verify if your solution is correct.
If you are new to Git, we strongly recommend that you install and use the Git-Mastery app.
PREPARATION: [Recommended] Install and Configure the Git-Mastery App
1. Install the Git-Mastery App
.exe file from the latest release..exe to your Windows System Variable PATH, by following this guide.C:\Users\Jane\Tools\gitmastery.exe, you should add C:\Users\Jane\Tools to your PATH.PATH to take effect).Windows Defender says gitmastery.exe is a virus?
In some cases, Windows Defender virus scanner can incorrectly block gitmastery.exe as a virus. The Git-Mastery team is currently working on getting the app white-listed. In the meantime, it is safe to override the warning/blockade, either by choosing Run anyway option (if given) or using the following steps.
Windows Security → Virus & threat protection.Protection history.gitmastery.exe and click it.Actions → Allow on device.Alternatively, refer to this page to see how to exclude a file from Windows virus scanner (look for the section named 'Exclusions').
brew tap git-mastery/gitmastery
brew install gitmastery
Ensure you are running libc version 2.38 or newer (you can use the ldd --version command to check the current version).
Then install the app by running the following commands:
echo "deb [trusted=yes] https://git-mastery.github.io/gitmastery-apt-repo any main" | \
sudo tee /etc/apt/sources.list.d/gitmastery.list > /dev/null
sudo apt install software-properties-common
sudo add-apt-repository "deb https://git-mastery.github.io/gitmastery-apt-repo any main"
sudo apt update
sudo apt-get install gitmastery
Use an AUR helper to install gitmastery-bin. For example using yay:
yay -S gitmastery-bin
Alternatively, you can build the PKGBUILD yourself following the instructions on the Arch wiki.
If you are using a Linux distribution that is not yet supported by Git-Mastery, please download the right binary for your architecture from the latest release.
Install it to /usr/bin to access the binary, the following using version 3.3.0 as an example.
install -D -m 0755 gitmastery-3.3.0-linux-arm64 /usr/bin/gitmastery
2. To verify the installation, run the gitmastery --help command from a couple of different folder locations. An example given below (IMPORTANT: change the cd command to match your folders):
gitmastery --help
cd ../my-projects # cd into a different folder
gitmastery --help
The current version of the app takes about 3-5 seconds to respond to a command. This is because the app comes with a bundled Python runtime (so that users don't need to install Python first) which needs to load first before the command can be executed.
3. Trigger the initial setup by running the gitmastery setup command in a suitable folder (the app will create files/folders inside this folder).
mkdir gitmastery-home
cd gitmastery-home
gitmastery setup
The gitmastery setup command will perform the following tasks:
user.name and user.email are set.gitmastery-exercises) by pressing Enter.gitmastery-exercises folder.Notes:
gitmastery check git command.4. Keep the app updated. As the Git-Mastery app is under active development, it is likely to get updated several times during the semester. When you run a gitmastery <command>, the output will warn you if there is a new version, in which case you should update the app immediately, by following the instructions in that message.
Instructions for updating the Git-Mastery app are here.
To be able to save snapshots of a folder using Git, you must first put the folder under Git's control by initialising a Git repository in that folder.
Normally, we use Git to manage a revision history of a specific folder, which gives us the ability to revision-control any file in that folder and its subfolders.
To put a folder under the control of Git, we initialise a repository (short name: repo) in that folder. This way, we can initialise repos in different folders, to revision-control different clusters of files independently of each other e.g., files belonging to different projects.
You can follow the hands-on practical below to learn how to initialise a repo in a folder.
What is this? HANDS-ON panels contain hands-on activities you can do as you learn Git. If you are new to Git, we strongly recommend that you do them yourself (even if they appear straightforward), as hands-on usage will help you internalise the concepts and operations better.
Preparation Choose a folder to put under Git's control. The folder may or may not contain any files. For this practical, let us create a folder named things for this purpose.
You can get the Git-Mastery app to for doing this practical, or create the sandbox manually. Instructions for the both options are given below.
To prepare the sandbox for this practical:
gitmastery-exercises folder.gitmastery download hp-init-repo command.The sandbox will be set up inside the gitmastery-exercises/hp-init-repo folder.
Assuming you have a folder named git-practicals that you wish to use for doing Git hands-on practicals in, you can run the following commands.
cd git-practicals
mkdir things
Avoid putting Git repos inside cloud-synced (e.g., OneDrive, Dropbox) folders. Reason: Multiple tools trying to detect/sync changes in the same folder can cause conflicts and unexpected behaviors.
If you want to access project files from multiple computers, use Git to do that (rather than cloud syncing tools).
1 Then cd into it.
cd things
2 Run the git status command to check the status of the folder.
git status
fatal: not a git repository (or any of the parent directories): .git
Don't panic. The error message is expected. It confirms that the folder currently does not have a Git repo.
3 Now, initialise a repository in that folder.
Use the command git init which should initialise the repo.
git init
Initialized empty Git repository in things/.git/
The output might also contain a hint about a name for an initial branch (e.g., hint: Using 'master' as the name for the initial branch ...). You can ignore that for now.
Note how the output mentions the repo being created in things/.git/ (not things/). More on that later.
Windows: Click File → Clone/New… → Click on + Create button on the top menu bar.

Enter the location of the directory and click Create.
Mac: New... → Create Local Repository (or Create New Repository) → Click ... button to select the folder location for the repository → click the Create button.
done!
Initialising a repo results in two things:
To confirm, you can run the git status command. It should respond with something like the following:
git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
Don't worry if you don't understand the output (we will learn about them later); what matters is that it no longer gives an error message as it did before.
done!
.git inside the things folder. This folder will be used by Git to store metadata about this repository.A Git-controlled folder is divided into two main parts:
.git subfolder, which contains all the metadata and history.What is this? EXERCISE panels contain a Git-Mastery exercise that you can download using the Git-Mastery app, and you can use the same app to verify that your solution is correct.
EXERCISE: under-control
What is this? DETOUR panels contain related directions you can optionally explore. We recommend that you only skim them the first time you are going through a tour (i.e., just to know what each detour covers); you can revisit them later, to deepen your knowledge further, or when you encounter a use case related to the concepts covered by the detour.
DETOUR: Undoing a Repo Initialisation
When Git initialises a repo in a folder, it does not touch any files in the folder, other than create the .git folder its contents. So, reversing the operation is as simple as deleting the newly-created .git folder.
git status #run this to confirm a repo exists
rm -rf .git #delete the .git folder
git status #this should give an error, as the repo no longer exists
To save a snapshot, you start by specifying what to include in it, also called staging.
Git considers new files that you add to the working directory as 'untracked' i.e., Git is aware of them, but they are not yet under Git's control. The same applies to files that existed in the working folder at the time you initialised the repo.
A Git repo has an internal space called the staging area which it uses to build the next snapshot. Another name for the staging area is the index.
We can stage an untracked file to tell Git that we want its current version to be included in the next snapshot (in Git terminology, such a snapshot is called a commit). Once you stage an untracked file, it becomes 'tracked' (i.e., under Git's control). A staged file can be unstaged to indicate that we no longer want it to be included in the next snapshot.
In the example below, you can see how staging files change the status of the repo as you go from (a) to (c).
staging area
[empty]
other metadata ...
├─ fruits.txt (untracked!)
└─ colours.txt (untracked!)
staging area
└─ fruits.txt
other metadata ...
├─ fruits.txt (tracked)
└─ colours.txt (untracked!)
fruits.txt.staging area
├─ fruits.txt
└─ colours.txt
other metadata ...
├─ fruits.txt (tracked)
└─ colours.txt (tracked)
colours.txt.preparation
You can continue using the sandbox created in the previous hands-on practical, or,
1 Add a file (e.g., fruits.txt) to the things repo folder.
Here is an easy way to do that with a single terminal command.
echo -e "apples\nbananas\ncherries" > fruits.txt
apples
bananas
cherries
Windows users: Use the git-bash terminal to run the above command (and all commands given in these lessons). Some of them might not work in other terminals such as the PowerShell.
To see the content of the file, you can use the cat command:
cat fruits.txt
2 Stage the new file.
2.1 Check the status of the folder using the git status command.
git status
On branch master
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
fruits.txt
nothing added to commit but untracked files present (use "git add" to track)
2.2 Use the git add <file> command to stage the file.
git add fruits.txt
You can replace the add with stage (e.g., git stage fruits.txt) and the result is the same (they are synonyms).
Windows users: When using the echo command to write to text files from Git Bash, you might see a warning LF will be replaced by CRLF the next time Git touches it when Git interacts with such a file. This warning is caused by the way line endings are handled differently by Git and Windows. You can simply ignore it, or suppress it in future by running the following command:
git config --global core.safecrlf false
2.3 Check the status again. You can see the file is no longer 'untracked'.
git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: fruits.txt
As before, don't worry if you don't understand the content of the output (we'll unpack it in a later lesson). The point to note is that the file is no longer listed as 'untracked'.
2.1 Note how the file is shown as ‘unstaged’. The question mark icon indicates the file is untracked.
If the newly-added file does not show up in Sourcetree UI, refresh the UI (: F5
| ⌥+R)
Sourcetree screenshots/instructions: vs
Note that Sourcetree UI can vary slightly between Windows and Mac versions. Some of the screenshots given in our lessons are from the Windows version while some are from the Mac version.
In som cases, we have specified how they differ.
In other cases, you may need to adapt if the given screenshots/instructions are slightly different from what you are seeing in your Sourcetree.
2.2 Stage the file:
Select the fruits.txt and click on the Stage Selected button.

Staging can be done using tick boxes or the ... menu in front of the file.

2.3 Note how the file is staged now i.e., fruits.txt appears in the Staged files panel now.
If Sourcetree shows a \ No newline at the end of the file message below the staged lines (i.e., below the cherries line in the above screenshot), that is because you did not hit enter after entering the last line of the file (hence, Git is not sure if that line is complete). To rectify, move the cursor to the end of the last line in that file and hit enter (like you are adding a blank line below it). This new change will now appear as an 'unstaged' change. Stage it as well.
done!
If you modify a staged file, it goes into the 'modified' state i.e., the file contains modifications that are not present in the copy that is waiting (in the staging area) to be included in the next snapshot. If you wish to include these new changes in the next snapshot, you need to stage the file again, which will overwrite the copy of the file that was previously in the staging area.
The example below shows how the status of a file changes when it is modified after it was staged.
staging area
Alice
other metadata ...
Alice
staging area
Alice
other metadata ...
Alice
Bob
staging area
Alice
Bob
other metadata ...
Alice
Bob
preparation
You can continue using the sandbox created in the previous hands-on practical, or,
1 First, add another line to fruits.txt, to make it 'modified'.
Here is a way to do that with a single terminal command.
echo "dragon fruits" >> fruits.txt
apples
bananas
cherries
dragon fruits
2 Now, verify that Git sees that file as 'modified'.
Use the git status command to check the status of the working directory.
$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: fruits.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: fruits.txt
Note how fruits.txt now appears twice, once as new file: ... (representing the version of the file we staged earlier, which had only three lines) and once as modified: ... (representing the latest version of the file which now has a fourth line).
Note how fruits.txt appears in the Staged files panel as well as 'Unstaged files'.
3 Stage the file again, the same way you added/staged it earlier.
4 Verify that Git no longer sees it as 'modified', similar to step 2.
done!
Staging applies regardless of whether a file is currently tracked.
Git also supports fine-grained selective staging i.e., staging only specific changes within a file while leaving other changes to the same file unstaged. This will be covered in a later lesson.
Git does not track empty folders. It tracks only folders that contain tracked files.
You can test this by adding an empty subfolder inside the things folder (e.g., things/more-things) and checking if it shows up as 'untracked' (it will not). If you add a file to that folder (e.g., things/more-things/food.txt) and then staged that file (e.g., git add more-things/food.txt), the folder will now be included in the next snapshot.
PRO-TIP: Applying a Git command to multiple files in one go
When a Git command expects a list of files or paths as a parameter (as the git add command does), these parameters are known as pathspecs — patterns that tell Git which files or directories to operate on. Pathspecs can be simple file names, directory names, or more complex patterns.
Here are some common ways to write them, shown with examples using the git add <pathspec> command:
Specify multiple files, separated by spaces:
git add f1.txt f2.txt data/lists/f3.txt # stages the specified three files
Use a glob pattern:
git add '*.txt' # stages all .txt files in the current directory
Quoting the glob pattern is recommended so your shell doesn’t expand it before Git sees it.
Use . to indicate 'all in the current directory and subdirectories':
git add . # stages all files in current directory and its subdirectories
Specific directory, to indicate 'this directory and its subdirectories':
git add path/to/dir # stages all files in path/to/dir and its subdirectories
Negated pathspecs, to indicate 'except these':
git add . ':!*.log' # stage everything except .log files
Git supports combining these features — for example, you could add all .txt files except those in a certain folder using:
git add '*.txt' ':!docs/*.txt'
EXERCISE: stage-fright
DETOUR: Unstaging Changes
You can unstage a staged file, which simply removes it from the staging area but keeps the changes in your working directory. This is useful if you later realise that you don’t actually want to include a staged file in the next commit — perhaps you staged it by mistake, or you want to include that change in a later commit.
To unstage a file you added or modified, run git restore --staged <pathspec>. This command removes the file from the staging area, leaving your working directory untouched.
git restore --staged plan.txt budget.txt data/list.txt
If your repo does not have any commits yet, git restore --staged will fail with the error fatal: could not resolve HEAD.
The remedy is to use git reset <pathspec> instead.
git reset plan.txt
In fact, git reset is an alternative way of unstaging files, and it works regardless of whether you have any commits.
Wait. Then why does git restore --staged exists at all, given it is more verbose and doesn't even work in some special cases?
Answer: It is still considered the "modern" way of unstaging files (it was introduced more recently), because it is more intuitive and purpose-specific -- whereas the git reset serves multiple purposes and, if used incorrectly, can cause unintended consequences.
The restore command can accept multiple files/paths as input, which means you can use the notation for specifying multiple files. For example, to unstage all changes you've staged, you can use the git restore --staged .
To unstage a file deletion (staged using git rm), use the same command as above. It will unstage the deletion and restore the file in the staging area.
If you also deleted the file from your working directory, you may need to recover it separately with git restore <file-name(s)>
git restore data/list.txt data/plan.txt
To 'nuke' all changes (i.e., get rid of all staged and unstaged changes to tracked files), you can add the --worktree flag to the git restore --staged <pathspec> command.
git restore --staged --worktree . # nuke all changes in current folder and subfolders
To unstage a file, locate the file among the staged files section, click the ... in front the file, and choose Unstage file:

EXERCISE: staging-intervention
Related DETOUR: Staging File Deletions
When you delete a tracked file from your working directory, Git doesn’t automatically assume you want that change to be part of your next commit. To tell Git you intend to record a file deletion in the repository’s history, you need to stage the deletion explicitly.
That aspect is covered in the tour Staging File Deletions given in the lesson T1L5. Saving a Snapshot.
After staging, you can now proceed to save the snapshot, aka creating a commit.
Saving a snapshot is called committing and a saved snapshot is called a commit.
A Git commit is a full snapshot of your working directory based on the files you have staged, more precisely, a record of the exact state of all files in the staging area (index) at that moment -- even the files that have not changed since the last commit. This is in contrast to other revision control software that only store the in a commit. Consequently, a Git commit has all the information it needs to recreate the snapshot of the working directory at the time the commit was created.
A commit also includes metadata such as the author, date, and an optional commit message describing the change.
A Git commit is a snapshot of all tracked files, not simply a delta of what changed since the last commit.
Assuming you have previously staged changes to the fruits.txt, go ahead and create a commit.
1 First, let us do a sanity check using the git status command.
git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: fruits.txt
2 Now, create a commit using the commit command. The -m switch is used to specify the commit message.
git commit -m "Add fruits.txt"
[master (root-commit) d5f91de] Add fruits.txt
1 file changed, 5 insertions(+)
create mode 100644 fruits.txt
3 Verify the staging area is empty using the git status command again.
git status
On branch master
nothing to commit, working tree clean
Note how the output says nothing to commit which means the staging area is now empty.
Click the Commit button, enter a commit message (e.g. add fruits.txt) into the text box, and click Commit.
done!
EXERCISE: grocery-shopping
DETOUR: Staging File Deletions
When you delete a tracked file from your working directory, Git doesn’t automatically assume you want that change to be part of your next commit. To tell Git you intend to record a file deletion in the repository’s history, you need to stage the deletion explicitly.
When you stage a deleted file, you’re adding the removal of the file to the staging area, just like you’d stage a modified or newly created file. After staging, the next commit will reflect that the file was removed from the project.
Note that staging a file deletion matters only if there is at least one commit in the repository. Before any commits are made, there is no file history, so deletions have no effect on the repository.
To delete a file and stage the deletion in one go, you can use the git rm <pathspec> command. It removes the file from the working directory and stages the deletion at the same time.
git rm data/list.txt plan.txt
If you’ve already deleted the file manually (for example, using rm or deleting it in your file explorer), you can still stage the deletion using the git add <pathspec> command. Even though the file no longer exists, git add records its deletion into the staging area.
git add data/list.txt
Unstaging file deletions is covered in the tour Unstaging Changes given in the lesson T1L4. Specifying What to Include in a Snapshot.
Staging a file deletion is done similar to staging other changes.
Related DETOUR: Updating the Last Commit
Git allows you to amend the most recent commit. This is useful when you realise there’s something you’d like to change — e.g., fix a typo in the commit message, or to exclude some unintended change from the commit.
That aspect is covered in the tour Updating the Last Commit given in the lesson T5L3. Reorganising Commits.
Related DETOUR: Resetting Uncommitted Changes
At times, you might need to get rid of uncommitted changes so that you have a fresh start to the next commit.
That aspect is covered in the tour Resetting Uncommitted Changes given in the lesson T4L5. Rewriting History to Start Over.
Related DETOUR: Undoing/Deleting Recent Commits
How do you undo or delete the last few commits if you realise they were incorrect, unnecessary, or done too soon?
That aspect is covered in the tour Undoing/Deleting Recent Commits given in the lesson T4L5. Rewriting History to Start Over.
It is useful to be able to visualise the commits timeline, aka the revision graph.
Git commits form a timeline, as each corresponds to a point in time when you asked Git to take a snapshot of your working directory. Each commit links to at least one previous commit, forming a structure that we can traverse.
A timeline of commits is called a branch. By default, Git names the initial branch master -- though many now use main instead. You'll learn more about branches in future lessons. For now, just be aware that the commits you create in a new repo will be on a branch called master (or main) by default.
gitGraph
%%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master (or main)'}} }%%
commit id: "Add fruits.txt"
commit id: "Update fruits.txt"
commit id: "Add colours.txt"
commit id: "..."
Git can show you the list of commits in the Git history.
Preparation Use the things repo you created in a previous lesson. Alternatively, you can use the commands given below to create such a repo from scratch.
mkdir things # create a folder for the repo
cd things
git init
echo -e "apples\nbananas\ncherries\ndragon fruits" > fruits.txt
git add fruits.txt
git commit -m "Add fruits.txt"
You can copy-paste a list of commands (such as commands given above), including any comments, to the terminal. After that, hit enter to run them in sequence.
1 View the list of commits, which should show just the one commit you created just now.
You can use the git log command to see the commit history.
git log
commit ... (HEAD -> master)
Author: ... <...@...>
Date: ...
Add fruits.txt
Use the Q key to exit the output screen of the git log command.
Note how the output has some details about the commit you just created. You can ignore most of it for now, but notice it also shows the commit message you provided.
Expand the BRANCHES menu and click on the master to view the history graph, which contains only one node at the moment, representing the commit you just added. For now, ignore the label master attached to the commit.
2 Create a few more commits (i.e., a few rounds of add/edit files → stage → commit), and observe how the list of commits grows.
Here is an example list of bash commands to add two commits while observing the list of commits
echo "figs" >> fruits.txt # add another line to fruits.txt
git add fruits.txt # stage the updated file
git commit -m "Insert figs into fruits.txt" # commit the changes
git log # check commits list
echo "a file for colours" >> colours.txt # add a colours.txt file
echo "a file for shapes" >> shapes.txt # add a shapes.txt file
git add colours.txt shapes.txt # stage both files in one go
git commit -m "Add colours.txt, shapes.txt" # commit the changes
git log # check commits list
The output of the final git log should be something like this:
commit ... (HEAD -> master)
Author: ... <...@...>
Date: ...
Add colours.txt, shapes.txt
commit ...
Author: ... <...@...>
Date: ...
Insert figs into fruits.txt
commit ...
Author: ... <...@...>
Date: ...
Add fruits.txt
SIDEBAR: Working with the 'less' pager
Some Git commands — such as git log— may show their output through a pager. A pager is a program that lets you view long text one screen at a time, so you don’t miss anything that scrolls off the top. For example, the output of git log command will temporarily hide the current content of the terminal, and enter the pager view that shows output one screen at a time. When you exit the pager, the git log output will disappear from view, and the previous content of the terminal will reappear.
command 1
output 1
git log
→
commit f761ea63738a...
Author: ... <...@...>
Date: Sat ...
Add colours.txt
By default, Git uses a pager called less. Given below are some useful commands you can use inside the less pager.
| Command | Description |
|---|---|
q | Quit less and return to the terminal |
↓ or j | Move down one line |
↑ or k | Move up one line |
Space | Move down one screen |
b | Move up one screen |
G | Go to the end of the content |
g | Go to the beginning of the content |
/pattern | Search forward for pattern (e.g., /fix) |
n | Repeat the last search (forward) |
N | Repeat the last search (backward) |
h | Show help screen with all less commands |
If you’d rather see the output directly, without using a pager, you can add the --no-pager flag to the command e.g.,
git --no-pager log
It is possible to ask Git to not use less at all, use a different pager, or fine-tune how less is used. For example, you can reduce Git's use of the pager (recommended), using the following command:
git config --global core.pager "less -FRX"
Explanation:
-F : Quit if the output fits on one screen (don’t show pager unnecessarily)-R : Show raw control characters (for coloured Git output)-X : Keep content visible after quitting the pager (so output stays on the terminal)To see the list of commits, click on the History item (listed under the WORKSPACE section) on the menu on the right edge of Sourcetree.
After adding two more commits, the list of commits should look something like this:
done!
The Git data model consists of two types of entities: objects and refs (short for references). In this lesson, you will encounter examples of both.
A Git revision graph is a visualisation of a repo's revision history, consisting of one or more branches. First, let us learn to work with simpler revision graphs consisting of one branch, such as the one given below.
f761ea63738a67258628e9e54095b88ea67d95e2) that acts like a fingerprint, ensuring that every commit can be referenced unambiguously. That is, every commit has a unique SHA-1 hash value.Given every commit has a unique hash, the commit hash values you see in our examples will be different from the hash values of your own commits, for example, when following our hands-on practicals.
Edges in the revision graph represent links between a commit and its parent commit(s). In some revision graph visualisations, you might see arrows (instead of lines) showing how each commit points to its parent commit.
Git uses refs to name and keep track of various points in a repository’s history. These refs are essentially 'named-pointers' that can serve as bookmarks to reach a certain point in the revision graph using the ref name.
In the revision graph above, there are two refs master and ←HEAD.
C3.master branch.HEAD may point directly to a specific commit instead of a branch. This situation is called a "detached HEAD", which will be covered in a later lesson.Target Use Git features to examine the revision graph of a simple repo.
Preparation Use a repo with just a few commits and only one branch.
1 First, use a simple git log to view the list of commits.
git log
commit f761ea63738a... (HEAD -> master)
Author: ... <...@...>
Date: Sat ...
Add colours.txt, shapes.txt
commit 2bedace69990...
Author: ... <...@...>
Date: Sat ...
Add figs to fruits.txt
commit d5f91de5f0b5...
Author: ... <...@...>
Date: Fri ...
Add fruits.txt
Given below the visual representation of the same revision graph. As you can see, the log output shows the refs slightly differently, but it is not hard to see what they mean.
2 Use the --oneline flag to get a more concise view. Note how the commit SHA has been truncated to first seven characters (first seven characters of a commit SHA is enough for Git to identify a commit).
git log --oneline
f761ea6 (HEAD -> master, origin/master) Add colours.txt, shapes.txt
2bedace Add figs to fruits.txt
d5f91de Add fruits.txt
3 The --graph flag makes the result closer to a graphical revision graph. Note the * that indicates a node in a revision graph.
git log --oneline --graph
* f761ea6 (HEAD -> master, origin/master) Add colours.txt, shapes.txt
* 2bedace Add figs to fruits.txt
* d5f91de Add fruits.txt
The --graph option is more useful when examining a more complicated revision graph consisting of multiple parallel branches.
Click the History to see the revision graph.
HEAD ref may not be shown -- it is implied that the HEAD ref is pointing to the same commit the currently active branch ref is pointing.
done!
At this point: You should now be able to initialise a Git repository in a folder and commit snapshots of its files at times of your choice. So far, you did not learn how to actually make use of those snapshots (other than to show a list of them) -- we will do that in later tours.
What's next: Tour 2: Backing up a Repo on the Cloud
Target Usage: To back up a Git repository on a cloud-based Git service such as GitHub.
Motivation: One (of several) benefits of maintaining a copy of a repo on a cloud server: it acts as a safety net (e.g., against the folder becoming inaccessible due to a hardware fault).
Lesson plan:
T2L1. Remote Repositories covers that part.
T2L2. Preparing to use GitHub covers that part.
T2L3. Creating a Repo on GitHub covers that part.
T2L4. Linking a Local Repo With a Remote Repo covers that part.
T2L5. Updating the Remote Repo covers that part.
T2L6. Omitting Files from Revision Control covers that part.
To back up your Git repo on the cloud, you’ll need to use a remote repository service, such as GitHub.
A repo you have on your computer is called a local repo. A remote repo is a repo hosted on a remote computer and allows remote access. Some use cases for remote repositories:
It is possible to set up a Git remote repo on your own server, but an easier option is to use a remote repo hosting service such as GitHub.
To use GitHub, you need to sign up for an account, and configure related tools/settings first.
GitHub is a web-based service that hosts Git repositories and adds collaboration features on top of Git. Two other similar platforms are GitLab and Bitbucket. While Git manages version control locally, such platforms provide additional features such as shared access to repositories, issue tracking, code reviews, and permission controls. They are widely used in software development projects, for both open-source software (OSS) and closed-source software projects.
On GitHub, a Git repo can be put in one of two spaces:
Every GitHub user must have a user account, even if they primarily work within an organisation.
PREPARATION: Create a GitHub account
Create a personal GitHub account as described in GitHub Docs → Creating an account on GitHub, if you don't have one yet.
Choose a sensible GitHub username as you are likely to use it for years to come in professional contexts e.g., in job applications.
[Optional, but recommended] Set up your GitHub profile, as explained in GitHub Docs → Setting up your profile.
Before you can interact with GitHub from your local Git client, you need to set up authentication. In the past, you could simply enter your GitHub username and password, but GitHub no longer accepts passwords for Git operations. Instead, you’ll use a more secure method — such as a Personal Access Token (PAT) or SSH keys — to prove your identity.
A Personal Access Token (PAT) is essentially a long, random string that acts like a password, but it can be scoped to specific permissions (e.g., read-only or full access) and revoked at any time. This makes it more secure and flexible than a traditional password.
Git supports two main protocols for communicating with GitHub: HTTPS and SSH.
PREPARATION: Set up authentication with GitHub
Set up your computer's GitHub authentication, as described in the se-edu guide Setting up GitHub Authentication.
GitHub associates a commit to a user based on the email address in the commit metadata. When you push a commit, GitHub checks if the email matches a verified email on a GitHub account. If it does, the commit is shown as authored by that user. If the email doesn’t match any account, the commit is still accepted but won’t be linked to any profile.
GitHub provides a no-reply email (e.g., 12345678+username@users.noreply.github.com) that you can use as your Git user.email to hide your real email while still associating commits with your GitHub account.
PREPARATION: [Optional] Configure user.email to use the no-reply email from GitHub
If you prefer not to include your real email address in commits, you can do the following:
Find your no-reply email provided by GitHub: Navigate to the email settings of your GitHub account and select the option to Keep my email address private. The no-reply address will then be displayed, typically in the format ID+USERNAME@users.noreply.github.com.

Update your user.email with that email address e.g.,
git config --global user.email "12345678+username@users.noreply.github.com"
GitHub offers its own clients to make working with GitHub more convenient.
gh) brings GitHub-specific commands to your terminal, letting you perform operations on GitHub from your command line.If you are using Git-Mastery exercises (strongly recommended), you need to install and configure GitHub CLI because it is needed by Git-Mastery exercises involving GitHub.
PREPARATION: Set up GitHub CLI
1. Download and run the installer from the GitHub CLI releases page. This is the file named as GitHub CLI {version} windows {chip variant} installer.
1. Install GitHub CLI using Homebrew:
brew install gh
1. Install GitHub CLI, as explained in the GitHub CLI Linux installation guide for your distribution.
2. Authenticate yourself to GitHub account:
gh auth login
When prompted, choose the protocol (i.e., HTTPS or SSH) you used previously to set up your GitHub authentication.
3. Give GitHub CLI permission to delete repos in your account, as this is required for some of the Git-Mastery exercises.
gh auth refresh -s delete_repo
4. Verify the setup by checking the status of your GitHub CLI with your GitHub account.
gh auth status
You should see confirmation that you’re logged in.
5. Verify that Github and GitHub CLI is set up for Git-Mastery:
gitmastery check github
6. [Optional, Recommended] Ask Git-Mastery to switch on the 'progress sync' feature.
# cd into the gitmastery-exercises folder first
gitmastery progress sync on
What happens when you switch on the Git-Mastery 'progress sync' feature?
The first step of backing up a local repo on GitHub: create an empty repository on GitHub.
You can create a remote repository based on an existing local repository, to serve as a remote copy of your local repo. For example, suppose you created a local repo and worked with it for a while, but now you want to upload it onto GitHub. The first step is to create an empty repository on GitHub.
1 Login to your GitHub account and choose to create a new repo.

2 In the next screen, provide a name for your repo. Refer the screenshot below on some guidance on how to provide the required information.

Click Create repository button to create the new repository.
If you enable any of the three Add _____ options shown above, GitHub will not only create a repo, but will also initialise it with some initial content. That is not what we want here. To create an empty remote repo, keep those options disabled.
3 Note the URL of the repo. It will be of the form
https://github.com/{your_user_name}/{repo_name}.git.
e.g., https://github.com/johndoe/foobar.git (note the .git at the end)

done!
EXERCISE: remote-control
The second step of backing up a local repo on GitHub: link the local repo with the remote repo on GitHub.
A Git remote is a reference to a repository hosted elsewhere, usually on a server like GitHub, GitLab, or Bitbucket. It allows your local Git repo to communicate with another remote copy — for example, to upload locally-created commits that are missing in the remote copy.
By adding a remote, you are informing the local repo details of a remote repo it can communicate with, for example, where the repo exists and what name to use to refer to the remote.
The URL you use to connect to a remote repo depends on the protocol — HTTPS or SSH:
https://github.com/ (for GitHub users). e.g.,https://github.com/username/repo-name.git
git@github.com:. e.g.,git@github.com:username/repo-name.git
A Git repo can have multiple remotes. You simply need to specify different names for each remote (e.g., upstream, central, production, other-backup ...).
Add the empty remote repo you created on GitHub as a remote of a local repo you have.
1 In a terminal, navigate to the folder containing the local repo things you created earlier.
2 List the current list of remotes using the git remote -v command, for a sanity check. No output is expected if there are no remotes yet.
3 Add a new remote repo using the git remote add <remote-name> <remote-url> command.
Format of the <remote-url>:
https://github.com/<owner>/<repo>.git # using HTTPS
git@github.com:<owner>/<repo>.git # using SSH
The full command:
git remote add origin https://github.com/JohnDoe/things.git # using HTTPS
git remote add origin git@github.com:JohnDoe/things.git # using SSH
4 List the remotes again to verify the new remote was added.
git remote -v
origin https://github.com/johndoe/things.git (fetch)
origin https://github.com/johndoe/things.git (push)
The same remote will be listed twice, to show that you can do two operations (fetch and push) using this remote. You can ignore that for now. The important thing is the remote you added is being listed.
1 Open the local repo in Sourcetree.
2 Open the dialog for adding a remote, as follows:
Choose Repository → Repository Settings menu option.
Choose Repository → Repository Settings... → Choose Remotes tab.
3 Add a new remote to the repo with the following values.
Remote name: the name you want to assign to the remote repo i.e., origin
URL/path: the URL of your remote repo
https://github.com/<owner>/<repo>.git # using HTTPS
git@github.com:<owner>/<repo>.git # using SSH
e.g.,
https://github.com/JohnDoe/things.git # using HTTPS
git@github.com:JohnDoe/things.git # using SSH
Username: your GitHub username
4 Verify the remote was added by going to Repository → Repository Settings again.
5 Add another remote, to verify that a repo can have multiple remotes. You can use any name (e.g., backup and any URL for this).
done!
To find the URL of a repo on GitHub, you can click on the Code button:

EXERCISE: link-me
DETOUR: Managing Details of a Remote
To change the URL of a remote (e.g., origin), use git remote set-url <remote-name> <new-url> e.g.,
git remote set-url origin https://github.com/user/repo.git
To rename a remote, use git remote rename <old-name> <new-name> e.g.,
git remote rename origin upstream
To delete a remote from your Git repository, use git remote remove <remote-name> e.g.,
git remote remove origin
To check the current remotes and their URLs, use:
git remote -v
The third step of backing up a local repo on GitHub: push a copy of the local repo to the remote repo.
You can push content of one repository to another, usually from your local repo to a remote repo. Pushing transfers recorded Git history (such as past commits), but it does not transfer unstaged changes or untracked files.
You can configure Git to track a pairing between a local branch and a remote branch, so in future you can push from the same local branch to the corresponding remote branch without needing to specify them again. For example, you can set your local master branch to track the master branch on the remote repo origin i.e., local master branch will track the branch origin/master.
In the revision graph above, you see a new type of ref ( origin/master). This is a remote-tracking branch ref that represents the state of a corresponding branch in a remote repository (if you previously set up the branch to 'track' a remote branch). In this example, the master branch in the remote origin is also at the commit C3 (which means you have not created new commits after you pushed to the remote).
If you now create a new commit C4, the state of the revision graph will be as follows:
Explanation: When you create C4, the current branch master moves to C4, and HEAD moves along with it. However, the master branch in the remote origin remains at C3 (because you have not pushed C4 yet). That is, the remote-tracking branch origin/master is one commit behind the local branch master (or, the local branch is one commit ahead). The origin/master ref will move to C4 only after you push your local branch to the remote again.
Preparation Use a local repo that is connected to an empty remote repo e.g., the things repo from previous hands-on practicals:
1 Push the master branch to the remote. Also instruct Git to track this branch pair.
Use the git push -u <remote-repo-name> <local-branch-name> to push the commits to a remote repository.
git push -u origin master
Explanation:
push: the Git sub-command that pushes the current local repo content to a remote repoorigin: name of the remotemaster: branch to push-u (or --set-upstream): the flag that tells Git to track that this local master is tracking origin/master branchClick the Push button on the buttons ribbon at the top.

In the next dialog, ensure the settings are as follows, ensure the Track option is selected, and click the Push button on the dialog.

2 Observe the remote-tracking branch origin/master is now pointing at the same commit as the master branch.
Use the git log --oneline --graph to see the revision graph.
* f761ea6 (HEAD -> master, origin/master) Add colours.txt, shapes.txt
* 2bedace Add figs to fruits.txt
* d5f91de Add fruits.txt
Click the History to see the revision graph.
HEAD ref may not be shown -- it is implied that the HEAD ref is pointing to the same commit the currently active branch ref is pointing.origin/master) is not showing up, you may need to enable the Show Remote Branches option.
done!
The push command can be used repeatedly to send further updates to another repo e.g., to update the remote with commits you created since you pushed the first time.
Target Add a commit to the same local repo, and push it to the remote repo.
1 Commit some changes in your local repo. Example:
echo "Elderberries" >> fruits.txt
git commit -am "Update fruits list"
Use the git commit command to create commits, as you did before.
Optionally, you can run the git status command, which should confirm that your local branch is 'ahead' by one commit (i.e., the local branch has commits that are not present in the corresponding branch in the remote repo).
git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
You can also use the git log --oneline --graph command to see where the branch refs are. Note how the remote-tracking branch origin/master is one commit behind the local master.
e60deae (HEAD -> master) Update fruits list
f761ea6 (origin/master) Add colours.txt, shapes.txt
2bedace Add figs to fruits.txt
d5f91de Add fruits.txt
Create commits as you did before.
Before pushing the new commit, Sourcetree will indicate that your local branch is 'ahead' by one commit (i.e., the local branch has one new commit that is not in the corresponding branch in the remote repo).
2 Push the new commits to your fork on GitHub.
To push the newer commit(s) in the current branch master to the remote origin, you can use any of the following commands:
git push origin mastergit push originmaster) even if you don't specify it.git pushmaster). Due to tracking you've set up earlier, Git will assume that you want to push it to the matching branch on origin.After pushing, the revision graph should look something like the following (note how both local and remote-tracking branch refs are pointing to the same commit again).
e60deae (HEAD -> master, origin/master) Update fruits list
f761ea6 Add colours.txt, shapes.txt
2bedace Add figs to fruits.txt
d5f91de Add fruits.txt
To push, click the Push button on the top buttons ribbon, ensure the settings are as follows in the next dialog, and click the Push button on the dialog.
After pushing the new commit to the remote, the remote-tracking branch ref should move to the new commit:

done!
Note that you can push between two repos only if those repos have a shared history among them (i.e., one should have been created by copying the other).
EXERCISE: push-over
DETOUR: Pushing to Multiple Repos
You can push to any number of repos, as long as the target repos and your repo have a shared history.
upstream, central, production, backup ...), if you haven't done so already.e.g., git push backup master

Git allows you to specify which files should be omitted from revision control.
You can specify which files Git should ignore from revision control. While you can always omit files from revision control simply by not staging them, having an 'ignore-list' is more convenient, especially if there are files inside the working folder that are not suitable for revision control (e.g., temporary log files) or files you want to prevent from accidentally including in a commit (files containing confidential information).
A repo-specific ignore-list of files can be specified in a .gitignore file, stored in the root of the repo folder.
The .gitignore file itself can be either revision controlled or ignored.
.gitignore file changes over time), simply commit it as you would commit any other file..gitignore file itself.The .gitignore file supports file patterns e.g., adding temp/*.tmp to the .gitignore file prevents Git from tracking any .tmp files in the temp directory.
SIDEBAR: .gitignore File Syntax
Blank lines: Ignored and can be used for spacing.
Comments: Begin with # (lines starting with # are ignored).
# This is a comment
Write the name or pattern of files/directories to ignore.
log.txt # Ignores a file named log.txt
Wildcards:
* matches any number of characters, except / (i.e., for matching a string within a single directory level):abc/*.tmp # Ignores all .tmp files in abc directory
** matches any number of characters (including /)**/foo.tmp # Ignores all foo.tmp files in any directory
? matches a single characterconfig?.yml # Ignores config1.yml, configA.yml, etc.
[abc] matches a single character (a, b, or c)file[123].txt # Ignores file1.txt, file2.txt, file3.txt
Directories:
/ to match directories.logs/ # Ignores the logs directory
/ match files/folders recursively.*.bak # Ignores all .bak files anywhere
/ are relative to the .gitignore location./secret.txt # Only ignores secret.txt in the root directory
Negation: Use ! at the start of a line to not ignore something.
*.log # Ignores all .log files
!important.log # Except important.log
Example:
# Ignore all log files
*.log
# Ignore node_modules folder
node_modules/
# Don’t ignore main.log
!main.log
1 Add a file into your repo's working folder that you presumably do not want to revision-control e.g., a file named temp.txt. Observe how Git has detected the new file.
Add a few other files with .tmp extension.
2 Configure Git to ignore those files:
Create a file named .gitignore in the working directory root and add the text temp.txt into it.
echo "temp.txt" >> .gitignore
temp.txt
Observe how temp.txt is no longer detected as 'untracked' by running the git status command (but now it will detect the .gitignore file as 'untracked'.
Update the .gitignore file as follows:
temp.txt
*.tmp
Observe how .tmp files are no longer detected as 'untracked' by running the git status command.
The file should be currently listed under Unstaged files. Right-click it and choose Ignore.... Choose Ignore exact filename(s) and click OK.
Also take note of other options available e.g., Ignore all files with this extension etc. They may be useful in future.
Note how the temp.text is no longer listed under Unstaged files. Observe that a file named .gitignore has been created in the working directory root and has the following line in it. This new file is now listed under Unstaged files.
temp.txt
Right-click on any of the .tmp files you added, and choose Ignore... as you did previously. This time, choose the option Ignore files with this extension.
Note how .temp files are no longer shown as unstaged files, and the .gitignore file has been updated as given below:
temp.txt
*.tmp
3 Optionally, stage and commit the .gitignore file.
done!
Files recommended to be omitted from version control
*.class, *.jar, *.exe.idea/) EXERCISE: ignoring-somethings
DETOUR: Ignoring Previously-Tracked Files
Adding a file to the .gitignore file is not enough if the file was already being tracked by Git in previous commits. In such cases, you need to do both of the following:
git rm --cached <file(s)> command.git rm --cached data/ic.txt
.gitignore file, as usual.At this point: You should now be able to create a copy of your repo on GitHub, and keep it updated as you add more commits to your local repo. If something goes wrong with your local repo (e.g., disk crash), you can now recover the repo using the remote repo (this tour did not cover how exactly you can do that -- it will be covered in a future tour).
What's next: Tour 3: Working Off a Remote Repo