Week 5 [Mon, Sep 8th] - Topics

Detailed Table of Contents



Guidance for the item(s) below:

As you have learned the Java basic topics already, it is time to move to intermediate level topics. This week, we cover several such topics, starting with the one given below.

[W5.1] Java: Casting

W5.1a

C++ to Java → Miscellaneous Topics → Casting

Casting is the action of converting from one type to another. You can use the (newType) syntax to cast a value to a type named newType.

When you cast a primitive value to another type, there may be a loss of precision.

The code below casts a double value to an int value and casts it back to a double. Note the loss of precision.

double d = 5.3;
System.out.println("Before casting to an int: " + d);
int i = (int)d; // cast d to an int
System.out.println("After casting to an int: " + i);
d = (double)i; // cast i back to a double
System.out.println("After casting back a double: " + d);

Before casting to an int: 5.3
After casting to an int: 5
After casting back a double: 5.0

Downcasting is when you cast an object reference from a superclass to a subclass.

Assume the following class hierarchy:

class Animal{
    void speak(){
        System.out.println("I'm an animal");
    }
}

class Cat extends Animal{
    @Override
    void speak() {
        System.out.println("I'm a Cat");
    }
}

class DomesticCat extends Cat{
    @Override
    void speak() {
        System.out.println("I'm a DomesticCat");
    }
    void catchMice(){
        // ...
    }
}

The foo method below downcasts an Animal object to its subclasses.

public static void foo(Animal a){
    a.speak();
    Cat c = (Cat)a; // downcast a to a Cat
    c.speak();
    DomesticCat dc = (DomesticCat)a; // downcast a to a DomesticCat
    dc.speak();
    dc.catchMice();
}

Note that the dc.catchMice() line will not compile if a is not downcast to a DomesticCat object first. Reason: the catchMice method is specific to the DomesticCat class not not present in the Animal or the Cat classes.
Furthermore, the foo method will fail at runtime if the argument a is not a DomesticCat object. Reason: an object cannot be cast to another class unless the object is of that class to begin with e.g., you cannot cast a Dog object into a Cat object.

Upcasting is when you cast an object reference from a subclass to a superclass. However, upcasting is done automatically by the compiler even if you do not specify it explicitly.

This example upcasts a Cat object to its superclass Animal:

Cat c = new Cat();
Animal a1 = (Animal)c; //upcasting c to the Animal class
Animal a2 = c; //upcasting is implicit

Note that due to polymorphism, the behavior of the object will reflect the actual type of the object irrespective of the type of the variable holding a reference to it.

The call to the speak() method in the code below always executes the speak() method of the DomesticCat class because the actual type of the object remains DomesticCat although the reference to it is being downcast/upcast to various other types.

Animal a = new DomesticCat(); //implicit upcast
a.speak();
Cat c = (Cat)a; //downcast
c.speak();
DomesticCat dc = (DomesticCat)a; //downcast
dc.speak();

I'm a DomesticCat
I'm a DomesticCat
I'm a DomesticCat

Casting to an incompatible type can result in a ClassCastException at runtime.

This code will cause a ClassCastException:

Object o = new Animal();
Integer x = (Integer)o;

Exception in thread "main" java.lang.ClassCastException: misc.casting.Animal cannot be
cast to java.lang.Integer at misc.casting.CastingExamples.main(CastingExamples.java:14)

You can use the instanceof operator to check if a cast is safe to perform.

This code checks if the object a is an instance of the Cat class before casting it to a Cat.

Cat c;
if (a instanceof Cat){
    c = (Cat)a;
}


Guidance for the item(s) below:

It is time to move on to some intermediate level OOP concepts. Next, let's learn about abstract classes and how they are implemented in Java.

[W5.2] OOP + Java: Abstract Classes

W5.2a

Paradigms → OOP → Inheritance → Abstract classes and methods

Abstract class: A class declared as an abstract class cannot be instantiated, but it can be subclassed.

You can declare a class as abstract when a class is merely a representation of commonalities among its subclasses in which case it does not make sense to instantiate objects of that class.

The Animal class that exists as a generalization of its subclasses Cat, Dog, Horse, Tiger etc. can be declared as abstract because it does not make sense to instantiate an Animal object.

Abstract method: An abstract method is a method signature without a method implementation.

The move method of the Animal class is likely to be an abstract method as it is not possible to implement a move method at the Animal class level to fit all subclasses because each animal type can move in a different way.

A class that has an abstract method becomes an abstract class because the class definition is incomplete (due to the missing method body) and it is not possible to create objects using an incomplete class definition.


W5.2b

C++ to Java → Inheritance → Abstract classes and methods

In Java, an abstract method is declared with the keyword abstract and given without an implementation. If a class includes abstract methods, then the class itself must be declared abstract.

The speak method in this Animal class is abstract. Note how the method signature ends with a semicolon and there is no method body. This makes sense as the implementation of the speak method depends on the type of the animal and it is meaningless to provide a common implementation for all animal types.

public abstract class Animal {

    protected String name;

    public Animal(String name){
        this.name = name;
    }
    public abstract String speak();
}

As one method of the class is abstract, the class itself is abstract.

An abstract class is declared with the keyword abstract. Abstract classes can be used as reference type but cannot be instantiated.

This Account class has been declared as abstract although it does not have any abstract methods. Attempting to instantiate Account objects will result in a compile error.

public abstract class Account {

    int number;

    void close(){
        //...
    }
}

Account a; OK to use as a type
a = new Account(); Compile error!

In Java, even a class that does not have any abstract methods can be declared as an abstract class.

When an abstract class is subclassed, the subclass should provide implementations for all of the abstract methods in its superclass or else the subclass must also be declared abstract.

The Feline class below inherits from the abstract class Animal but it does not provide an implementation for the abstract method speak. As a result, the Feline class needs to be abstract too.

public abstract class Feline extends Animal {
    public Feline(String name) {
        super(name);
    }

}

The DomesticCat class inherits the abstract Feline class and provides the implementation for the abstract method speak. As a result, it need not be (but can be) declared as abstract.

public class DomesticCat extends Feline {
    public DomesticCat(String name) {
        super(name);
    }

    @Override
    public String speak() {
        return "Meow";
    }
}
  • Animal a = new Feline("Mittens");
    Compile error! Feline is abstract.
  • Animal a = new DomesticCat("Mittens");
    OK. DomesticCat can be instantiated and assigned to a variable of Animal type (the assignment is allowed by polymorphism).

Exercises:

[Key Exercise] print area with abstract Shape

The Main class below keeps a list of Circle and Rectangle objects and prints the area (as an int value) of each shape when requested.

public class Main {
    private static Shape[] shapes = new Shape[100];
    private static int shapeCount = 0;

    public static void addShape(Shape s){
        shapes[shapeCount] = s;
        shapeCount++;
    }

    public static void printAreas(){
        for (int i = 0; i < shapeCount; i++){
            shapes[i].print();
        }
    }

    public static void main(String[] args) {
        addShape(new Circle(5));
        addShape(new Rectangle(3, 4));
        addShape(new Circle(10));
        addShape(new Rectangle(4, 4));
        printAreas();
    }
}

Circle of area 78
Rectangle of area 12
Circle of area 314
Rectangle of area 16

Circle class and Rectangle class is given below:

public class Circle extends Shape {

    private int radius;

    public Circle(int radius) {
        this.radius = radius;
    }

    @Override
    public int area() {
        return (int)(Math.PI * radius * radius);
    }

    @Override
    public void print() {
        System.out.println("Circle of area " + area());
    }
}
public class Rectangle extends Shape {
    private int height;
    private int width;

    public Rectangle(int height, int width){
        this.height = height;
        this.width = width;
    }

    @Override
    public int area() {
        return height * width;
    }

    @Override
    public void print() {
        System.out.println("Rectangle of area " + area());
    }
}

Add the missing Shape class as an abstract class with two abstract methods.

Partial solution



Statements about abstract classes




Guidance for the item(s) below:

From abstract classes, we move to another related OOP concept interfaces, and how they are implemented in Java.

[W5.3] OOP + Java: Interfaces

W5.3a

Paradigms → OOP → Inheritance → Interfaces

An interface is a behavior specification i.e., a collection of . If a class , it means the class is able to support the behaviors specified by the said interface.

There are a number of situations in software engineering when it is important for disparate groups of programmers to agree to a "contract" that spells out how their software interacts. Each group should be able to write their code without any knowledge of how the other group's code is written. Generally speaking, interfaces are such contracts. --Oracle Docs on Java

Suppose SalariedStaff is an interface that contains two methods setSalary(int) and getSalary(). AcademicStaff can declare itself as implementing the SalariedStaff interface, which means the AcademicStaff class must implement all the methods specified by the SalariedStaff interface i.e., setSalary(int) and getSalary().

A class implementing an interface results in an is-a relationship, just like in class inheritance.

In the example above, AcademicStaff is a SalariedStaff. An AcademicStaff object can be used anywhere a SalariedStaff object is expected e.g., SalariedStaff ss = new AcademicStaff().


W5.3b

C++ to Java → Inheritance → Interfaces

The text given in this section borrows some explanations and code examples from the -- Java Tutorial.

