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 |
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:
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.
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 NullPointerException
null
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 r
p
r
so that its attributes x
and y
match those of p
import 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()
: int
int
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 3
2
public 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:
You learned that objects can have members (methods, attributes). Well, there is something called class-level members too. Let's learn what they are, how they differ from the former, and how to implement 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:
While you can (and will be) defining your own classes, Java comes with a whole bunch of built-in classes that you can use right-away. Let's learn about some of the most useful such built-in classes next.
An Application Programming Interface (API) specifies the interface through which other programs can interact with a software component. It is a contract between the component and its clients.
A class has an API (e.g., API of the Java String
class, API of the Python str
class) which is a collection of public methods that you can invoke to make use of the class.
The GitHub API is a collection of web request formats that the GitHub server accepts and their corresponding responses. You can write a program that interacts with GitHub through that API.
When developing large systems, if you define the API of each component early, the development team can develop the components in parallel because the future behavior of the other components are now more predictable.
Java comes with a rich collection of classes that you can use. They form what is known as the Java API (Application Programming Interface). Each class in the API comes with documentation in a standard format.
String is a built-in Java class that you can use without importing. Given below are some useful String methods:
Strings provide a method named charAt
, which extracts a character. It returns a char
, a primitive type that stores an individual character (as opposed to strings of them).
String fruit = "banana";
char letter = fruit.charAt(0);
The argument 0 means that we want the letter at position 0. Like array indexes, string indexes start at 0, so the character assigned to letter
is 'b'
.
You can convert a string to an array of characters using the toCharArray
method.
char[] fruitChars = fruit.toCharArray()
Strings provide methods, toUpperCase
and toLowerCase
, that convert from uppercase to lowercase and back.
After these statements run, upperName
refers to the string "ALAN TURING"
but name
still refers to "Alan Turing"
.
String name = "Alan Turing";
String upperName = name.toUpperCase();
System.out.println(name);
System.out.println(upperName);
Alan Turing
ALAN TURING
Note that a string method cannot change the string object on which the method is invoked, because strings are . For example, when you invoke toUpperCase
on a string "abc"
, you get a new string object "ABC"
as the return value rather than the string "abc"
being changed to "ABC"
. As a result, for such string methods that seemingly modify the string but actually return a new string instead e.g., toLowerCase
, invoking the method has no effect if you don’t assign the return value to a variable.
String s = "Ada";
s.toUpperCase(); // no effect
s = s.toUpperCase(); // the correct way
Another useful method is replace
, which finds and replaces instances of one string within another.
This example replaces "Computer Science"
with "CS"
.
String text = "Computer Science is fun!";
text = text.replace("Computer Science", "CS");
System.out.println(text);
CS is fun!
The substring
method returns a new string that copies letters from an existing string, starting at the given index.
"banana".substring(0)
"banana"
"banana".substring(2)
"nana"
"banana".substring(6)
""
If it’s invoked with two arguments, they are treated as a start and end index:
"banana".substring(0, 3)
"ban"
"banana".substring(2, 5)
"nan"
"banana".substring(6, 6)
""
The indexOf
method searches for a single character (or a substring) in a string and returns the index of the first occurrence. The method returns -1
if there are no occurrences.
"banana".indexOf('a')
1
"banana".indexOf('a', 2)
3
searches for 'a'
, starting from position 2"banana".indexOf('x')
-1
"banana".indexOf("nan")
2
searches for the substring "nan"
To compare two strings, it is tempting to use the ==
and !=
operators.
String name1 = "Alan Turing";
String name2 = "Alan Turing";
System.out.println(name1 == name2);
This code compiles and runs, and most of the time it shows true
. But it is not correct. The problem is, , the ==
operator checks whether the two variables refer to the same object (by comparing the references). If you give it two different string objects that contain the same letters, it is supposed to yield false
because they are two distinct objects even if they contain the same text. However, because Java strings are immutable, in some cases (but not always) Java reuses existing string objects instead of creating multiple objects, which can cause the above code to yield true
. Therefore, it is not safe to use ==
to compare strings if your intention is to check if they contain the same text.
The right way to compare strings is with the equals
method.
This example invokes equals
on name1
and passes name2
as an argument. The equals
method returns true
if the strings contain the same characters; otherwise it returns false
.
if (name1.equals(name2)) {
System.out.println("The names are the same.");
}
If the strings differ, you can use compareTo
to see which comes first in alphabetical order. The return value from compareTo
is the difference between the first characters in the strings that differ. If the strings are equal, their difference is zero. If the first string (the one on which the method is invoked) comes first in the alphabet, the difference is negative. Otherwise, the difference is positive.
In this example, compareTo
returns positive 8, because the second letter of "Alan" comes 8 letters after the second letter of "Ada".
String name1 = "Alan";
String name2 = "Ada";
int diff = name1.compareTo(name2);
if (diff == 0) {
System.out.println("The names are the same.");
} else if (diff < 0) {
System.out.println("name1 comes before name2.");
} else if (diff > 0) {
System.out.println("name2 comes before name1.");
}
Both equals
and compareTo
are case-sensitive. The uppercase letters come before the lowercase letters, so "Ada"
comes before "ada"
. To check if two strings are similar irrespective of the differences in case, you can use the equalsIgnoreCase
method.
String s1 = "Apple";
String s2 = "apple";
System.out.println(s1.equals(s2)); //false
System.out.println(s1.equalsIgnoreCase(s2)); //true
Some more comparison-related String
methods:
contains
: checks if one string is a sub-string of the other e.g., Snapple
and app
startsWith
: checks if one string has the other as a substring at the beginning e.g., Apple
and App
endsWith
: checks if one string has the other as a substring at the end e.g., Crab
and ab
You can embed a special character e.g., line break, tab, backspace, etc. in a string using an escape sequence.
Escape sequence | meaning |
---|---|
\n | newline character |
\t | tab character |
\b | backspace character |
\f | form feed character |
\r | carriage return character |
\" | " (double quote) character |
\' | ' (single quote) character |
\\ | \ (back slash) character |
\uDDDD | character from the Unicode character set, by specifying the Unicode as four hex digits in the place of DDDD |
An example of using escape sequences to print some special characters.
System.out.println("First line\nSecond \"line\"");
First line
Second "line"
As the behavior of the \n
, the recommended way to print a line break is using the System.lineSeparator()
as it works the same in all platforms.
Using System.lineSeparator()
to print a line break.
System.out.println("First" + System.lineSeparator() + "Second");
First
Second
Sometimes programs need to create strings that are formatted in a certain way. String.format
takes a format specifier followed by a sequence of values and returns a new string formatted as specified.
The following method returns a time string in 12-hour format. The format specifier \%02d
means “two digit integer padded with zeros”, so timeString(19, 5)
returns the string "07:05 PM"
.
public static String timeString(int hour, int minute) {
String ampm;
if (hour < 12) {
ampm = "AM";
if (hour == 0) {
hour = 12; // midnight
}
} else {
ampm = "PM";
hour = hour - 12;
}
// returns "07:05 PM"
return String.format("%02d:%02d %s", hour, minute, ampm);
}
Implement the printPrice
method in the code below to produce the given output. Its behavior:
item
is a string in the format name--$price
i.e., a name and a price of an item separated using a --
e.g., banana--$3/50
NAME: price
where the name is in upper case. The price does not have a $
sign and has .
in place of the /
banana--$3/50
BANANA: 3.50
name
part of the input can have trailing/leading spaces which should be omitted from the output.
banana --$3/50
BANANA: 3.50
Do a Web search to find how to remove leading/trailing spaces. Suggested search terms java string remove leading trailing spaces
public class Main {
public static void printPrice(String item) {
// TODO: add your code here
}
public static void main(String[] args) {
printPrice("sandwich --$4/50");
printPrice(" soda --$10/00");
printPrice(" fries --$0/50");
}
}
SANDWICH: 4.50
SODA: 10.00
FRIES: 0.50
Hint
Primitive values (like int
, double
, and char
) do not provide methods.
For example, you can’t call equals
on an int
:
int i = 5;
System.out.println(i.equals(5)); // compiler error
But for each primitive type, there is a corresponding class in the Java library, called a wrapper class, as given in the table below. They are in the java.lang
package i.e., no need to import.
Primitive type | Wrapper class |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
Double d = new Double(2.5);
int i = d.intValue();
System.out.println(d);
System.out.println(i);
2.5
2
Each wrapper class defines constants MIN_VALUE
and MAX_VALUE
.
Accessing max and min values for integers:
System.out.println(Integer.MIN_VALUE + " : " + Integer.MAX_VALUE);
-2147483648 : 2147483647
Wrapper classes provide methods for strings to other types e.g., Integer.parseInt
converts a string to (you guessed it) an integer. The other wrapper classes provide similar methods, like Double.parseDouble
and Boolean.parseBoolean
.
Integer.parseInt("1234")
1234
Wrapper classes also provide toString
, which returns a string representation of a value.
Integer.toString(1234)
"1234"
Implement the printTotalScore
method in the code below to produce the given output. Its behavior:
values
is an array of strings, each string representing an integer e.g., ["5", "-1"]
["5", "-1"]
4
public class Main {
public static void printTotalScore(String[] values){
// TODO: add your code here
}
public static void main(String[] args) {
printTotalScore(new String[]{});
printTotalScore(new String[]{"0", "124", "-15"});
}
}
0
109
java.util.Arrays
provides methods for working with arrays. One of them, toString
, returns a string representation of an array. It also provides a copyOf
that copies an array.
Using Arrays.copyOf
and Arrays.toString
:
int[] a = new int[]{1,2,3,4};
int[] b = Arrays.copyOf(a, 3); // copy first three elements
System.out.println(Arrays.toString(b));
int[] c = Arrays.copyOf(a, a.length); // copy all elements
System.out.println(Arrays.toString(c));
[1, 2, 3]
[1, 2, 3, 4]
Implement the following two methods in the code below to produce the given output.
filterEmails(String[] items): String[]
items
is an array of strings each of which may be an email address or some other random stringString[]
containing email addresses that were in items
. Any string containing @
is considered as an email.["aaa@bbb", "xyz"]
["aaa@bbb"]
printItems(String[] items)
items
in the standard array format. e.g., ["aaa", "bbb"]
[aaa, bbb]
import java.util.Arrays;
public class Main {
public static String[] filterEmails(String[] items){
// TODO: add your code here
}
public static void printItems(String[] items){
// TODO: add your code here
}
public static void main(String[] args) {
printItems(filterEmails(new String[]{}));
printItems(filterEmails(new String[]{"abc"}));
printItems(filterEmails(new String[]{"adam@example.com", "aab", "john@example.com", "some@"}));
printItems(filterEmails(new String[]{"xyz", "@bee.com", "aab"}));
}
}
[]
[]
[adam@example.com, john@example.com, some@]
[@bee.com]
Hint
Scanner
is a class that provides methods for inputting words, numbers, and other data. Scanner
provides a method called nextLine
that reads a line of input from the keyboard and returns a String. The following example reads two lines and repeats them back to the user:
import java.util.Scanner;
public class Echo {
public static void main(String[] args) {
String line;
Scanner in = new Scanner(System.in);
System.out.print("Type something: ");
line = in.nextLine();
System.out.println("You said: " + line);
System.out.print("Type something else: ");
line = in.nextLine();
System.out.println("You also said: " + line);
}
}
Scanner
class normally reads inputs as strings but it can read in a specific type of input too.
The code below uses the nextInt
method of the Scanner
class to read an input as an integer.
Scanner in = new Scanner(System.in);
System.out.print("What is your age? ");
int age = in.nextInt();
in.nextLine(); // read the new-line character that follows the integer
System.out.print("What is your name? ");
String name = in.nextLine();
System.out.printf("Hello %s, age %d\n", name, age);
Write a program to ask the user for a description of overseas expenses (presumably, the user has just returned from an overseas trip) and calculate the total in local currency.
$1.0
= local $1.70
$amount
e.g., $1.50
Here is one example output:
Your expenses while overseas?beer $4.50 books $3.00 $5.00 for food, that's all
Expenses in overseas currency:[$4.50, $3.00, $5.00]
Total in local currency: $21.25
Here is another:
Your expenses while overseas?nothing. I lived off my friends all the time.
Expenses in overseas currency:[]
Total in local currency: $0.00
One more:
Your expenses while overseas? Just $10
Expenses in overseas currency:[$10]
Total in local currency: $17.00
Here's the skeleton code to use as the starting point:
public class Main {
// You can add more methods here
public static void main(String[] args) {
String line;
Scanner in = new Scanner(System.in);
System.out.print("Your expenses while overseas?");
// TODO: add your code here
}
}
You can use the split
method of the String
class to convert a sentence into an array of words. e.g.,
String sentence = "hello my dear";
// split using the space as the delimiter
String[] words = sentence.split(" ");
System.out.println(Arrays.toString(words));
[hello, my, dear]
Hint
Guidance for the item(s) below:
Previously, you learned about coding standards as a way to improve code quality. The next topic along that theme covers guidelines for naming things in code.
Proper naming improves the readability of code. It also reduces bugs caused by ambiguities regarding the intent of a variable or a method.
There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton
Every system is built from a domain-specific language designed by the programmers to describe that system. Functions are the verbs of that language, and classes are the nouns.
-- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
Use nouns for classes/variables and verbs for methods/functions.
Name for a | Bad | Good |
---|---|---|
Class | CheckLimit | LimitChecker |
Method | result() | calculate() |
Distinguish clearly between single-valued and multi-valued variables.
Good
Person student;
ArrayList<Person> students;
Good
name = 'Jim'
names = ['Jim', 'Alice']
Use correct spelling in names. Avoid 'texting-style' spelling. Avoid foreign language words, slang, and names that are only meaningful within specific contexts/times e.g., terms from private jokes, a TV show currently popular in your country.
A name is not just for differentiation; it should explain the named entity to the reader accurately and at a sufficient level of detail.
Bad | Good |
---|---|
processInput() (what 'process'?) | removeWhiteSpaceFromInput() |
flag | isValidInput |
temp |
If a name has multiple words, they should be in a sensible order.
Bad | Good |
---|---|
bySizeOrder() | orderBySize() |
Imagine going to the doctor's and saying "My eye1 is swollen"! Don’t use numbers or case to distinguish names.
Bad | Bad | Good |
---|---|---|
value1 , value2 | value , Value | originalValue , finalValue |
Related things should be named similarly, while unrelated things should NOT.
Example: Consider these variables
colorBlack
: hex value for color blackcolorWhite
: hex value for color whitecolorBlue
: number of times blue is usedhexForRed
: hex value for color redThis is misleading because colorBlue
is named similar to colorWhite
and colorBlack
but has a different purpose while hexForRed
is named differently but has a very similar purpose to the first two variables. The following is better:
hexForBlack
hexForWhite
hexForRed
blueColorCount
Avoid misleading or ambiguous names (e.g., those with multiple meanings), similar sounding names, hard-to-pronounce ones (e.g., avoid ambiguities like "is that a lowercase L, capital I or number 1?", or "is that number 0 or letter O?"), almost similar names.
Bad | Good | Reason |
---|---|---|
phase0 | phaseZero | Is that zero or letter O? |
rwrLgtDirn | rowerLegitDirection | Hard to pronounce |
right left wrong | rightDirection leftDirection wrongResponse | right is for 'correct' or 'opposite of 'left'? |
redBooks readBooks | redColorBooks booksRead | red and read (past tense) sounds the same |
FiletMignon | egg | If the requirement is just a name of a food, egg is a much easier to type/say choice than FiletMignon |
Guidance for the item(s) below:
Previously, you learned how to save revision history in your local repository, in the form of commits. Next, let us use how to make use of that history.
Target Usage: To make use of the revision history stored by Git.
Motivation: Having put in effort to record the revision history of the working folder, it only makes sense that we use the revision history to our benefit. For example, to be able to answer questions such as "What did I change in this file since last Monday?"
Lesson plan:
T4L1. Examining a Commit covers that part.
T4L2. Tagging Commits covers that part.
T4L3. Comparing Points of History covers that part.
T4L4. Traversing to a Specific Commit covers that part.
T4L5. Rewriting History to Start Over covers that part.
T4L6. Reverting a Specific Commit covers that part.
It is useful to be able to see what changes were included in a specific commit.
When you examine a commit, normally what you see is the 'changes made since the previous commit'. This does not mean that a Git commit contains only the changes made since the previous commit. As you recall, a Git commit contains a full snapshot of the working directory. However, tools used to examine commits typically show only the changes, as that is the more informative part.
Git shows changes included in a commit by dynamically calculating the difference between the snapshots stored in the target commit and the parent commit. This is because Git commits store snapshots of the working directory, not changes themselves.
To address a specific commit, you can use its SHA (e.g., e60deaeb2964bf2ebc907b7416efc890c9d4914b
). In fact, just the first few characters of the SHA is enough to uniquely address a commit (e.g., e60deae
), provided the partial SHA is long enough to uniquely identify the commit (i.e., only one commit has that partial SHA).
Naturally, a commit can be addressed using any ref pointing to it too (e.g., HEAD
, master
).
Another related technique is to use the <ref>~<n>
notation (e.g., HEAD~1
) to address the commit that is n
commits prior to the commit pointed by <ref>
i.e., "start with the commit pointed by <ref>
and go back n
commits".
A related alternative notation is HEAD~
, HEAD~~
, HEAD~~~
, ... to mean HEAD~1
, HEAD~2
, HEAD~3
etc.
HEAD
or master
HEAD~1
or master~1
or HEAD~
or master~
HEAD~2
or master~2
Git uses the diff format to show file changes in a commit. The diff format was originally developed for Unix. It was later extended with headers and metadata to show changes between file versions and commits. Here is an example diff showing the changes to a file.
diff --git a/fruits.txt b/fruits.txt
index 7d0a594..f84d1c9 100644
--- a/fruits.txt
+++ b/fruits.txt
@@ -1,6 +1,6 @@
-apples
+apples, apricots
bananas
cherries
dragon fruits
-elderberries
figs
@@ -20,2 +20,3 @@
oranges
+pears
raisins
diff --git a/colours.txt b/colours.txt
new file mode 100644
index 0000000..55c8449
--- /dev/null
+++ b/colours.txt
@@ -0,0 +1 @@
+a file for colours
A Git diff can consist of multiple file diffs, one for each changed file. Each file diff can contain one or more hunk i.e., a localised group of changes within the file — including lines added, removed, or left unchanged (included for context).
Given below is how the above diff is divided into its components:
File diff for fruits.txt
:
diff --git a/fruits.txt b/fruits.txt
index 7d0a594..f84d1c9 100644
--- a/fruits.txt
+++ b/fruits.txt
Hunk 1:
@@ -1,6 +1,6 @@
-apples
+apples, apricots
bananas
cherries
dragon fruits
-elderberries
figs
Hunk 2:
@@ -20,2 +20,3 @@
oranges
+pears
raisins
File diff for colours.txt
:
diff --git a/colours.txt b/colours.txt
new file mode 100644
index 0000000..55c8449
--- /dev/null
+++ b/colours.txt
Hunk 1:
@@ -0,0 +1 @@
+a file for colours
Here is an explanation of the diff:
Part of Diff | Explanation |
---|---|
diff --git a/fruits.txt b/fruits.txt | The diff header, indicating that it is comparing the file fruits.txt between two versions: the old (a/ ) and new (b/ ). |
index 7d0a594..f84d1c9 100644 | Shows the before and after the change, and the file mode (100 means a regular file, 644 are file permission indicators). |
--- a/fruits.txt +++ b/fruits.txt | Marks the old version of the file (a/fruits.txt ) and the new version of the file (b/fruits.txt ). |
@@ -1,6 +1,6 @@ | This hunk header shows that lines 1-6 (i.e., starting at line 1 , showing 6 lines) in the old file were compared with lines 1–6 in the new file. |
-apples +apples, apricots | Removed line apples and added line apples, apricots . |
bananas cherries dragon fruits | Unchanged lines, shown for context. |
-elderberries | Removed line: elderberries . |
figs | Unchanged line, shown for context. |
@@ -20,2 +20,3 @@ | Hunk header showing that lines 20-21 in the old file were compared with lines 20–22 in the new file. |
oranges +pears raisins | Unchanged line. Added line: pears .Unchanged line. |
diff --git a/colours.txt b/colours.txt | The usual diff header, indicates that Git is comparing two versions of the file colours.txt : one before and one after the change. |
new file mode 100644 | This is a new file being added. 100644 means it’s a normal, non-executable file with standard read/write permissions. |
index 0000000..55c8449 | The usual SHA hashes for the two versions of the file. 0000000 indicates the file did not exist before. |
--- /dev/null +++ b/colours.txt | Refers to the "old" version of the file (/dev/null means it didn’t exist before), and the new version. |
@@ -0,0 +1 @@ | Hunk header, saying: “0 lines in the old file were replaced with 1 line in the new file, starting at line 1.” |
+a file for colours | Added line |
Points to note:
+
indicates a line being added.-
indicates a line being deleted.TargetView contents of specific commits in a repo.
Preparation You can use any repo that has commits e.g., the things
repo.
1 Locate the commits to view, using the revision graph.
git log --oneline --decorate
e60deae (HEAD -> master, origin/master) Update fruits list
f761ea6 Add colours.txt, shapes.txt
2bedace Add figs to fruits.txt
d5f91de Add fruits.txt
2 Use the git show
command to view specific commits.
git show # shows the latest commit
commit e60deaeb2964bf2ebc907b7416efc890c9d4914b (HEAD -> master, origin/master)
Author: damithc <...@...>
Date: Sat Jun ...
Update fruits list
diff --git a/fruits.txt b/fruits.txt
index 7d0a594..6d502c3 100644
--- a/fruits.txt
+++ b/fruits.txt
@@ -1,6 +1,6 @@
-apples
+apples, apricots
bananas
+blueberries
cherries
dragon fruits
-elderberries
figs
To view the parent commit of the latest commit, you can use any of these commands:
git show HEAD~1
git show master~1
git show e60deae # first few characters of the SHA
git show e60deae..... # run git log to find the full SHA and specify the full SHA
To view the commit that is two commits before the latest commit, you can use git show HEAD~2
etc.
Click on the commit. The remaining panels (indicated in the image below) will be populated with the details of the commit.
done!
PRO-TIP: Use Git Aliases to Work Faster
The Git alias feature allows you to create custom shortcuts for frequently used Git commands. This saves time and reduces typing, especially for long or complex commands. Once an alias is defined, you can use the alias just like any other Git command e.g., use git lodg
as an alias for git log --oneline --decorate --graph
.
To define a global git alias, you can use the git config --global alias.<alias> "command"
command. e.g.,
git config --global alias.lodg "log --oneline --graph --decorate"
You can also create shell-level aliases using your shell configuration (e.g., .bashrc
, .zshrc
) to make even shorter aliases. This lets you create shortcuts for any command, including Git commands, and even combine them with other tools. e.g., instead of the Git alias git lodg
, you can define a shorter shell-level alias glodg
.
1. Locate your .bash_profile
file (likely to be in : C:\Users\<YourName>\.bash_profile
-- if it doesn’t exist, create it.)
1. Locate your shell's config file e.g., .bashrc
or .zshrc
(likely to be in your ~
folder)
1. Locate your shell's config file e.g., .bashrc
or .zshrc
(likely to be in your ~
folder)
Oh-My-Zsh for Zsh terminal supports a Git plugin that adds a wide array of Git command aliases to your terminal.
2. Add aliases to that file:
alias gs='git status'
alias glod='git log --oneline --graph --decorate'
3. Apply changes by running the command source ~/.zshrc
or source ~/.bash_profile
or source ~/.bashrc
, depending on which file you put the aliases in.
When working with many commits, it helps to tag specific commits with custom names so they’re easier to refer to later.
Git lets you tag commits with names, making them easy to reference later. This is useful when you want to mark specific commits -- such as releases or key milestones (e.g., v1.0
or v2.1
). Using tags to refer to commits is much more convenient than using SHA hashes. In the diagram below, v1.0 and interim are tags.
A tag stays fixed to the commit. Unlike branch refs or HEAD
, tags do not move automatically as new commits are made. As you see below, after adding a new commit, tags stay in the previous commits while master←HEAD has moved to the new commit.
Git supports two kinds of tags:
Annotated tags are generally preferred for versioning and public releases, while lightweight tags are often used for less formal purposes, such as marking a commit for your own reference.
Target Add a few tags to a repository.
Preparation Fork and clone the samplerepo-preferences. Use the cloned repo on your computer for the following steps.
1 Add a lightweight tag to the current commit as v1.0
:
git tag v1.0
2 Verify the tag was added. To view tags:
git tag
v1.0
To view tags in the context of the revision graph:
git log --oneline --decorate
507bb74 (HEAD -> master, tag: v1.0, origin/master, origin/HEAD) Add donuts
de97f08 Add cake
5e6733a Add bananas
3398df7 Add food.txt
3 Use the tag to refer to the commit e.g., git show v1.0
should show the changes in the tagged commit.
4 Add an annotated tag to an earlier commit. The example below adds a tag v0.9
to the commit HEAD~2
with the message First beta release
. The -a
switch tells Git this is an annotated tag.
git tag -a v0.9 HEAD~2 -m "First beta release"
5 Check the new annotated tag. While both types of tags appear similarly in the revision graph, the show
command on an annotated tag will show the details of the tag and the details of the commit it points to.
git show v0.9
tag v0.9
Tagger: ... <...@...>
Date: Sun Jun ...
First beta release
commit ....999087124af... (tag: v0.9)
Author: ... <...@...>
Date: Sat Jun ...
Add figs to fruits.txt
diff --git a/fruits.txt b/fruits.txt
index a8a0a01..7d0a594 100644
# rest of the diff goes here
Right-click on the commit (in the graphical revision graph) you want to tag and choose Tag…
.
Specify the tag name e.g., v1.0
and click Add Tag
.
Configure tag properties in the next dialog and press Add
. For example, you can choose whether to make it a lightweight tag or an annotated tag (default).
Tags will appear as labels in the revision graph, as seen below. To see the details of an annotated tag, you need to use the menu indicated in the screenshot.
done!
If you need to change what a tag points to, you must delete the old one and create a new tag with the same name. This is because tags are designed to be fixed references to a specific commit, and there is no built-in mechanism to 'move' a tag.
Preparation Continue with the same repo you used for the previous hands-on practical.
Move the v1.0
tag to the commit HEAD~1
, by deleting it first and creating it again at the destination commit.
Delete the previous v1.0
tag by using the -d
. Add it again to the other commit, as before.
git tag -d v1.0
git tag v1.0 HEAD~1
The same dialog used to add a tag can be used to delete and even move a tag. Note that the 'moving' here translates to deleting and re-adding behind the scene.
done!
Tags are different from commit messages, in purpose and in form. A commit message is a description of the commit that is part of the commit itself. A tag is a short name for a commit, which you can use to address a commit.
Pushing commits to a remote does not push tags automatically. You need to push tags specifically.
Target Push tags you created earlier to the remote.
Preparation Continue with the same repo you used for the previous hands-on practical.
You can go to your remote on GitHub link https://github.com/{USER}/{REPO}/tags
(e.g., https://github.com/johndoe/samplerepo-preferences/tags
) to verify the tag is present there.
Note how GitHub assumes these tags are meant as releases, and automatically provides zip and tar.gz archives of the repo (as at that tag).
1 Push a specific tag in the local repo to the remote (e.g., v1.0
) using the git push <origin> <tag-name>
command.
git push origin v1.0
In addition to verifying the tag's presence via GitHub, you can also use the following command to list the tags presently in the remote.
git ls-remote --tags origin
2 Delete a tag in the remote, using the git push --delete <remote> <tag-name>
command.
git push --delete origin v1.0
3 Push all tags to the remote repo, using the git push <remote> --tags
command.
git push origin --tags
To push a specific tag, use the following menu:
To push all tags, you can tick the Push all tags
option when pushing commits:
done!
Git can tell you the net effect of changes between two points of history.
Git's diff feature can show you what changed between two points in the revision history. Given below are some use cases.
Usage 1: Examining changes in the working directory
Example use case: To verify the next commit will include exactly what you intend it to include.
Preparation For this, you can use the things
repo you created earlier. If you don't have it, you can clone a copy of a similar repo given here.
1 Do some changes to the working directory. Stage some (but not all) changes. For example, you can run the following commands.
echo -e "blue\nred\ngreen" >> colours.txt
git add . # a shortcut to stage all changes
echo "no shapes added yet" >> shapes.txt
2 Examine the staged and unstaged changes.
The git diff
command shows unstaged changes in the working directory (tracked files only). The output of the diff
command, is a diff view (introduced in this lesson).
git diff
diff --git a/shapes.txt b/shapes.txt
index 5c2644b..949c676 100644
--- a/shapes.txt
+++ b/shapes.txt
@@ -1 +1,2 @@
a file for shapes
+no shapes added yet!
The git diff --staged
command shows the staged changes (same as git diff --cached
).
git diff --staged
Select the two commits: Click on one commit, and Ctrl-Click (or Cmd-Click) on the second commit. The changes between the two selected commits will appear in the other panels, as shown below:
done!
Usage 2: Comparing two commits at different points of the revision graph
Example use case: Suppose you’re trying to improve the performance of a piece of software by experimenting with different code tweaks. You commit after each change (as you should). After several commits, you now want to review the overall effect of all those changes on the code.
Target Compare two commits in a repo.
Preparation You can use any repo with multiple commits e.g., the things
repo.
You can use the git diff <commit1> <commit2>
command for this.
..
notation to specify the commit range too e.g., 0023cdd..fcd6199
, HEAD~2..HEAD
git diff v0.9 HEAD
diff --git a/colours.txt b/colours.txt
new file mode 100644
index 0000000..55c8449
--- /dev/null
+++ b/colours.txt
@@ -0,0 +1 @@
+a file for colours
# rest of the diff ...
Swap the commit order in the command and see what happens.
git diff HEAD v0.9
diff --git a/colours.txt b/colours.txt
deleted file mode 100644
index 55c8449..0000000
--- a/colours.txt
+++ /dev/null
@@ -1 +0,0 @@
-a file for colours
# rest of the diff ...
As you can see, the diff
is directional i.e., diff <commit1> <commit2>
shows what changes you need to do to go from the <commit1>
to <commit2>
. If you swap <commit1>
and <commit2>
, the output will change accordingly e.g., lines previously shown as 'added' will now be shown as 'deleted'.
Select the two commits: Click on one commit, and Ctrl-Click (or Cmd-Click) on the second commit. The changes between the two selected commits will appear in the other panels, as shown below:
The same method can be used to compare the current state of the working directory (which might have uncommitted changes) to a point in the history.
done!
Usage 3: Examining changes to a specific file
Example use case: Similar to other use cases but when you are interested in a specific file only.
Target Examine the changes done to a file between two different points in the version history (including the working directory).
Preparation Use any repo with multiple commits e.g. the things
repo.
Add the -- path/to/file
to a previous diff command to narrow the output to a specific file. Some examples:
git diff -- fruits.txt # unstaged changes to fruits.txt
git diff --staged -- src/main.java # staged changes to src/main.java
git diff HEAD~2..HEAD -- fruits.txt # changes to fruits.txt between commits
Sourcetree UI shows changes to one file at a time by default; just click on the file to view changes to that file. To view changes to multiple files, Ctrl-Click (or Cmd-Click) on multiple files to select them.
done!
Another useful feature of revision control is to be able to view the working directory as it was at a specific point in history, by checking out a commit created at that point.
Suppose you added a new feature to a software product, and while testing it, you noticed that another feature added two commits ago doesn’t handle a certain edge case correctly. Now you’re wondering: did the new feature break the old one, or was it already broken? Can you go back to the moment you committed the old feature and test it in isolation, and come back to the present after you found the answer? With Git, you can.
To view the working directory at a specific point in history, you can check out a commit created at that point.
When you check out a commit, Git:
HEAD
ref to that commit, marking it as the current state you’re viewing.→
[check out commit C2
...]
Checking out a specific commit puts you in a "detached HEAD
" state: i.e., the HEAD
no longer points to a branch, but directly to a commit (see the above diagram for an example). This isn't a problem by itself, but any commits you make in this state can be lost, unless certain follow-up actions are taken. It is perfectly fine to be in a detached state if you are only examining the state of the working directory at that commit.
To get out of a "detached HEAD" state, you can simply check out a branch, which "re-attaches" HEAD
to the branch you checked out.
→
[check out master
...]
Target Checkout a few commits in a local repo, while examining the working directory to verify that it matches the state when you created the corresponding commit
Preparation Use any repo with commits e.g., the things
repo
1 Examine the revision tree, to get your bearing first.
git log --oneline --decorate
Reminder: You can use aliases to reduce typing Git commands.
e60deae (HEAD -> master, origin/master) Update fruits list
f761ea6 (tag: v1.0) Add colours.txt, shapes.txt
2bedace (tag: v0.9) Add figs to fruits.txt
d5f91de Add fruits.txt
2 Use the checkout <commit-identifier>
command to check out a commit other than the one currently pointed by HEAD
. You can use any of the following methods:
git checkout v1.0
: checks out the commit tagged v1.0
git checkout 0023cdd
: checks out the commit with the hash 0023cdd
git checkout HEAD~2
: checks out the commit 2 commits behind the most recent commit.git checkout HEAD~2
Note: switching to 'HEAD~2'.
You are in 'detached HEAD' state.
# rest of the warning about the detached head ...
HEAD is now at 2bedace Add figs to fruits.txt
3 Verify HEAD
and the working directory have updated as expected.
HEAD
should now be pointing at the target commitshapes.txt
should not be in the folder).git log --oneline --decorate
2bedace (HEAD, tag: v0.9) Add figs to fruits.txt
d5f91de Add fruits.txt
HEAD
is indeed pointing at the target commit.
But note how the output does not show commits you added after the checked-out commit.
The --all
switch tells git log
to show commits from all refs, not just those reachable from the current HEAD
. This includes commits from other branches, tags, and remotes.
git log --oneline --decorate --all
e60deae (origin/master, master) Update fruits list
f761ea6 (tag: v1.0) Add colours.txt, shapes.txt
2bedace (HEAD, tag: v0.9) Add figs to fruits.txt
d5f91de Add fruits.txt
4 Go back to the latest commit by checking out the master
branch again.
git checkout master
In the revision graph, double-click the commit you want to check out, or right-click on that commit and choose Checkout...
.
Click OK
to the warning about ‘detached HEAD’ (similar to below).
The specified commit is now loaded onto the working folder, as indicated by the HEAD
label.
To go back to the latest commit on the master
branch, double-click the master
branch.
If you check out a commit that comes before the commit in which you added a certain file (e.g., temp.txt
) to the .gitignore
file, and if the .gitignore
file is version controlled as well, Git will now show it under ‘unstaged modifications’ because at Git hasn’t been told to ignore that file yet.
done!
If there are uncommitted changes in the working directory, Git proceeds with a checkout only if it can preserve those changes.
The Git stash feature temporarily sets aside uncommitted changes you’ve made (in your working directory and staging area), without committing them. This is useful when you’re in the middle of some work, but need to switch to another state (e.g., checkout a previous commit), and your current changes are not yet ready to be committed or discarded. You can later reapply the stashed changes when you’re ready to resume that work.
DETOUR: Stashing Uncommitted Changes Temporarily
For basic usage, you can use the following two commands:
git stash
: Stash staged and unstaged changesgit stash pop
: Reapplies the latest stashed changes and removes it from the stash list.RESOURCES
A more detailed explanation of stashing: https://www.atlassian.com/git/tutorials/saving-changes/git-stash
A video explanation:
DETOUR: Dealing with Uncommitted Conflicting Changes at a Checkout
To proceed with a checkout when there are conflicting uncommitted changes in the working directory, there are several options:
Git can also reset the revision history to a specific point so that you can start over from that point.
Suppose you realise your last few commits have gone in the wrong direction, and you want to go back to an earlier commit and continue from there — as if the “bad” commits never happened. Git’s reset feature can help you do that.
Git reset moves the tip of the current branch to a specific commit, optionally adjusting your staged and unstaged changes to match. This effectively rewrites the branch's history by discarding any commits that came after that point.
Resetting is different from the checkout feature:
HEAD
ref.→
[reset to C2
...]
master
branch!There are three types of resets: soft, mixed, hard. All three move the branch pointer to a new commit, but they vary based on what happens to the staging area and the working directory.
Preparation First, set the stage as follows (e.g., in the things
repo):
i) Add four commits that are supposedly 'bad' commits.
ii) Do a 'bad' change to one file and stage it.
iii) Do a 'bad' change to another file, but don't stage it.
The following commands can be used to add commits B1
-B4
:
echo "bad colour" >> colours.txt
git commit -am "Incorrectly update colours.txt"
echo "bad shape" >> shapes.txt
git commit -am "Incorrectly update shapes.txt"
echo "bad fruit" >> fruits.txt
git commit -am "Incorrectly update fruits.txt"
echo "bad line" >> incorrect.txt
git add incorrect.txt
git commit -m "Add incorrect.txt"
echo "another bad colour" >> colours.txt
git add colours.txt
echo "another bad shape" >> shapes.txt
Now we have some 'bad' commits and some 'bad' changes in both the staging area and the working directory. Let's use the reset feature to get rid of all of them, but do it in three steps so that you can learn all three types of resets.
1 Do a soft reset to B2
(i.e., discard last two commits). Verify,
master
branch is now pointing at B2
, and,B3
and B4
) are now in the staging area.Use the git reset --soft <commit>
command to do a soft reset.
git reset --soft HEAD~2
You can run the following commands to verify the current status of the repo is as expected.
git status # check overall status
git log --oneline --decorate # check the branch tip
git diff # check unstaged changes
git diff --staged # check staged changes
Right-click on the commit that you want to reset to, and choose Reset <branch-name> to this commit
option.
In the next dialog, choose Soft - keep all local changes
.
2 Do a mixed reset to commit B1
. Verify,
master
branch is now pointing at B1
.incorrect.txt
appears as an 'untracked' file -- this is because unstaging a change of type 'add file' results in an untracked file.Use the git reset --mixed <commit>
command to do a mixed reset. The --mixed
flag is the default, and can be omitted.
git reset HEAD~1
Verify the repo status, as before.
Similar to the previous reset, but choose the Mixed - keep working copy but reset index
option in the reset dialog.
3 Do a hard reset to commit C4
. Verify,
master
branch is now pointing at C4
i.e., all 'bad' commits are gone.incorrect.txt
-- Git leaves untracked files alone, as untracked files are not meant to be under Git's control).Use the git reset --hard <commit>
command.
git reset --hard HEAD~1
Verify the repo status, as before.
Similar to the previous reset, but choose the Hard - discard all working copy changes
option.
done!
Rewriting history can cause your local repo to diverge from its remote counterpart. For example, if you discard earlier commits and create new ones in their place, and you’ve already pushed the original commits to a remote repository, your local branch history will no longer match the corresponding remote branch. Git refers to this as a diverged history.
To protect the integrity of the remote, Git will reject attempts to push a diverged branch using a normal push. If you want to overwrite the remote history with your local version, you must perform a force push.
Preparation Choose a local-remote repo pair under your control e.g., the things
repo from Tour 2: Backing up a Repo on the Cloud.
1 Rewrite the last commit: Reset the current branch back by one commit, and add a new commit.
For example, you can use the following commands.
git reset --hard HEAD~1
echo "water" >> drinks.txt
git add .
git commit -m "Add drinks.txt"
2 Observe how the local branch is diverged.
git log --oneline --graph --all
* fc1d04e (HEAD -> master) Add drinks.txt
| * e60deae (upstream/master, origin/master) Update fruits list
|/
* f761ea6 (tag: v1.0) Add colours.txt, shapes.txt
* 2bedace (tag: v0.9) Add figs to fruits.txt
* d5f91de Add fruits.txt
3 Attempt to push to the remote. Observe Git rejects the push.
git push origin master
To https://github.com/.../things.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'https://github.com/.../things.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: ...
4 Do a force-push.
You can use the --force
(or -f
) flag to force push.
git push -f origin master
A safer alternative to --force
is --force-with-lease
which overwrites the remote branch only if it hasn’t changed since you last fetched it (i.e., only if remote doesn't have recent changes that you are unaware of):
git push --force-with-lease origin master
done!
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.
To get rid of uncommitted changes, you can reset the repo to the last commit (i.e., HEAD
):
The command git reset
(without specifying a commit) defaults to git reset HEAD
.
git reset
: moves any staged changes to working directory (i.e., unstage).
git reset --hard
: get rid of any staged and unstaged 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 a detour in the lesson T5L3. Reorganising Commits.
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?
Undoing or deleting recent n
commits is easily accomplished with Git's reset
feature.
n
commits and discard the those changes entirely, do a hard
reset the commit HEAD~n
e.g.,git reset --hard HEAD~3
n
commits, but keep changes staged, do a soft
reset the commit HEAD~n
e.g.,git reset --soft HEAD~3
n
commits, and move changes to the working directory, do a mixed
reset the commit HEAD~n
e.g.,git reset --mixed HEAD~3
To do the above for the most recent commit only, use HEAD~1
(or just HEAD~
).
DETOUR: Resetting a Remote-Tracking Branch Ref
Suppose you moved back the current branch ref by two commits, as follows:
git reset --hard HEAD~2
→
If you now wish to move back the remote-tracking branch ref by two commits, so that the local repo 'forgets' that it previously pushed two more commits to the remote, you can do:
git update-ref refs/remotes/origin/master HEAD
→
The git update-ref refs/remotes/origin/master HEAD
commands resets the remote-tracking branch ref origin/master
to follow the current HEAD
.
update-ref
is an example of what are known as Git plumbing commands -- lower-level commands used by Git internally. In contrast, day-to-day Git commands (such as commit
, log
, push
etc.) are known as porcelain commands (as in, in bathrooms we see the porcelain parts but not the plumbing parts that operates below the surface to make everything work).
Git can add a new commit to reverse the changes done in a specific past commit, called reverting a commit.
When a past commit introduced a bug or an unwanted change, but you do not want to modify that commit — because rewriting history can cause problems if others have already based work on it — you can instead revert that commit.
Reverting creates a new commit that cancels out the changes of the earlier one i.e., Git computes the opposite of the changes introduced by that commit — essentially a reverse diff — and applies it as a new commit on top of the current branch. This way, the problematic changes are reversed while preserving the full history, including the "bad" commit and the "fix".
→
[revert C2
]
C2
Preparation Run the following commands to create a repo with a few commits:
mkdir pioneers
cd pioneers
git init
echo "hacked the matrix" >> neo.txt
git add .
git commit -m "Add Neo"
echo "father of theoretical computing" >> alan-turing.txt
git add .
git commit -m "Add Turing"
echo "created COBOL, compiler pioneer" >> grace-hopper.txt
git add .
git commit -m "Add Hopper"
1 Revert the commit Add Neo
.
You can use the git revert <commit>
command to revert a commit. In this case, we want to revert the commit that is two commits behind the HEAD
.
git revert HEAD~2
What happens next:
In the revision graph, right-click on the commit you want to revert, and choose Reverse commit...
done!
A revert can result in a conflict, if the new changes done to reverse the previous commit conflict with the changes done in other more recent commits. Then, you need to resolve the conflict before the revert operation can proceed. Conflict resolution is covered in a later topic.
At this point: You should now be able to use a repository’s revision history to understand how the working directory evolved over time -- and use that insight to inform your work.
How useful this history is depends greatly on how well it was constructed -- for example, how frequently and meaningfully commits were made -- we’ll explore that in a later tour.
What's next: Tour 5: Fine-Tuning the Revision History
Target Usage: To maintain a clean and meaningful revision history.
Motivation: The usefulness of the revision history increases if it consists of well-crafted and well-documented commits.
Lesson plan:
T5L1. Controlling What Goes Into a Commit covers that part.
T5L2. Writing Good Commit Messages covers that part.
T5L3. Reorganising Commits covers that part.
To create well-crafted commits, you need to know how to control which precise changes go into a commit.
Crafting a commit involves two aspects:
SIDEBAR: Guidelines on what to include in a commit
A good commit represents a single, logical unit of change — something that can be described clearly in one sentence. For example, fixing a specific bug, adding a specific feature, or refactoring a specific function. If each commit tells a clear story about why the change was made and what it achieves, your repository history becomes a valuable narrative of the project’s development. Here are some (non-exhaustive) guidelines:
Git can let you choose not just which files, but which specific changes within those files, to include in a commit. Most Git tools — including the command line and many GUIs — let you interactively select which "hunks" or even individual lines of a file to stage. This allows you to separate unrelated changes and avoid committing unnecessary edits. If you make multiple changes in the same file, you can selectively stage only the parts that belong to the current logical change.
This level of control is particularly useful when:
Preparation You can use any repo for this.
1 Do several changes to some tracked files. Change multiple files. Also change multiple locations in the same file.
2 Stage some changes in some files while keeping other changes in the same files unstaged.
As you know, you can use git add <filename>
to stage changes to an entire file.
To select which hunks to stage, you can use the git add -p
command instead (-p
stands for 'by patch'):
git add -p
This command will take you to an interactive mode in which you can go through each hunk and decide if you want to stage it. The video below contains a demonstration of how this feature works:
To stage a hunk, you can click the Stage
button above the hunk in question:
Most git operations can be done faster through the CLI than equivalent Git GUI clients, once you are familiar enough with the CLI commands.
However, selective staging is one exception where a good GUI can do better than the CLI, if you need to do many fine-grained staging operations (e.g., frequently staging only parts of hunks).
done!
Detailed and well-written commit messages can increase the value of Git revision history.
Every commit you make in Git also includes a commit message that explains the change. While one-line messages are fine for small or obvious changes, as your revision history grows, good commit messages become an important source of information — for example, to understand the rationale behind a specific change made in the past.
A commit message is meant to explain the intent behind the changes, not just what was changed. The code (or diff) already shows what changed. Well-written commit messages make collaboration, code reviews, debugging, and future maintenance easier by helping you and others quickly understand the project’s history without digging into the code of every commit.
A complete commit message can include a short summary line (the subject) followed by a more detailed body if needed. The subject line should be a concise description of the change, while the body can elaborate on the context, rationale, side effects, or other details if the change is more complex.
A commit message has the following structure (note how the subject and the body are separated by a blank line):
Subject line
<blank line>
Body
# lines starting with '#' are ignored (they will not be included in the commit message)
Here is an example commit message:
Find command: make matching case-insensitive
Find command is case-sensitive.
A case-insensitive find is more user-friendly because users cannot be
expected to remember the exact case of the keywords.
Let's,
* update the search algorithm to use case-insensitive matching
* add a script to migrate stress tests to the new format
Do some changes to a repo you have.
Commit the changes while writing a full commit message (i.e., subject + body).
When you are ready to commit, use the git commit
command (without specifying a commit message).
git commit
This will open your default text editor (like Vim, Nano, or VS Code). Write the commit message inside the editor.
Save and close the editor to create the commit.
You can write your full commit message in the textbox you have been using to write commit messages already.
done!
Following a style guide makes your commit messages more consistent and fit-for-purpose. Many teams adopt established guidelines. These style guides typically contain common conventions that Git users follow when writing commit messages. For example:
Fix typo in README
rather than Fixed typo
or Fixes typo
).PRO-TIP: Configure Git to use your preferred text editor
Git will use the default text editor when it needs you to write a commit message. However, Git can be configured to use a different text editor of your choice.
You can use the following command to set the Git's default text editor:
git config --global core.editor "<editor command>"
Some examples for <editor command>
Editor | Command to use |
---|---|
Vim (default) | vim |
Nano | nano |
VS Code | code --wait e.g., git config --global core.editor "code --wait" For this to work, your computer should already be configured to launch VS Code using the code command. See here to find how (refer the 'Launching from command line' section). |
Sublime Text | subl -n -w |
Atom | atom --wait |
Notepad++ | notepad++.exe (Windows only) |
Notepad | notepad (Windows built-in) |
Why use --wait
or -w
? Graphical editors (like VS Code or Sublime) start a separate process, which can take a few seconds. Without --wait
, Git may think editing is done before you actually write the message. --wait
makes Git pause until the editor window is closed.
RESOURCES
When the revision history gets 'messy', Git has a way to 'tidy up' the recent commits.
Git has a powerful tool called interactive rebasing which lets you review and reorganise your recent commits. With it, you can reword commit messages, change their order, delete commits, combine several commits into one (squash), or split a commit into smaller pieces. This feature is useful for tidying up a commit history that has become messy — for example, when some commits are out of order, poorly described, or include changes that would be clearer if split up or combined.
Preparation Run the following commands to create a sample repo that we'll be using for this hands-on practical:
mkdir samplerepo-sitcom
cd samplerepo-sitcom
git init
echo "Aspiring actress" >> Penny.txt
git add .
git commit -m "C1: Add Penny.txt"
echo "Scientist" >> Sheldon.txt
git add .
git commit -m "C3: Add Sheldon.txt"
echo "Comic book store owner" >> Stuart.txt
git add .
git commit -m "C2: Add Stuart.txt"
echo "Engineer" >> Stuart.txt
git commit -am "X: Incorrectly update Stuart.txt"
echo "Engineer" >> Howard.txt
git add .
git commit -m "C4: Adddd Howard.txt"
Target Here are the commits that should be in the created repo, and how each commit needs to be 'tidied up'.
C4: Adddd Howard.txt
-- Fix typo in the commit message Adddd
→ Add
.X: Incorrectly update Stuart.txt
-- Drop this commit.C2: Add Stuart.txt
-- Swap this commit with the one below.C3: Add Sheldon.txt
-- Swap this commit with the one above.C1: Add Penny.txt
-- No change required.1 Start the interactive rebasing.
To start the interactive rebase, use the git rebase -i <start-commit>
command. -i
stands for 'interactive'. In this case, we want to modify the last four commits (hence, HEAD~4
).
git rebase -i HEAD~4
pick 97a8c4a C3: Add Sheldon.txt
pick 60bd28d C2: Add Stuart.txt
pick 8b9a36f X: Incorrectly update Stuart.txt
pick 8ab6941 C4: Adddd Howard.txt
# Rebase ee04afe..8ab6941 onto ee04afe (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# create a merge commit using the original merge commit's
# message (or the oneline, if no original merge commit was
# specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
# to this position in the new commits. The <ref> is
# updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
The command will take you to the text editor, which will present you with a wall of text similar to the above. It has two parts:
pick
indicated by default (pick
means 'use this commit in the result') for each.2 Edit the commit list to specify the rebase actions, as follows:
pick 60bd28d C2: Add Stuart.txt
pick 97a8c4a C3: Add Sheldon.txt
drop 8b9a36f X: Incorrectly update Stuart.txt
reword 8ab6941 C4: Addddd Howard.txt
4 Once you save edits and exit the text editor, Git will perform the rebase based on the actions you specified, from top to bottom.
At some steps, Git will pause the rebase and ask for your inputs. In this case, it will ask you to specify the new commit message when it is processing the following line.
reword 8ab6941 C4: Addddd Howard.txt
To go to the interactive rebase mode, right-click the parent commit of the earliest commit you want to reorganise (in this case, it is C1: Add Penny.txt
) and choose Rebase children of <SHA> interactively...
2 To indicate what action you want to perform on each commit, select the commit in the list and click on the button for the action you want to do on it:
3 To execute the rebase, after indicating the action for all commits (the dialog will look like the below), click OK
.
The final result should be something like the following, 'tidied up' exactly as we wanted:
* 727d877 C4: Add Howard.txt
* 764fc29 C3: Add Sheldon.txt
* 08a965a C2: Add Stuart.txt
* 6436598 C1: Add Penny.txt
done!
Rebasing rewrites history. It is not recommended to rebase commits you have already shared with others.
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.
Updating the commit message
To change the commit message subject only, use the git commit --amend -m "<new commit message>"
command.
git commit --amend -m "Fix bug that froze the GUI"
To change the entire commit message (not just the subject), run the git commit --amend
command, which will open the text editor for you to edit the commit message. The commit will be updated when you close the text editor.
Click on the Commit
button on the top menu. In the region that you use to enter the commit message, use one of the two methods given below to go into the 'Amend last commit' mode.
Updating changes in the commit
While there are multiple ways to do this, one method that will work universally is to do a 'soft reset' of the last commit, update the staging area as you wish, and commit again.
'Updating' a commit does not really update that commit -- it simply creates a new commit with the new data. The original commit remains and is 'left behind' in the repo, and will be garbage-collected after a while if it is not referenced by anything else.
At this point: You should now be able to create more meaningful commits from the start, and also refine them further after they’ve been created.
What's next: Tour 6: Branching Locally