In Java, an interface is a reference type, similar to a class, mainly containing method signatures. Defining an interface is similar to creating a new class except it uses the keyword interface in place of class.

Here is an interface named DrivableVehicle that defines methods needed to drive a vehicle.

public interface DrivableVehicle {
    void turn(Direction direction);
    void changeLanes(Direction direction);
    void signalTurn(Direction direction, boolean signalOn);
    // more method signatures
}

Note that the method signatures have no braces ({ }) and are terminated with a semicolon.

Interfaces cannot be instantiated—they can only be implemented by classes. When an instantiable class implements an interface, indicated by the keyword implements, it provides a method body for each of the methods declared in the interface.

Here is how a class CarModelX can implement the DrivableVehicle interface.

public class CarModelX implements DrivableVehicle {

    @Override
    public void turn(Direction direction) {
       // implementation
    }

    // implementation of other methods
}

An interface can be used as a type e.g., DrivableVehicle dv = new CarModelX();.

Interfaces can inherit from other interfaces using the extends keyword, similar to a class inheriting another.

Here is an interface named SelfDrivableVehicle that inherits the DrivableVehicle interface.

public interface SelfDrivableVehicle extends DrivableVehicle {
   void goToAutoPilotMode();
}

Note that the method signatures have no braces and are terminated with a semicolon.

Furthermore, Java allows multiple inheritance among interfaces. A Java interface can inherit multiple other interfaces. A Java class can implement multiple interfaces (and inherit from one class).

The design below is allowed by Java. In case you are not familiar with UML notation used: solid lines indicate normal inheritance; dashed lines indicate interface inheritance; the triangle points to the parent.

  1. Staff interface inherits (note the solid lines) the interfaces TaxPayer and Citizen.
  2. TA class implements both Student interface and the Staff interface.
  3. Because of point 1 above, TA class has to implement all methods in the interfaces TaxPayer and Citizen.
  4. Because of points 1,2,3, a TA is a Staff, is a TaxPayer and is a Citizen.

Interfaces can also contain constants and static methods.

This example adds a constant MAX_SPEED and a static method isSpeedAllowed to the interface DrivableVehicle.

public interface DrivableVehicle {

    int MAX_SPEED = 150;

    static boolean isSpeedAllowed(int speed){
        return speed <= MAX_SPEED;
    }

    void turn(Direction direction);
    void changeLanes(Direction direction);
    void signalTurn(Direction direction, boolean signalOn);
    // more method signatures
}

Interfaces can contain default method implementations and nested types. They are not covered here.


Exercises:

[Key Exercise] print Printable items

The Main class below passes a list of Printable objects (i.e., objects that implement the Printable interface) for another method to be printed.

public class Main {

    public static void printObjects(Printable[] items) {
        for (Printable p : items) {
            p.print();
        }
    }

    public static void main(String[] args) {
        Printable[] printableItems = new Printable[]{
                new Circle(5),
                new Rectangle(3, 4),
                new Person("James Cook")};

        printObjects(printableItems);
    }
}

Circle of area 78
Rectangle of area 12
Person of name James Cook

Classes Shape, Circle, and Rectangle are given below:

public abstract class Shape {

    public abstract int area();
}
public class Circle extends Shape implements Printable {

    private int radius;

    public Circle(int radius) {
        this.radius = radius;
    }

    @Override
    public int area() {
        return (int)(Math.PI * radius * radius);
    }

    @Override
    public void print() {
        System.out.println("Circle of area " + area());
    }
}
public class Rectangle extends Shape implements Printable {
    private int height;
    private int width;

    public Rectangle(int height, int width){
        this.height = height;
        this.width = width;
    }

    @Override
    public int area() {
        return height * width;
    }

    @Override
    public void print() {
        System.out.println("Rectangle of area " + area());
    }
}

Add the missing Printable interface. Add the missing methods of the Person class given below.

public class Person implements Printable {

    private String name;

    // todo: add missing methods
}

Partial solution





Guidance for the item(s) below:

As you add more and more Java classes to your project, keeping all those classes in the same directory becomes untenable. The solution is covered in the next topic.

[W5.4] Java: Packages

W5.4a

C++ to Java → Miscellaneous Topics → Packages

You can organize your types (i.e., classes, interfaces, enumerations, etc.) into packages for easier management (among other benefits).

To create a package, you put a package statement at the very top of every source file in that package. The package statement must be the first line in the source file and there can be no more than one package statement in each source file. Furthermore, the package of a type should match the folder path of the source file. Similarly, the compiler will put the .class files in a folder structure that matches the package names.

The Formatter class below (in <source folder>/seedu/tojava/util/Formatter.java file) is in the package seedu.tojava.util. When it is compiled, the Formatter.class file will be in the location <compiler output folder>/seedu/tojava/util:

package seedu.tojava.util;

public class Formatter {
    public static final String PREFIX = ">>";

    public static String format(String s){
        return PREFIX + s;
    }
}

Package names are written in all lower case (not camelCase), using the dot as a separator. Packages in the Java language itself begin with java. or javax. Companies use their reversed Internet domain name to begin their package names.

For example, com.foobar.doohickey.util can be the name of a package created by a company with a domain name foobar.com

To use a public from outside its package, you must do one of the following:

  1. Use the to refer to the member
  2. Import the package or the specific package member

The Main class below has two import statements:

  • import seedu.tojava.util.StringParser: imports the class StringParser in the seedu.tojava.util package
  • import seedu.tojava.frontend.*: imports all the classes in the seedu.tojava.frontend package
package seedu.tojava;

import seedu.tojava.util.StringParser;
import seedu.tojava.frontend.*;

public class Main {

    public static void main(String[] args) {

        // Using the fully qualified name to access the Processor class
        String status = seedu.tojava.logic.Processor.getStatus();

        // Using the StringParser previously imported
        StringParser sp = new StringParser();

        // Using classes from the tojava.frontend package
        Ui ui = new Ui();
        Message m = new Message();

    }
}

Note how the class can still use the Processor without importing it first, by using its fully qualified name seedu.tojava.logic.Processor

Importing a package does not import its sub-packages, as packages do not behave as hierarchies despite appearances.

import seedu.tojava.frontend.* does not import the classes in the sub-package seedu.tojava.frontend.widget.

If you do not use a package statement, your type doesn't have a package -- a practice not recommended (except for small code examples) as it is not possible for a type in a package to import a type that is not in a package.

Optionally, a static import can be used to import static members of a type so that the imported members can be used without specifying the type name.

The class below uses static imports to import the constant PREFIX and the method format() from the seedu.tojava.util.Formatter class.

import static seedu.tojava.util.Formatter.PREFIX;
import static seedu.tojava.util.Formatter.format;

public class Main {

    public static void main(String[] args) {

        String formatted = format("Hello");
        boolean isFormatted = formatted.startsWith(PREFIX);
        System.out.println(formatted);
    }
}

Formatter class


Note how the class can use PREFIX and format() (instead of Formatter.PREFIX and Formatter.format()).

When using the command line to compile/run Java, you should take the package into account.

If the seedu.tojava.Main class is defined in the file Main.java,

  • when compiling from the <source folder>, the command is:
    javac seedu/tojava/Main.java
  • when running it from the <compiler output folder>, the command is:
    java seedu.tojava.Main

Resources:


Guidance for the item(s) below:

As the size of your Java codebase grows, every class being able to access every member of every other class can be problematic. Hence, there should be a way to control the access to our Java classes and their members. The solution is given in the topic below.

[W5.5] Java: Access Modifiers

W5.5a

C++ to Java → Miscellaneous Topics → Access modifiers

Access level modifiers determine whether other classes can use a particular field or invoke a particular method.

There are two levels of access control:

  1. At the class level:

    • public: the class is visible to all classes everywhere
    • no modifier (the default, also known as package-private): it is visible only within its own package

  2. At the member level:

    • public or no modifier (package-private): same meaning as when used with top-level classes
    • private: the member can only be accessed in its own class
    • protected: the member can only be accessed within its own package (as with package-private) and, in addition, by a subclass of its class in another package

The following table shows the access to members permitted by each modifier.

Modifier
public
protected
no modifier
private

Access levels affect you in two ways:

  1. When you use classes that come from another source, such as the classes in the Java platform, access levels determine which members of those classes your own classes can use.
  2. When you write a class, you need to decide what access level every member variable and every method in your class should have.


Guidance for the item(s) below:

So far, your iP may have assumed a 'perfect world' e.g., user input is always in the expected format. To make the product ready for the not-so-perfect real world, the code should be able to handle error conditions. Let's learn how to do that.

[W5.6] Error Handling: Exceptions

W5.6a

Implementation → Error Handling → Introduction → What

Well-written applications include error-handling code that allows them to recover gracefully from unexpected errors. When an error occurs, the application may need to request user intervention, or it may be able to recover on its own. In extreme cases, the application may log the user off or shut down the system. -- Microsoft


W5.6b

Implementation → Error Handling → Exceptions → What

Exceptions are used to deal with 'unusual' but not entirely unexpected situations that the program might encounter at runtime.

Exception:

The term exception is shorthand for the phrase "exceptional event." An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program's instructions. –- Java Tutorial (Oracle Inc.)

Examples:

  • A network connection encounters a timeout due to a slow server.
  • The code tries to read a file from the hard disk but the file is corrupted and cannot be read.

W5.6c

Implementation → Error Handling → Exceptions → How

Most languages allow code that encountered an "exceptional" situation to encapsulate details of the situation in an Exception object and throw/raise that object so that another piece of code can catch it and deal with it. This is especially useful when the code that encountered the unusual situation does not know how to deal with it.

The extract below from the -- Java Tutorial (with slight adaptations) explains how exceptions are typically handled.

When an error occurs at some point in the execution, the code being executed creates an exception object and hands it off to the runtime system. The exception object contains information about the error, including its type and the state of the program when the error occurred. Creating an exception object and handing it to the runtime system is called throwing an exception.

After a method throws an exception, the runtime system attempts to find something to handle it in the . The runtime system searches the call stack for a method that contains a block of code that can handle the exception. This block of code is called an exception handler. The search begins with the method in which the error occurred and proceeds through the call stack in the reverse order in which the methods were called. When an appropriate handler is found, the runtime system passes the exception to the handler. An exception handler is considered appropriate if the type of the exception object thrown matches the type that can be handled by the handler.

The exception handler chosen is said to catch the exception. If the runtime system exhaustively searches all the methods on the call stack without finding an appropriate exception handler, the program terminates.

Advantages of exception handling in this way:

  • The ability to propagate error information through the call stack.
  • The separation of code that deals with 'unusual' situations from the code that does the 'usual' work.

Exercises:

Benefits of exceptions



W5.6d

C++ to Java → Exceptions → What are Exceptions?

Given below is an extract from the -- Java Tutorial, with some adaptations.

There are three basic categories of exceptions In Java:

  • Checked exceptions: exceptional conditions that a well-written application should anticipate and recover from. All exceptions are checked exceptions, except for Error, RuntimeException, and their subclasses.

Suppose an application prompts a user for an input file name, then opens the file by passing the name to the constructor for java.io.FileReader. Normally, the user provides the name of an existing, readable file, so the construction of the FileReader object succeeds, and the execution of the application proceeds normally. But sometimes the user supplies the name of a nonexistent file, and the constructor throws java.io.FileNotFoundException. A well-written program will catch this exception and notify the user of the mistake, possibly prompting for a corrected file name.

  • Errors: exceptional conditions that are external to the application, and that the application usually cannot anticipate or recover from. Errors are those exceptions indicated by Error and its subclasses.

Suppose that an application successfully opens a file for input, but is unable to read the file because of a hardware or system malfunction. The unsuccessful read will throw java.io.IOError. An application might choose to catch this exception, in order to notify the user of the problem — but it also might make sense for the program to print a stack trace and exit.

  • Runtime exceptions: conditions that are internal to the application, and that the application usually cannot anticipate or recover from. Runtime exceptions are those indicated by RuntimeException and its subclasses. These usually indicate programming bugs, such as logic errors or improper use of an API.

Consider the application described previously that passes a file name to the constructor for FileReader. If a logic error causes a null to be passed to the constructor, the constructor will throw NullPointerException. The application can catch this exception, but it probably makes more sense to eliminate the bug that caused the exception to occur.

Errors and runtime exceptions are collectively known as unchecked exceptions.


W5.6e

C++ to Java → Exceptions → How to use Exceptions

The content below uses extracts from the -- Java Tutorial, with some adaptations.

A program can catch exceptions by using a combination of the try, catch blocks.

  • The try block identifies a block of code in which an exception can occur.
  • The catch block identifies a block of code, known as an exception handler, that can handle a particular type of exception.

The writeList() method below calls a method process() that can cause two type of exceptions. It uses a try-catch construct to deal with each exception.

public void writeList() {
    print("starting method");
    try {
        print("starting process");
        process();
        print("finishing process");

    } catch (IndexOutOfBoundsException e) {
        print("caught IOOBE");

    } catch (IOException e) {
        print("caught IOE");

    }
    print("finishing method");
}

Some possible outputs:

No exceptions IOException IndexOutOfBoundsException
starting method
starting process
finishing process
finishing method
starting method
starting process
finishing process
caught IOE
finishing method
starting method
starting process
finishing process
caught IOOBE
finishing method

You can use a finally block to specify code that is guaranteed to execute with or without the exception. This is the right place to close files, recover resources, and otherwise clean up after the code enclosed in the try block.

The writeList() method below has a finally block:

public void writeList() {
    print("starting method");
    try {
        print("starting process");
        process();
        print("finishing process");

    } catch (IndexOutOfBoundsException e) {
        print("caught IOOBE");

    } catch (IOException e) {
        print("caught IOE");

    } finally {
        // clean up
        print("cleaning up");
    }
    print("finishing method");
}

Some possible outputs:

No exceptions IOException IndexOutOfBoundsException
starting method
starting process
finishing process
cleaning up
finishing method
starting method
starting process
finishing process
caught IOE
cleaning up
finishing method
starting method
starting process
finishing process
caught IOOBE
cleaning up
finishing method
  • The try statement should contain at least one catch block or a finally block and may have multiple catch blocks.

  • The class of the exception object indicates the type of exception thrown. The exception object can contain further information about the error, including an error message.

You can use the throw statement to throw an exception. The throw statement requires a object as the argument.

Here's an example of a throw statement.

if (size == 0) {
    throw new EmptyStackException();
}

In Java, Checked exceptions are subject to the Catch or Specify Requirement: code that might throw checked exceptions must be enclosed by either of the following:

  • A try statement that catches the exception. The try must provide a handler for the exception.
  • A method that specifies that it can throw the exception. The method must provide a throws clause that lists the exception.

Unchecked exceptions are not required to follow to the Catch or Specify Requirement but you can apply the requirement to them too.

Here's an example of a method specifying that it throws certain checked exceptions:

public void writeList() throws IOException, IndexOutOfBoundsException {
    print("starting method");
    process();
    print("finishing method");
}
Some possible outputs:
No exceptions IOException IndexOutOfBoundsException
starting method
finishing method
starting method
finishing method
starting method
finishing method

Java comes with a collection of built-in exception classes that you can use. When they are not enough, it is possible to create your own exception classes.


Exercises:

[Key Exercise] parse rectangle descriptor

The Main class below parses a string descriptor of a rectangle of the format "WIDTHxHEIGHT" e.g., "3x4" and prints the area of the rectangle.

public class Main {

    public static void printArea(String descriptor){
        //TODO: modify the code below
        System.out.println(descriptor + "=" + calculateArea(descriptor));
    }

    private static int calculateArea(String descriptor) {
        //TODO: modify the code below
        String[] dimensions = descriptor.split("x");
        return Integer.parseInt(dimensions[0]) * Integer.parseInt(dimensions[1]);
    }

    public static void main(String[] args) {
        printArea("3x4");
        printArea("5x5");
    }
}

3x4=12
5x5=25
  1. Update the code of printArea to print an error message if WIDTH and/or HEIGHT are not numbers e.g., "Ax4"
    calculateArea will throw the unchecked exception NumberFormatException if the code tries to parse a non-number to an integer.

  2. Update the code of printArea to print an error message if the descriptor is missing WIDTH and/or HEIGHT e.g., "x4"
    calculateArea will throw the unchecked exception IndexOutOfBoundsException if one or both dimensions are missing.

  3. Update the code of calculateArea to throw the checked exception IllegalShapeException if there are more than 2 dimensions e.g., "5x4x3" and update the printArea to print an error message for those cases. Here is the code for the IllegalShapeException.java

public class IllegalShapeException extends Exception {
  //no other code needed
}

Here is the expected behavior after you have done the above changes:

public class Main {

    //...

    public static void main(String[] args) {
        printArea("3x4");
        printArea("3xy");
        printArea("3x");
        printArea("3");
        printArea("3x4x5");
    }
}

3x4=12
WIDTH or HEIGHT is not a number: 3xy
WIDTH or HEIGHT is missing: 3x
WIDTH or HEIGHT is missing: 3
Too many dimensions: 3x4x5

Partial solution




W5.6f

Implementation → Error Handling → Exceptions → When

In general, use exceptions only for 'unusual' conditions. Use normal return statements to pass control to the caller for conditions that are 'normal'.



Guidance for the item(s) below:

This week, you will be dealing with the requirements aspect of the tP. While there isn't time in this week to learn that topic fully, let's learn a couple the techniques you'll be using to deal with requirements of your tP this week: user stories and feature lists.

[W5.7] Specifying Requirements [quick peek ahead]


User Stories

W5.7a

Requirements → Specifying Requirements → User Stories → Introduction

User story: User stories are short, simple descriptions of a feature told from the perspective of the person who desires the new capability, usually a user or customer of the system. [Mike Cohn]

A common format for writing user stories is:

User story format: As a {user type/role} I can {function} so that {benefit}

Examples (from a Learning Management System):

  1. As a student, I can download files uploaded by lecturers, so that I can get my own copy of the files
  2. As a lecturer, I can create discussion forums, so that students can discuss things online
  3. As a tutor, I can print attendance sheets, so that I can take attendance during the class

You can write user stories using a physical medium or a digital tool. For example, you can use index cards or sticky notes, and arrange them on walls or tables. Alternatively, you can use a software (e.g., GitHub Project Boards, Trello, Google Docs, ...) to manage user stories digitally.


Exercises:

Which of these are true about user stories?


What's wrong with this user story?


Extract user stories from customer statement



W5.7b

Requirements → Specifying Requirements → User Stories → Details

The {benefit} can be omitted if it is obvious.

As a user, I can login to the system so that I can access my data

It is recommended to confirm there is a concrete benefit even if you omit it from the user story. If not, you could end up adding features that have no real benefit.

You can add more characteristics to the {user role} to provide more context to the user story.

  • As a forgetful user, I can view a password hint, so that I can recall my password.
  • As an expert user, I can tweak the underlying formatting tags of the document, so that I can format the document exactly as I need.

You can write user stories at various levels. High-level user stories, called epics (or themes) cover bigger functionality. You can then break down these epics to multiple user stories of normal size.

[Epic] As a lecturer, I can monitor student participation levels

  • As a lecturer, I can view the forum post count of each student
    so that I can identify the activity level of students in the forum
  • As a lecturer, I can view webcast view records of each student
    so that I can identify the students who did not view webcasts
  • As a lecturer, I can view file download statistics of each student
    so that I can identify the students who did not download lecture materials

You can add conditions of satisfaction to a user story to specify things that need to be true for the user story implementation to be accepted as ‘done’.

As a lecturer, I can view the forum post count of each student so that I can identify the activity level of students in the forum.

Conditions:

Separate post count for each forum should be shown
Total post count of a student should be shown
The list should be sortable by student name and post count

Other useful info that can be added to a user story includes (but not limited to)

  • Priority: how important the user story is
  • Size: the estimated effort to implement the user story
  • Urgency: how soon the feature is needed

Exercises:

Correct statements about user stories



W5.7c

Requirements → Specifying Requirements → User Stories → Usage

User stories capture user requirements in a way that is convenient for , , and .

[User stories] strongly shift the focus from writing about features to discussing them. In fact, these discussions are more important than whatever text is written. [Mike Cohn, MountainGoat Software 🔗]

User stories differ from mainly in the level of detail. User stories should only provide enough details to make a reasonably low risk estimate of how long the user story will take to implement. When the time comes to implement the user story, the developers will meet with the customer face-to-face to work out a more detailed description of the requirements. [more...]

User stories can capture non-functional requirements too because even NFRs must benefit some stakeholder.

An example of an NFR captured as a user story:

As a/an ___, I want to ___, so that ___.
impatient user to be able to experience reasonable response time from the website while up to 1000 concurrent users are using it I can use the app even when the traffic is at the maximum expected level

Given their lightweight nature, user stories are quite handy for recording requirements during early stages of requirements gathering.

A recipe for brainstorming user stories

Given below is a possible recipe you can take when using user stories for early stages of requirement gathering.

Step 0: Clear your mind of preconceived product ideas

Even if you already have some idea of what your product will look/behave like in the end, clear your mind of those ideas. The product is the solution. At this point, we are still at the stage of figuring out the problem (i.e., user requirements). Let's try to get from the problem to the solution in a systematic way, one step at a time.

Step 1: Define the target user as a persona:

Decide your target user's profile (e.g. a student, office worker, programmer, salesperson) and work patterns (e.g. Does he work in groups or alone? Does he share his computer with others?). A clear understanding of the target user will help when deciding the importance of a user story. You can even narrow it down to a persona. Here is an example:

Jean is a university student studying in a non-IT field. She interacts with a lot of people due to her involvement in university clubs/societies. ...

Step 2: Define the problem scope:

Decide the exact problem you are going to solve for the target user. It is also useful to specify what related problems it will not solve so that the exact scope is clear.

ProductX helps Jean keep track of all her school contacts. It does not cover communicating with contacts.

Step 3: List scenarios to form a narrative:

Think of the various scenarios your target user is likely to go through as she uses your app. Following a chronological sequence as if you are telling a story might be helpful.

A. First use:

  1. Jean gets to know about ProductX. She downloads it and launches it to check out what it can do.
  2. After playing around with the product for a bit, Jean wants to start using it for real.
  3. ...

B. Second use: (Jean is still a beginner)

  1. Jean launches ProductX. She wants to find ...
  2. ...

C. 10th use: (Jean is a little bit familiar with the app)

  1. ...

D. 100th use: (Jean is an expert user)

  1. Jean launches the app and does ... and ... followed by ... as per her usual habit.
  2. Jean feels some of the data in the app are no longer needed. She wants to get rid of them to reduce clutter.

More examples that might apply to some products:

  • Jean uses the app at the start of the day to ...
  • Jean uses the app before going to sleep to ...
  • Jean hasn't used the app for a while because she was on a three-month training programme. She is now back at work and wants to resume her daily use of the app.
  • Jean moves to another company. Some of her clients come with her but some don't.
  • Jean starts freelancing in her spare time. She wants to keep her freelancing clients separate from her other clients.

Step 4: List the user stories to support the scenarios:

Based on the scenarios, decide on the user stories you need to support. For example, based on the scenario 'A. First use', you might have user stories such as these:

  • As a potential user exploring the app, I can see the app populated with sample data, so that I can easily see how the app will look like when it is in use.
  • As a user ready to start using the app, I can purge all current data, so that I can get rid of sample/experimental data I used for exploring the app.

To give another example, based on the scenario 'D. 100th use', you might have user stories such as these:

  • As an expert user, I can create shortcuts for tasks, so that I can save time on frequently performed tasks.
  • As a long-time user, I can archive/hide unused data, so that I am not distracted by irrelevant data.

Do not 'evaluate' the value of user stories while brainstorming. Reason: an important aspect of brainstorming is not judging the ideas generated.

Other tips:

  • Don't be too hasty to discard 'unusual' user stories: Those might make your product unique and stand out from the rest, at least for the target users.
  • Don't go into too much detail: For example, consider this user story: As a user, I want to see a list of tasks that need my attention most at the present time, so that I pay attention to them first.
    When discussing this user story, don't worry about what tasks should be considered 'needs my attention most at the present time'. Those details can be worked out later.
  • Don't be biased by preconceived product ideas: When you are at the stage of identifying user needs, clear your mind of ideas you have about what your end product will look like. That is, don't try to reverse-engineer a preconceived product idea into user stories.
  • Don't discuss implementation details or whether you are actually going to implement it: When gathering requirements, your decision is whether the user's need is important enough for you to want to fulfil it. Implementation details can be discussed later. If a user story turns out to be too difficult to implement later, you can always omit it from the implementation plan.

While use cases can be recorded on in the initial stages, an online tool is more suitable for longer-term management of user stories, especially if the team is not .

Tool Examples: How to use some example online tools to manage user stories



Resources:


Feature Lists

W5.7d

Requirements → Specifying Requirements → Feature Lists → What

Feature list: A list of features of a product grouped according to some criteria such as aspect, priority, order of delivery, etc.

A sample feature list from a simple Minesweeper game (only a brief description has been provided to save space):

  1. Basic play – Single player play.
  2. Difficulty levels
    • Medium levels
    • Advanced levels
  3. Versus play – Two players can play against each other.
  4. Timer – Additional fixed time restriction on the player.
  5. ...


Guidance for the item(s) below:

Let's learn about a few more Git techniques, starting with branching. Although these techniques are not really needed for the iP, we require you to use them in the iP so that you have more time to practice them before they are really needed in the tP.

[W5.8] RCS: Branching

W5.8a

Git Learning Trail → Tour 6: Branching Locally

Tour 6: Branching Locally

Target Usage: To make use of multiple timelines of work in a local repository.

Motivation: At times, you need to do multiple parallel changes to files (e.g., to try two alternative implementations of the same feature).

Lesson plan:

To work in parallel timelines, you can use Git branches.

   T6L1. Creating Branches covers that part.

Most work done in branches eventually gets merged together.

   T6L2. Merging Branches covers that part.

When merging branches, you need to guide Git on how to resolve conflicting changes in different branches.

   T6L3. Resolving Merge Conflicts covers that part.

Branches can be renamed, for example, to fix a mistake in the branch name.

   T6L4. Renaming Branches covers that part.

Branches can be deleted to get rid of them when they are no longer needed.

   T6L5. Deleting Branches covers that part.

T6L1. Creating Branches


To work in parallel timelines, you can use Git branches.

This lesson covers that part.

Git branches let you develop multiple versions of your work in parallel — effectively creating diverged timelines of your repository’s history. For example, one team member can create a new branch to experiment with a change, while the rest of the team continues working on another branch. Branches can have meaningful names, such as master, release, or draft.

A Git branch is simply a ref (a named label) that points to a commit and automatically moves forward as you add new commits to that branch. As you’ve seen before, the HEAD ref indicates which branch you’re currently working on, by pointing to the corresponding branch ref.
When you add a commit, it goes into the branch you are currently on, and the branch ref (together with the HEAD ref) moves to the new commit.

Git creates a branch named master by default (Git can be configured to use a different name e.g., main).

Given below is an illustration of how branch refs move as branches evolve. Refer to the text below it for explanations of each stage.

  • There is only one branch (i.e., master) and there is only one commit on it. The HEAD ref is pointing to the master branch (as we are currently on that branch).
  • A new commit has been added. The master and the HEAD refs have moved to the new commit.
  • A new branch fix1 has been added. The repo has switched to the new branch too (hence, the HEAD ref is attached to the fix1 branch).
  • A new commit (c) has been added. The current branch ref fix1 moves to the new commit, together with the HEAD ref.
  • The repo has switched back to the master branch. Hence, the HEAD has moved back to master branch's .
    At this point, the repo's working directory reflects the code at commit b (not c).
  • A new commit (d) has been added. The master and the HEAD refs have moved to that commit.
  • The repo has switched back to the fix1 branch and added a new commit e to it. Note how the branch ref fix1 (together with HEAD) has moved to the new commit e while the branch ref master still points to d.

Note that appearance of the revision graph (colors, positioning, orientation etc.) varies based on the Git client you use, and might not match the exact diagrams given above.

HANDS-ON: Work on parallel branches

Preparation Fork the samplerepo-things repo, and clone it onto your computer.

1 Observe that you are on the branch called master.

$ git status
on branch master


2 Start a branch named feature1 and switch to the new branch.

You can use the branch command to create a new branch and the checkout command to switch to a specific branch.

$ git branch feature1
$ git checkout feature1

One-step shortcut to create a branch and switch to it at the same time:

$ git checkout –b feature1

The new switch command

Git recently introduced a switch command that you can use instead of the checkout command given above.

To create a new branch and switch to it:

$ git branch feature1
$ git switch feature1

One-step shortcut (by using -c or --create flag):

$ git switch –c feature1

Click on the Branch button on the main menu. In the next dialog, enter the branch name and click Create Branch.

Note how the feature1 is indicated as the current branch (reason: Sourcetree automatically switches to the new branch when you create a new branch, if the Checkout New Branch was selected in the previous dialog).


3 Create some commits in the new branch, as follows.

  • Add a file named numbers.txt, stage it, commit it.
  • Observe how commits you add while on feature branch will becomes part of that branch.
    Observe how the master ref and the HEAD ref move to the new commit.

As before, you can use the git log --oneline --decorate command for this.


  • At times, the HEAD ref of the local repo is represented as in Sourcetree, as illustrated in the screenshot below .

  • The HEAD ref is not shown in the UI if it is already pointing at the active branch.


  • Add some texts to numbers.txt, stage the changes, and commit it. This commit too will be added to the feature1 branch.

4 Switch to the master branch. Note how the changes you made in the feature1 branch are no longer in the working directory.

$ git switch master

Double-click the master branch.

Revisiting master vs origin/master

In the screenshot above, you see a master ref and a origin/master ref for the same commit. The former identifies the of the local master branch while the latter identifies the tip of the master branch at the remote repo named origin. The fact that both refs point to the same commit means the local master branch and its remote counterpart are with each other. Similarly, origin/HEAD ref appearing against the same commit indicates that of the remote repo is pointing to this commit as well.


5 Add a commit to the master branch. Let’s imagine it’s a bug fix.
To keep things simple for the time being, this commit should not involve the numbers.txt file that you changed in the feature1 branch. Of course, this is easily done, as the numbers.txt file you added in the feature branch is not even visible when you are in the master branch.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch feature1
    commit id: "f1"
    commit id: "[feature1] f2"
    checkout master
    commit id: "[HEAD → master] m3"
    checkout feature1

6 Switch between the two branches and see how the working directory changes accordingly. That is, now you have two parallel timelines that you can freely switch between.

done!

You can also start a branch from an earlier commit, instead of the latest commit in the current branch. For that, simply check out the commit you wish to start from.

HANDS-ON: Start a branch from an earlier commit

In the samplerepo-things repo that you used above, let's create a new branch that starts from the same commit the feature1 branch started from. Let's pretend this branch will contain an alternative version of the content we added in the feature1 branch.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch feature1
    branch feature1-alt
    checkout feature1
    commit id: "f1"
    commit id: "[feature1] f2"
    checkout master
    commit id: "[HEAD → master] m3"
    checkout feature1-alt
    commit id: "[HEAD → feature1-alt] a1"

Avoid this rookie mistake!

Always remember to switch back to the master branch before creating a new branch. If not, your new branch will be created on top of the current branch.

  1. Switch to the master branch.
  2. Checkout the commit that is at which the feature1 branch diverged from the master branch (e.g. git checkout HEAD~1). This will create a detached HEAD.
  3. Create a new branch called feature1-alt. The HEAD will now point to this new branch (i.e., no longer 'detached').
  4. Add a commit on the new branch.

PRO-TIP: Creating a branch based on another branch in one shot

Suppose you are currently on branch b2 and you want to create a new branch b3 that starts from b1. Normally, you can do that in two steps:

git switch b1     # switch to the intended base branch first
git switch -c b3  # create the new branch and switch to it

This can be done in one shot using the git switch -c <new-branch> <base-branch> command:

git switch -c b3 b1

done!


T6L2. Merging Branches


Most work done in branches eventually gets merged together.

This lesson covers that part.

Merging combines the changes from one branch into another, bringing their diverged timelines back together.

When you merge, Git looks at the two branches and figures out how their histories have diverged since their merge base (i.e., the most recent common ancestor commit of two branches). It then applies the changes from the other branch onto your current branch, creating a new commit. The new commit created when merging is called a merge commit — it records the result of combining both sets of changes.

Given below is an illustration of how such a merge looks like in the revision graph:

  • We are on the fix1 branch (as indicated by HEAD).
  • We have switched to the master branch (thus, HEAD is now pointing to master ref).
  • The fix1 branch has been merged into the master branch, creating a merge commit f. The repo is still on the master branch.

A merge commit has two parent commits e.g., in the above example, the merge commit f has both d and e as parent commits. The parent commit on the receiving branch is considered the first parent and the other is considered the second parent e.g., in the example above, fix1 branch is being merged into the master branch (i.e., the receiving branch) -- accordingly, d is the first parent and e is the second parent.

HANDS-ON: Merge a branch (with a merge commit)

Preparation We continue with the samplerepo-things repo from earlier, which should look like the following. Note that we are ignoring the feature1-alt branch, for simplicity.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch feature1
    commit id: "f1"
    commit id: "[feature1] f2"
    checkout master
    commit id: "[HEAD → master] m3"
    checkout feature1

1 Switch back to the feature1 branch.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch feature1
    commit id: "f1"
    commit id: "[HEAD → feature1] f2"
    checkout master
    commit id: "[master] m3"
    checkout feature1

2 Merge the master branch to the feature1 branch, giving an end-result like the following. Also note how Git has created a merge commit (shown as mc1 in the diagram below).

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch feature1
    commit id: "f1"
    commit id: "f2"
    checkout master
    commit id: "[master] m3"
    checkout feature1
    merge master id: "[HEAD → feature1] mc1"
$ git merge master

Right-click on the master branch and choose merge master into the current branch. Click OK in the next dialog.
The revision graph should look like this now (colours and line alignment might vary but the graph structure should be the same):


Observe how the changes you made in the master branch (i.e., the imaginary bug fix in m3) is now available even when you are in the feature1 branch.
Furthermore, observe (e.g., git show HEAD) how the merge commit contains the sum of changes done in commits m3, f1, and f2.

3 Add another commit to the feature1 branch, in which you do some further changes to the numbers.txt.
Switch to the master branch and add one more commit.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch feature1
    commit id: "f1"
    commit id: "f2"
    checkout master
    commit id: "m3"
    checkout feature1
    merge master id: "mc1"
    commit id: "[feature1] f3"
    checkout master
    commit id: "[HEAD → master] m4"

4 Merge feature1 to the master branch, giving an end-result like this:

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch feature1
    commit id: "f1"
    commit id: "f2"
    checkout master
    commit id: "m3"
    checkout feature1
    merge master id: "mc1"
    commit id: "[feature1] f3"
    checkout master
    commit id: "m4"
    merge feature1 id: "[HEAD → master] mc2"
git merge feature1

Right-click on the feature1 branch and choose Merge.... The resulting revision graph should look like this:


Now, any changes you made in feature1 branch are available in the master branch.

done!

When the branch you're merging into hasn't diverged — meaning it hasn't had any new commits since the merge base — Git simply moves the branch pointer forward to include all the new commits, keeping the history clean and linear. This is called a fast-forward merge because Git simply "fast-forwards" the branch pointer to the tip of the other branch. The result looks as if all the changes had been made directly on one branch, without any branching at all.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "[HEAD → master] m2"
    branch bug-fix
    commit id: "b1"
    commit id: "[bug-fix] b2"
    checkout master


[merge bug-fix]

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    commit id: "b1"
    commit id: "[HEAD → master][bug-fix] b2"
    checkout master

In the example above, the master branch has not changed since the merge base (i.e., m2). Hence, merging the branch bug-fix onto master can be done by fast-forwarding the master branch ref to the tip of the bug-fix branch (i.e., b2).

HANDS-ON: Do a fast-forward merge

Preparation Let's continue with the same samplerepo-things repo we used above, and do a fast-forward merge this time.

1 Create a new branch called add-countries, and some commits to it as follows:
Switch to the new branch, add a file named countries.txt, stage it, and commit it.
Do some changes to countries.txt, and commit those changes.
You should have something like this now:

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "[master] mc2"
    branch add-countries
    commit id: "a1"
    commit id: "[HEAD → add-countries] a2"

2 Go back to the master branch.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "[HEAD → master] mc2"
    branch add-countries
    commit id: "a1"
    commit id: "add-countries] a2"

3 Merge the add-countries branch onto the master branch. Observe that there is no merge commit. The master branch ref (and the HEAD ref along with it) moved to the tip of the add-countries branch (i.e., a2) and both branches now point to a2.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master (and add-countries)'}} }%%
    commit id: "mc2"
    commit id: "a1"
    commit id: "[HEAD → master][add-countries] a2"

done!

It is possible to force Git to create a merge commit even if fast forwarding is possible. This is useful if you prefer the revision graph to visually show when each branch was merged to the main timeline.

To prevent Git from fast-forwarding, use the --no-ff switch when merging. Example:

git merge --no-ff add-countries

Windows: Tick the box shown below when you merge a branch:


Mac:

Trigger the branch operation using the following menu button:

Sourcetree top menu

In the next dialog, tick the following option:

To permanently prevent fast-forwarding:

  1. Go to Sourcetree Settings.
  2. Navigate to the Git section.
  3. Tick the box Do not fast-forward when merging, always create commit.

A squash merge combines all the changes from a branch into a single commit on the receiving branch, without preserving the full commit history of the branch being merged. This is especially useful when the feature branch contains many small or experimental commits that would clutter the main branch’s history. By squashing, you retain the final state of the changes while presenting them as one cohesive unit, making the project history easier to read and manage. It also helps maintain a linear, simplified commit log on the main branch.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "[HEAD → master] m1"
    branch feature
    checkout feature
    commit id: "f1"
    commit id: "[feature] f2"


[squash merge...]

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    commit id: "[feature] f2"
    checkout master
    commit id: "[HEAD → master] s1 (same as f1+f2)" type: HIGHLIGHT

In the example above, the branch feature has been squash merged onto the master branch, creating a single 'squashed' commit s1 that combines all the commits in feature branch.

DETOUR: Undoing a Merge

  1. Ensure you are in the .
  2. Do a hard reset of that branch to the commit that would be the tip of that branch had you not done the offending merge i.e., rewind that branch to the state it was in before the merge.

In the example below, you merged master to feature1.

If you want to undo that merge,

  1. Ensure you are in the feature1 branch (because that's the branch that received the merge).
  2. Reset the feature1 branch to the commit highlighted (in yellow) in the screenshot above (because that was the tip of the feature1 branch before you merged the master branch to it).

DETOUR: Comparing Branches

Comparing branches in Git is useful when you want to understand how two lines of development differ — for example, before merging a branch, you might want to review what changes it introduces to the main branch.

Here are two ways to compare two branches:

  • Double-dot notation git diff branchA..branchB: Changes based on commits in branchB but not in branchA. This is the one used more commonly.
  • Triple-dot notation git diff branchA...branchB: This shows changes in all the commits that are reachable by either of two references but not by both of them.
    i.e., commits unique to branchA or branchB.

DETOUR: Doing a Squash Merge

To do a squash merge, you can use the --squash switch. It will prepare the squashed merge commit but will stop short of actually finalising the commit.

git merge --squash feature-1
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

At that point, you can go ahead and make the commit yourself, with the commit message you want.



T6L3. Resolving Merge Conflicts


When merging branches, you need to guide Git on how to resolve conflicting changes in different branches.

This lesson covers that part.

A merge conflict happens when Git can't automatically combine changes from two branches because the same parts of a file were modified differently in each branch. When this happens, Git pauses the merge and marks the conflicting sections in the affected files so you can resolve them yourself. Once you've reviewed and fixed the conflicts, you can tell Git they're resolved and complete the merge.

More generally, a conflict occurs when Git cannot automatically reconcile different changes made to the same part of a file -- branch merge conflicts is just one example.

HANDS-ON: Resolve merge conflict

Target To simulate a merge conflict and use it to learn how to resolve merge conflicts.

Preparation You can use any repo with at least one commit in the master branch.

1 Start a branch named fix1 in the repo. Create a commit that adds a line with some text to one of the files.

2 Switch back to master branch. Create a commit with a conflicting change i.e., it adds a line with some different text in the exact location the previous line was added.

3 Try to merge the fix1 branch onto the master branch. Git will pause mid-way during the merge and report a merge conflict. If you open the conflicted file, you will see something like this:

COLORS
------
blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red
white

4 Observe how the conflicted part is marked between a line starting with <<<<<< and a line starting with >>>>>>, separated by another line starting with =======.

Highlighted below is the conflicting part that is coming from the master branch:

blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red

This is the conflicting part that is coming from the fix1 branch:

blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red

5 Resolve the conflict by editing the file. Let us assume you want to keep both lines in the merged version. You can modify the file to be like this:

COLORS
------
blue
black
green
red
white

6 Stage the changes, and commit. You have now successfully resolved the merge conflict.

done!


T6L4. Renaming Branches


Branches can be renamed, for example, to fix a mistake in the branch name.

This lesson covers that part.

Local branches can be renamed easily. Renaming a branch simply changes the branch reference (i.e., the name used to identify the branch) — it is just a cosmetic change.

HANDS-ON: Rename local branches

Preparation First, create the repo samplerepo-books for this hands-on practical, by running the following commands in your terminal.

mkdir samplerepo-books
cd samplerepo-books
git init
echo "Horror Stories" >> horror.txt
git add .
git commit -m "Add horror.txt"
git switch -c textbooks
echo "Textbooks" >> textbooks.txt
git add .
git commit -m "Add textbooks.txt"
git switch master
git switch -c fantasy
echo "Fantasy Books" >> fantasy.txt
git add .
git commit -m "Add fantasy.txt"
git switch master
git merge --no-ff -m "Merge branch textbooks" textbooks

The above should give you a repo similar to the revision graph given below, on the left.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch textbooks
    checkout textbooks
    commit id: "[textbooks] t1"
    checkout master
    branch fantasy
    checkout fantasy
    commit id: "[fantasy] f1"
    checkout master
    merge textbooks id: "[HEAD → master] mc1"


[rename branches]

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch study-books
    checkout study-books
    commit id: "[study-books] t1"
    checkout master
    branch fantasy-books
    checkout fantasy-books
    commit id: "[fantasy-books] f1"
    checkout master
    merge study-books id: "[HEAD → master] mc1"

Target Rename the fantasy branch to fantasy-books. Similarly, rename textbooks branch to study-books. The outcome should be similar to the revision graph above, on the right.

steps:

To rename a branch, use the git branch -m <current-name> <new-name> command (-m stands for 'move'):

git branch -m fantasy fantasy-books
git branch -m textbooks study-books
git log --oneline --decorate --graph --all  # verify the changes
*   443132a (HEAD -> master) Merge branch textbooks
|\
| * 4969163 (study-books) Add textbooks.txt
|/
| * 0586ee1 (fantasy-books) Add fantasy.txt
|/
* 7f28f0e Add horror.txt

Note these additional switches to the log command:

  • --all: Shows all branches, not just the current branch.
  • --graph: Shows a graph-like visualisation (notice how * is used to indicate a commit, and branches are indicated using vertical lines).

Right-click on the branch name and choose Rename.... Provide the new branch name in the next dialog.


done!


T6L5. Deleting Branches


Branches can be deleted to get rid of them when they are no longer needed.

This lesson covers that part.

Deleting a branch deletes the corresponding branch ref from the revision history (it does not delete any commits). The impact of the loss of the branch ref depends on whether the branch has been merged.

When you delete a branch that has been merged, the commits of the branch will still exist in the history and will be safe. Only the branch ref is lost.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch bug-fix
    checkout bug-fix
    commit id: "[bug-fix] b1"
    checkout master
    merge bug-fix id: "[HEAD → master] mc1"


[delete branch bug-fix]

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch _
    checkout _
    commit id: "b1"
    checkout master
    merge _ id: "[HEAD → master] mc1"

In the above example, the only impact of the deletion is the loss of the branch ref bug-fix. All commits remain reachable (via the master branch), and there is no other impact on the revision history.

In fact, some prefer to delete the branch soon after merging it, to reduce branch references cluttering up the revision history.

When you delete a branch that has not been merged, the loss of the branch ref can render some commits unreachable (unless you know their commit IDs or they are reachable through other refs), putting them at risk of being lost eventually.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "[HEAD → master] m1"
    branch bug-fix
    checkout bug-fix
    commit id: "[bug-fix] b1"
    checkout master


[delete branch bug-fix]

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "[HEAD → master] m1"
    branch _
    checkout _
    commit id: "b1"
    checkout master

In the above example, the commit b1 is no longer reachable, unless we know its commit ID (i.e., the SHA).

SIDEBAR: What makes a commit 'unreachable'?

Recall that a commit only has a pointer to its parent commit (not its descendent commits).

A commit is considered reachable if you can get to it by starting at a branch, tag, or other ref and walking backward through its parent commits. This is the normal state for commits — they are part of the visible history of a branch or tag.

When no branch, tag, or ref points to a commit (directly or indirectly), it becomes unreachable. This often happens when you delete a branch or rewrite history (e.g., with reset or rebase), leaving some commits "orphaned" (or "dangling") without a ref pointing to them.

In the example below, C4 is unreachable (i.e., cannot be reached by starting at any of the three refs: v1.0 or master or HEAD), but the other three are all reachable.

C4unreachable!
C3 v1.0
C2 masterHEAD
C1

Unreachable commits are not deleted immediately — Git keeps them for a while before cleaning them up. By default, Git retains unreachable commits for at least 30 days, during which they can still be recovered if you know their SHA. After that, they will be garbage-collected, and will be lost for good.

HANDS-ON: Delete branches

Preparation First, create the repo samplerepo-books-2 for this hands-on practical, by running the following commands in your terminal.

mkdir samplerepo-books-2
cd samplerepo-books-2
git init
echo "Horror Stories" >> horror.txt
git add .
git commit -m "Add horror.txt"
git switch -c textbooks
echo "Textbooks" >> textbooks.txt
git add .
git commit -m "Add textbooks.txt"
git switch master
git switch -c fantasy
echo "Fantasy Books" >> fantasy.txt
git add .
git commit -m "Add fantasy.txt"
git switch master
git merge --no-ff -m "Merge branch textbooks" textbooks

1 Delete the (the merged) textbooks branch.

Use the git branch -d <branch> command to delete a local branch 'safely' -- this command will fail if the branch has unmerged changes.

git branch -d textbooks
git log --oneline --decorate --graph --all  # check the current revision graph
*   443132a (HEAD -> master) Merge branch textbooks
|\
| * 4969163 Add textbooks.txt
|/
| * 0586ee1 (fantasy) Add fantasy.txt
|/
* 7f28f0e Add horror.txt

Right-click on the branch name and choose Delete <branch>:

In the next dialog, click OK:


Observe that all commits remain. The only missing thing is the textbook ref.

2 Make a copy of the SHA of the tip of the (unmerged) fantasy branch.

3 Delete the fantasy branch.

Attempt to delete the branch. It should fail, as shown below:

git branch -d fantasy
error: the branch 'fantasy' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D fantasy'

As also hinted by the error message, you can replace the -d with -D to 'force' the deletion.

git branch -D fantasy

Now, check the revision graph:

git log --oneline --decorate --graph --all
*   443132a (HEAD -> master) Merge branch textbooks
|\
| * 4969163 Add textbooks.txt
|/
* 7f28f0e Add horror.txt

Attempt to delete the branch as you did before. It will fail because the branch has unmerged commits.

Try again but this time, tick the Force delete option, which will force Git to delete the unmerged branch:


Observe how the branch ref fantasy is gone, together with any unmerged commits on it.

4 Attempt to view the 'unreachable' commit whose SHA you noted in step 2.

e.g., git show 32b34fb (use the SHA you copied earlier)

Observe how the commit still exists and still is reachable using the commit ID, although it is not reachable by other means, and not visible in the revision graph.

done!


At this point: Now you can create, maintain, and merge multiple parallel branches in a local repo. This tour covered only the basic use of Git branches. More advanced usage will be covered in other tours.

What's next: Tour 7: Keeping Branches in Sync


W5.8b

Git Learning Trail → Tour 7: Keeping Branches in Sync

Tour 7: Keeping Branches in Sync

Target Usage: To keep branches in a local repository synchronised with each other, as needed.

Motivation: While working on one branch, you might want to have access to changes introduced in another branch (e.g., to take advantage of a bug fix introduced in another branch).

Lesson plan:

Merging is one way to keep one branch synchronised with another.

   T7L1. Merging to Sync Branches covers that part.

Rebasing is another way to synchronise one branch with another.

   T7L2. Rebasing to Sync Branches covers that part.

Cherry-picking is a Git operation that copies over a specific commit from one branch to another.

   T7L3. Copying Specific Commits covers that part.

T7L1. Merging to Sync Branches


Merging is one way to keep one branch synchronised with another.

This lesson covers that part.

When working in parallel branches, you’ll often need to sync (short for synchronise) one branch with another. For example, while developing a feature in one branch, you might want to bring in a recent bug fix from another branch that your branch doesn’t yet have.

The simplest way to sync branches is to merge — that is, to sync a branch b1 with changes from another branch b2, you merge b2 into b1. In fact, you can merge them periodically to keep one branch up to date with the other.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch bug-fix
    branch feature
    commit id: "f1"
    checkout master
    checkout bug-fix
    commit id: "b1"
    checkout master
    merge bug-fix
    checkout feature
    merge master id: "mc1"
    commit id: "f2"
    checkout master
    commit id: "m2"
    checkout feature
    merge master id: "mc2"
    checkout master
    commit id: "m3"
    checkout feature
    commit id: "[feature] f3"
    checkout master
    commit id: "[HEAD → master] m4"

In the example above, you can see how the feature branch is merging the master branch periodically to keep itself in sync with the changes being introduced to the master branch.


T7L2. Rebasing to Sync Branches


Rebasing is another way to synchronise one branch with another.

This lesson covers that part.

Rebasing is another way to synchronise one branch with another, while keeping the history cleaner and more linear. Instead of creating a merge commit to combine the branches, rebasing moves the entire sequence of commits from your branch and "replays" them on top of another branch. This effectively moves the base of your branch to the tip of the other branch (i.e., it 're-bases' it — hence the name), as if you had started your work from there in the first place.

Rebasing is especially useful when you want to update your branch with the latest changes from a main branch, but you prefer an uncluttered history with fewer merge commits.

Suppose we have the following revision graph, and we want to sync the feature branch with master, so that changes in commit m2 become visible to the feature branch.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout master
    commit id: "[master] m2"
    checkout feature
    commit id: "[HEAD → feature] f2"

If we merge the master branch to the feature branch as given below, m2 becomes visible to the feature branch. However, it creates a merge commit.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout master
    commit id: "[master] m2"
    checkout feature
    commit id: "f2"
    merge master id: "[HEAD → feature] mc1"

Instead of merging, if we rebased the feature branch on the master branch, we would get the following.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    checkout master
    commit id: "[branch: master] m2"
    branch feature
    checkout feature
    commit id: "f1a"
    commit id: "[HEAD → feature] f2a"

Note how the rebasing changed the base of the feature branch from m1 to m2. As a result, changes done in m2 are now visible to the feature branch. But there is no merge commit, and the revision graph is simpler.

Also note how the first commit in the feature branch, previously shown as f1, is now shown as f1a after the rebase. Although both commits contain the same changes, other details -- such as the parent commit -- are different, making them two distinct Git objects with different SHA values. Similarly, f2 and f2a are also different. Thus, the history of the entire feature branch has changed after the rebase.

Because rebasing rewrites the commit history of your branch, you should avoid rebasing branches that you’ve already published, and are potentially used by others -- rewriting published history can cause confusion and conflicts for those using the previous version of the commits.


T7L3. Copying Specific Commits


Cherry-picking is a Git operation that copies over a specific commit from one branch to another.

This lesson covers that part.

Cherry-picking is another way to synchronise branches, by applying specific commits from one branch onto another.

Unlike merging or rebasing — which bring over all changes since the branches diverged — cherry-picking lets you choose individual commits and apply just those, one at a time, to your current branch. This is useful when you want to bring over a bug fix or a small feature from another branch without merging the entire branch history.

Because cherry-picking copies only the chosen commits, it creates new commits on your branch with the same changes but different SHA values.

Suppose we have the following revision graph, and we want to bring over the changes introduced in m3 (in the master branch) onto the feature branch.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout master
    commit id: "m2"
    commit id: "m3" type: HIGHLIGHT
    commit id: "[master] m4"
    checkout feature
    commit id: "[HEAD → feature] f2"

After cherry-picking m3 onto the feature branch, the revision graph should look like the following:

gitGraph
%%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout master
    commit id: "m2"
    commit id: "m3" type: HIGHLIGHT
    commit id: "[master] m4"
    checkout feature
    commit id: "f3"
    commit id: "[HEAD → feature] m3a" type: HIGHLIGHT

Note how it makes the changes done in m3 available (from now on) in the feature branch, with minimal changes to the revision graph. Also note that the new commit m3a contains the same changes as m3, but it will be a different Git object with a different SHA value.

Cherry-picking is another Git operation that can result in conflicts i.e., if the changes in the cherry-picked commit conflict with the changes in the receiving branch.


At this point: You should now be able to bring changes from one branch to another in your local repository.

What's next: Tour 8: Working with Remote Branches


W5.8c

Git Learning Trail → Tour 8: Working with Remote Branches

Tour 8: Working with Remote Branches

Target Usage: To synchronise branches in the local repo with a remote repo's branches.

Motivation: It is useful to be able to have another copy of branches in a remote repo.

Lesson plan:

Local branches can be replicated in a remote.

   T8L1. Pushing Branches to a Remote covers that part.

Branches in a remote can be replicated in the local repo, and maintained in sync with each other.

   T8L2. Pulling Branches from a Remote covers that part.

Often, you'll need to delete a branch in a remote repo after it has served its purpose.

   T8L3. Deleting Branches from a Remote covers that part.

Occasionally, you might need to rename a branch in a remote repo.

   T8L4. Renaming Branches in a Remote covers that part.

T8L1. Pushing Branches to a Remote


Local branches can be replicated in a remote.

This lesson covers that part.

Pushing a copy of local branches to the corresponding remote repo makes those branches available remotely.

In a previous lesson, we saw how to push the default branch to a remote repository and have Git set up tracking between the local and remote branches using a remote-tracking reference. Pushing any other local branch to a remote works the same way as pushing the default branch — you simply specify the target branch instead of the default branch. Pushing any new commits in any local branch to a corresponding remote branch is done similarly as well.

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch bug-fix
    checkout master
    commit id: "[origin/master][HEAD → master] m2"
    checkout bug-fix
    commit id: "[bug-fix] b1"
    checkout master

[bug-fix branch does not exist in the remote origin]


gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch bug-fix
    checkout master
    commit id: "[origin/master][HEAD → master] m2"
    checkout bug-fix
    commit id: "[origin/bug-fix][bug-fix] b1"
    checkout master

[after pushing bug-fix branch to origin,
and setting up a remote-tracking branch]

HANDS-ON: Push local branches to remote

Preparation Fork the samplerepo-company to your GitHub account. When doing so, un-tick the Copy the master branch only option.
After forking, go to the fork and ensure both branches (master, and track-sales) are in there.

Clone the fork to your computer. It should look something like this:

gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch track-sales
    checkout track-sales
    commit id: "[origin/track-sales] s1"
    checkout master
    commit id: "[origin/master][origin/HEAD][HEAD → master] m3"

The origin/HEAD remote-tracking ref indicates where the HEAD ref is in the remote origin.

1 Create a new branch called hiring, and add a commit to that branch. The commit can contain any changes you want.

Here are the commands you can run in the terminal to do this step in one shot:

git switch -c hiring
echo "Receptionist: Pam" >> employees.txt
git commit -am "Add Pam to employees.txt"
gitGraph BT:
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    commit id: "m2"
    branch track-sales
    checkout track-sales
    commit id: "[origin/track-sales] s1"
    checkout master
    commit id: "[origin/master][origin/HEAD][master] m3"
    branch hiring
    checkout hiring
    commit id: "[HEAD → hiring] h1"

The resulting revision graph should look like the one above.

2 Push the hiring branch to the remote.

You can use the usual git push <remote> -u <branch> command to push the branch to the remote, and set up a remote-tracking branch at the same time.

git push origin -u hiring


3 Verify that the branch has been pushed to the remote by visiting the fork on GitHub, and looking for the origin/hiring remote-tracking ref in the local repo.

done!


T8L2. Pulling Branches from a Remote


Branches in a remote can be replicated in the local repo, and maintained in sync with each other.

This lesson covers that part.

Sometimes we need to create a local copy of a branch from a remote repository, make further changes to it, and keep it synchronised with the remote branch. Let's explore how to handle this in a few common use cases:

Use case 1: Working with branches that already existed in the remote repo when you cloned it to your computer.

When you clone a repository,

  1. Git checks out the default branch. You can start working on this branch immediately. This branch is tracking the default branch in the remote, which means you can easily synchronise changes in this branch with the remote by pulling and pushing.
  2. Git also fetches all the other branches from the remote. These other branches are not immediately available as local branches, but they are visible as remote-tracking branches.
    You can think of remote-tracking branches as read-only references to the state of those branches in the remote repository at the time of cloning. They allow you to see what work has been done on those branches without yet making local copies of them.
    To work on one of these branches, you can create a new local branch based on the remote-tracking branch. Once you do this, your local branch will usually be configured to track the corresponding branch on the remote, so you can easily synchronise your work later.
HANDS-ON: Work with a branch that existed in the remote

Preparation Use the same samplerepo-company repo you used in Lesson T8L1. Pushing Branches to a Remote. Fork and clone it if you haven't done that already.

1 Verify that the remote-tracking branch origin/track-sales exists in the local repo, but there is no local copy of it.

You can use the git branch -a command to list all local and tracking branches.

git branch -a
* hiring
  master
  remotes/origin/HEAD -> origin/master
  remotes/origin/hiring
  remotes/origin/master
  remotes/origin/track-sales

The * in the output above indicates the currently active branch.

Note how there is no track-sales in the list of branches (i.e., no local branch named track-sales), but there is a remotes/origin/track-sales (i.e., the remote-tracking branch)


Observe how the branch track-sales appear under REMOTESorigin but not under BRANCHES.


2 Create a local copy of the remote branch origin/track-sales.

You can use the git switch -c <branch> <remote-branch> command for this e.g.,

git switch -c track-sales origin/track-sales

Locate the track-sales remote-tracking branch (look under REMOTESorigin), right-click, and choose Checkout....

In the next dialog, choose as follows:


The above command/action does several things:

  1. Creates a new branch track-sales.
  2. Sets the new branch to track the remote branch origin/track-sales, which means the local branch ref track-sales will also move to where the origin/track-sales is.
  3. Switch to the newly-created branch i.e., makes it the current branch.

3 Add a commit to the track-sales branch and push to the remote, to verify that the local branch is tracking the remote branch.

Commands to perform this step in one shot:

echo "5 reams of paper" >> sales.txt
git commit -am "Update sales.txt"
git push origin track-sales

done!

Use case 2: Working with branches that were added to the remote repository after you cloned it e.g., a branch someone else pushed to the remote after you cloned.

Simply fetch to update your local repository with information about the new branch. After that, you can create a local copy of it and work with it just as you did in Use Case 1.


T8L3. Deleting Branches from a Remote


Often, you'll need to delete a branch in a remote repo after it has served its purpose.

This lesson covers that part.

To delete a branch in a remote repository, you simply tell Git to remove the reference to that branch from the remote. This does not delete the branch from your local repository — it only removes it from the remote, so others won’t see it anymore. This is useful for cleaning up clutter in the remote repo e.g., delete old or merged branches that are no longer needed on the remote.

HANDS-ON: Delete (and restore) branches in a remote

Preparation Fork the samplerepo-books to your GitHub account. When doing so, un-tick the Copy the master branch only option.
After forking, go to the fork and ensure all three branches are in there.

Clone the fork to your computer.

1 Create a local copy of the fantasy branch in your clone.

Follow instructions in Lesson T8L2. Pulling Branches from a Remote.

2 Delete the remote branch fantasy.

You can use the git push <remote> --delete <branch> command to delete a branch in a remote. This is like pushing changes in a branch to a remote, except we request the branch to be deleted instead, by adding the --delete switch.

git push origin --delete fantasy

Locate the remote branch under REMOTESorigin, right-click on the branch name, and choose Delete...:


3 Verify that the branch was deleted from the remote, by going to the fork on GitHub and checking the branches page https://github.com/{YOUR_USERNAME}/samplerepo-books/branches
e.g., https://github.com/johndoe/samplerepo-books/branches.

Also verify that the local copy has not been deleted.

4 Restore the remote branch from the local copy.

Push the local branch to the remote, while enabling the tracking option (as if pushing the branch to the remote for the first time), as covered in Lesson T8L1. Pushing Branches to a Remote.

In the above steps, we first created a local copy of the branch before deleting it in the remote repo. Doing so is optional. You can delete a remote branch without ever checking it out locally — you just need to know its name on the remote. Deleting the remote branch directly without creating a local copy is recommended if you simply want to clean up a remote branch you no longer need.

done!


T8L4. Renaming Branches in a Remote


Occasionally, you might need to rename a branch in a remote repo.

This lesson covers that part.

Git does not have a way to rename remote branches in place. Instead, you create a new branch with the desired name and delete the old one. This involves renaming your local branch to the new name, pushing it to the remote (which effectively creates a new remote branch), and then removing the old branch from the remote. This ensures the remote reflects the updated name while preserving the commit history and any work already done on the branch.

While Git cannot rename a remote branch in place, GitHub allows you to rename a branch in a remote repo. If you use this approach, the local repo still needs to be updated to reflect the change.

HANDS-ON: Rename branches in a remote

Preparation You can use the fork and the clone of the samplerepo-books that you created in Lesson T8L3. Deleting Branches from a Remote.

Target Rename the branch fantasy in the remote (i.e., your fork) to fantasy-books.

Steps

  1. Ensure you are in the master branch.
  2. Create a local copy of the remote-tracking branch origin/fantasy.
  3. Rename the local copy of the branch to fantasy-books.
  4. Push the renamed local branch to the remote, while setting up tracking for the branch as well.
  5. Delete the remote branch.
git switch master                     # ensure you are on the master branch
git switch -c fantasy origin/fantasy  # create a local copy, tracking the remote branch
git branch -m fantasy fantasy-books   # rename local branch
git push -u origin fantasy-books      # push the new branch to remote, and set it to track
git push origin --delete fantasy      # delete the old branch

You can run the git log --oneline --decorate --graph --all to check the revision graph after each step. The final outcome should be something like the below:

* 355915c (HEAD -> fantasy-books, origin/fantasy-books) Add fantasy.txt
| * 027b2b0 (origin/master, origin/HEAD, master) Merge branch textbooks
|/|
| * a6ebaec (origin/textbooks) Add textbooks.txt
|/
* d462638 Add horror.txt

Perform the above steps (each step was covered in a previous lesson).


done!


At this point: You should now be able to work with branches in a remote repo, and keep them synchronised with branches in the local repo.

What's next: More trails to be added in the future.