Learn Java

Lesson 64Phase 8: Tools of the trade

Git and GitHub: saving your work

Tracking changes with commits, working on branches, merging, and sharing code on GitHub with pull requests.

By Updated 50 min

  • repository
  • commit
  • staging
  • branch
  • merge
  • conflict

Every program you have written in this course lives in a folder that changes as you work. Edit a file today and yesterday's version is gone, unless you copied it somewhere by hand. Git keeps those versions for you. It saves your project as a series of snapshots you can go back to, lets you try an idea on a branch while the working version stays safe, and lets a team share one project through a site such as GitHub.

By the end of this lesson
  • Explain what a repository, a commit and the staging area are, and save your work with git add and git commit.
  • Read git status, git diff and git log --oneline to see what changed and where you are.
  • Keep compiled classes, build output and editor settings out of the repository with a .gitignore file.
  • Make a branch, merge it back, and resolve a merge conflict.
  • Share a project through a remote such as GitHub with push, clone and pull, and know what a pull request is for.
  • Undo a mistake with git restore, git commit --amend or git revert, and say why git reset --hard is dangerous.
From before
You have run Java from a terminal: your first Java program used cd to move into a folder, and javac, which leaves a .class file next to each source file. You have written tests in testing your code with JUnit, and if you built the console app project, you have a project worth keeping safe. Git works on any kind of file, so this lesson needs no new Java.

What Git keeps for you#

Without a tool, people keep old versions by copying files: Main.java, Main-old.java, Main-final.java, Main-final-2.java. A week later nobody remembers which copy works, or what changed between them.

Git replaces the pile of copies with a history. It is a version control system: a program that records the state of a folder of files whenever you ask it to. Each record holds every file at that moment, who made the record and when, and a short note saying what changed. Later you can compare any two records, bring back an old version of a file, or find the change that broke something.

A photo album is a good way to picture how Git does this.

  • A commit is one photo: a snapshot of the whole project at one moment, with a caption that says what changed. Once taken, a commit never changes.
  • The repository is the album: every commit you have made, in order. Git keeps it in a hidden folder called .git inside your project.
  • The staging area is the spot in front of the camera. Before each photo, you choose which changes step into the picture, with git add. Changes you leave out stay in your folder and can go into a later photo.
PlaceWhat it holdsHow a change gets there
Working folderYour files as they are right now, including changes Git has not savedYou edit and save files in your editor
Staging areaThe changes you have picked for the next commitgit add
RepositoryEvery commit: the whole history of the projectgit commit
A change travels from left to right: edit it, stage it, commit it.

Your first repository#

Before you make a repository, Git needs a few minutes of setup, once per computer.

Setting up Git once#

Open a terminal and ask Git for its version, to check that it is installed:

Terminal
read along
1$ git --version
2git version 2.49.0

Version 2.28 or newer has every command this page uses, and the transcripts come from 2.49. If the terminal cannot find git, install it. On Windows, download Git for Windows from git-scm.com. It includes Git Bash, a terminal that understands every command on this page. On macOS, the first time you run git --versionthe system offers to install Apple's command line developer tools, which include Git. On Linux, use your package manager, for example sudo apt install git on Ubuntu.

Next, tell Git who you are. Every commit records a name and an email address, so set them once for your whole computer. The third line makes main the name of the first branch in every new repository, the same name GitHub gives new repositories:

Terminal
read along
1$ git config --global user.name "Ada Lovelace"
2$ git config --global user.email "ada@example.com"
3$ git config --global init.defaultBranch main

Use your own name and email. These commands print nothing when they work. --globalmeans “for every repository on this computer”, and Git saves the settings in a file called .gitconfig in your home folder. Run git config --list to see them.

If you skip the third line
Without it, git init names the first branch master and prints a long hint suggesting you choose a name yourself. The Git project plans to make main the default in Git 3.0. Set it yourself, and your repositories match the ones on this page whichever version you have.

Turning a folder into a repository#

Here is a small project to put under Git: a library that holds books and prints them. Book is a record, Library keeps a list of books, and Main fills it. On your computer, each class is its own file: Book.java, Library.java and Main.java. So that you can run it on this page, the three are joined into one here.

Main.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 Library library = new Library();
7 library.add(new Book("Dune", "Frank Herbert"));
8 library.add(new Book("Emma", "Jane Austen"));
9
10 System.out.println("My library");
11 library.printAll();
12 }
13}
14
15class Library {
16 private final List<Book> books = new ArrayList<>();
17
18 public void add(Book book) {
19 books.add(book);
20 }
21
22 public void printAll() {
23 for (Book book : books) {
24 System.out.println(book.title() + " by " + book.author());
25 }
26 }
27}
28
29record Book(String title, String author) {
30}
Prints
›My library
›Dune by Frank Herbert
›Emma by Jane Austen

Open a terminal in the folder that holds the three files. git init turns the folder into a repository:

Terminal
read along
1$ ls
2Book.java Library.java Main.java
3$ git init
4Initialized empty Git repository in /home/you/library/.git/
5$ ls -a
6. .. .git Book.java Library.java Main.java

ls -a lists hidden files too, the ones whose names start with a dot. The only change is the new .gitfolder. That folder is the repository: every commit you make is stored inside it, while your own files stay where they were. Make one repository per project, in the project's top folder.

Asking Git what is going on: git status

git status is the command you will type most often. Run it whenever you are unsure what Git thinks.

Terminal
read along
1$ git status
2On branch main
3
4No commits yet
5
6Untracked files:
7 (use "git add <file>..." to include in what will be committed)
8 Book.java
9 Library.java
10 Main.java
11
12nothing added to commit but untracked files present (use "git add" to track)

It says you are on the branch main (branches come later in this lesson), that there are no commits yet, and that the three files are untracked: they sit in the folder, but Git is not keeping a history of them. The lines in parentheses are Git's hints about what you can do next, and they are worth reading every time.

Staging and committing

git add puts files into the staging area, exactly as they are at that moment:

Terminal
read along
1$ git add Book.java Library.java Main.java
2$ git status
3On branch main
4
5No commits yet
6
7Changes to be committed:
8 (use "git rm --cached <file>..." to unstage)
9 new file: Book.java
10 new file: Library.java
11 new file: Main.java

The files have moved under “Changes to be committed”. Now take the snapshot. -m gives the commit its message:

Terminal
read along
1$ git commit -m "Add a small library app"
2[main (root-commit) 6d404fe] Add a small library app
3 3 files changed, 28 insertions(+)
4 create mode 100644 Book.java
5 create mode 100644 Library.java
6 create mode 100644 Main.java

Read the first line from the left. The commit went onto main, it is the first commit of the repository (the root commit, the one with no parent), and its id starts with 6d404fe. The next line counts the change, three new files holding 28 lines in all, and the create mode lines list the files that are new in this commit.

Key idea

A commit is a snapshot of the whole project plus a message. You build the next snapshot in the staging area with git add, then save it for good with git commit.

Reading the history: git log

Terminal
read along
1$ git log
2commit 6d404fe8346c64b0a52659b7f7c32b16c69e7ac5 (HEAD -> main)
3Author: Ada Lovelace <ada@example.com>
4Date: Mon Oct 19 09:08:00 2026 +0100
5
6 Add a small library app
7$ git log --oneline
86d404fe (HEAD -> main) Add a small library app

git loglists commits, newest first, with each commit's full id, its author, its date and its message. git log --oneline gives one line per commit with the id shortened, here to its first seven characters, which is enough to name a commit in any command. (HEAD -> main) marks where you are; it will make sense once you meet branches.

When the log is longer than the window
Once a history fills more than one screen, git log shows it a screen at a time. Press Space to see more and q to get back to the prompt.

The everyday loop: change, check, stage, commit#

Now the project changes. Add a third book to Main.java, on the line after Emma: library.add(new Book("Persuasion", "Jane Austen"));. Save the file and ask Git what it sees:

Terminal
read along
1$ git status
2On branch main
3Changes not staged for commit:
4 (use "git add <file>..." to update what will be committed)
5 (use "git restore <file>..." to discard changes in working directory)
6 modified: Main.java
7
8no changes added to commit (use "git add" and/or "git commit -a")

Main.java is modified: it differs from the version in the last commit. To see how, ask for the difference with git diff:

Terminal
read along
1$ git diff
2diff --git a/Main.java b/Main.java
3index 23081fe..2259ba8 100644
4--- a/Main.java
5+++ b/Main.java
6@@ -3,6 +3,7 @@ public class Main {
7 Library library = new Library();
8 library.add(new Book("Dune", "Frank Herbert"));
9 library.add(new Book("Emma", "Jane Austen"));
10+ library.add(new Book("Persuasion", "Jane Austen"));
11
12 System.out.println("My library");
13 library.printAll();

The a/ file is the old version and the b/ file the new one. The line starting with @@ says where the change is: around line 3, inside public class Main. Lines that start with + were added, lines that start with - were removed, and the rest are unchanged lines shown for context. Here one line was added.

Stage the change and look again:

Terminal
read along
1$ git add Main.java
2$ git status
3On branch main
4Changes to be committed:
5 (use "git restore --staged <file>..." to unstage)
6 modified: Main.java
7$ git diff
8$ git diff --staged
9diff --git a/Main.java b/Main.java
10index 23081fe..2259ba8 100644
11--- a/Main.java
12+++ b/Main.java
13@@ -3,6 +3,7 @@ public class Main {
14 Library library = new Library();
15 library.add(new Book("Dune", "Frank Herbert"));
16 library.add(new Book("Emma", "Jane Austen"));
17+ library.add(new Book("Persuasion", "Jane Austen"));
18...

After git add, plain git diff prints nothing, because it compares your files with the staging area and the two now match. git diff --staged compares the staging area with the last commit, so it shows what the next commit will contain.

git diff
git diff --staged
Compares your files with the staging area.
Compares the staging area with the last commit.
Shows the changes you have not staged yet.
Shows what the next commit will contain.
Empty right after git add.
Empty right after git commit.

Commit the change and check the log:

Terminal
read along
1$ git commit -m "Add a third book"
2[main 74e31d6] Add a third book
3 1 file changed, 1 insertion(+)
4$ git log --oneline
574e31d6 (HEAD -> main) Add a third book
66d404fe Add a small library app

The history now holds two commits, newest at the top. That is the loop you will repeat all day: edit, git status, git diff, git add, git commit.

Quick check
You run git add Main.java, then edit Main.java again and save it, then run git commit -m "Add a book". What goes into the commit?

Habits that make good commits

  • Give each commit one idea. Adding a book and fixing an unrelated typo are two commits. Small commits are easy to read, to review and to undo.
  • Write the message as a short instruction, the way Git writes its own: Add a third book, Fix the total for an empty cart. Keep it under about 50 characters. If more needs saying, leave a blank line after it and explain why in a few sentences.
  • Commit work that compiles and passes its tests. Run the tests you learned to write in testing with JUnit before each commit, so every point in the history is a version that works.
  • Read git status before you commit. It takes a second and catches files you did not mean to include.
Two shortcuts you will see
git add . stages every change in the current folder and the folders inside it, new files included. git commit -a -m "..." stages every change to files Git already tracks and commits in one go, but skips new files. Both are quick, and both make it easy to commit something you did not mean to, so check git status first.

Keeping files out: .gitignore#

Compile the project, run it, and look at git status:

Terminal
read along
1$ javac *.java
2$ java Main
3My library
4Dune by Frank Herbert
5Emma by Jane Austen
6Persuasion by Jane Austen
7$ git status
8On branch main
9Untracked files:
10 (use "git add <file>..." to include in what will be committed)
11 Book.class
12 Library.class
13 Main.class
14
15nothing added to commit but untracked files present (use "git add" to track)

javac wrote a .class file for every class, and Git offers to track them. Keep them out of the repository. They are made from the .java files, so anyone can rebuild them, and every change to the code would change them too, filling each commit with differences nobody wrote. Commit what people write, and leave out what tools produce from it.

A file named .gitignorein the project's top folder tells Git which files to leave alone, one pattern per line. This one suits most Java projects:

.gitignore
read along
1# Compiled bytecode
2*.class
3
4# Build output from Maven and Gradle
5target/
6build/
7
8# IntelliJ IDEA settings
9.idea/
  • Lines that start with # are comments.
  • *.class matches every file whose name ends in .class, in any folder. The * stands for any run of characters.
  • target/ is the folder where Maven puts compiled classes and packaged programs, and build/is Gradle's. The slash at the end means “folders only”. You meet both tools in Maven and Gradle, next.
  • .idea/ is where IntelliJ IDEA keeps its settings for the project. Those describe one person's editor, and they differ from person to person.

Save the file and check again:

Terminal
read along
1$ git status
2On branch main
3Untracked files:
4 (use "git add <file>..." to include in what will be committed)
5 .gitignore
6
7nothing added to commit but untracked files present (use "git add" to track)
8$ git add .gitignore
9$ git commit -m "Ignore compiled classes and build output"
10[main e2d4128] Ignore compiled classes and build output
11 1 file changed, 9 insertions(+)
12 create mode 100644 .gitignore
13$ git log --oneline
14e2d4128 (HEAD -> main) Ignore compiled classes and build output
1574e31d6 Add a third book
166d404fe Add a small library app

The .class files are still in your folder, but Git no longer mentions them. The .gitignore itself is committed, so everyone who works on the project shares the same rules.

Ignoring does not untrack
A .gitignore only stops Git from picking up files it is not tracking yet. If Main.class was committed before you wrote the rule, Git keeps tracking it. Run git rm --cached Main.class to remove it from the repository while keeping it on your disk, then commit.
Never commit secrets
Passwords, API keys and other secrets do not belong in a repository. Once a commit is pushed, every copy of the history contains it, and deleting the file in a later commit does not remove it from the earlier ones. Keep secrets in a file such as .env and list that file in .gitignore.

Branches: a safe place to try ideas#

Suppose you want to add a search by author. It might take a few tries, and meanwhile you want main to stay a version that works. Branches are made for this.

A branch is a movable label that points at one commit. So far there is one, main, pointing at your latest commit. HEADis Git's name for where you are: it points at the branch you are on. When you commit, the branch that HEAD points at moves forward to the new commit. That is what (HEAD -> main) in the log means: HEAD is on main, and main is on this commit.

Terminal
read along
1$ git branch
2* main
3$ git switch -c add-search
4Switched to a new branch 'add-search'
5$ git branch
6* add-search
7 main

git branch lists the branches and stars the one you are on. git switch -c add-search creates a branch called add-search on the commit you are on, and switches to it. No files were copied: the new branch is a second label on the same commit.

Now write the search on this branch. Add a method to Library, then add two lines to Main after library.printAll();: one that prints By Jane Austen: and one that calls library.printByAuthor("Jane Austen");.

Library.java (the new method)
read along
1 public void printByAuthor(String author) {
2 for (Book book : books) {
3 if (book.author().equals(author)) {
4 System.out.println(book.title());
5 }
6 }
7 }

Then stage and commit, as usual:

Terminal
read along
1$ git status
2On branch add-search
3Changes not staged for commit:
4 (use "git add <file>..." to update what will be committed)
5 (use "git restore <file>..." to discard changes in working directory)
6 modified: Library.java
7 modified: Main.java
8
9no changes added to commit (use "git add" and/or "git commit -a")
10$ git add Library.java Main.java
11$ git commit -m "Add a search by author"
12[add-search 874c10a] Add a search by author
13 2 files changed, 11 insertions(+)
14$ git log --oneline
15874c10a (HEAD -> add-search) Add a search by author
16e2d4128 (main) Ignore compiled classes and build output
1774e31d6 Add a third book
186d404fe Add a small library app

Look at the log. HEAD -> add-search sits on the new commit, and main is still one commit behind. Only the branch you are on moved.

Switching branches changes your files

Compile and run the program on this branch, then switch back to main and do the same:

Terminal
read along
1$ javac *.java
2$ java Main
3My library
4Dune by Frank Herbert
5Emma by Jane Austen
6Persuasion by Jane Austen
7By Jane Austen:
8Emma
9Persuasion
10$ git switch main
11Switched to branch 'main'
12$ javac *.java
13$ java Main
14My library
15Dune by Frank Herbert
16Emma by Jane Austen
17Persuasion by Jane Austen

When you switch, Git rewrites the files in your folder to match the commit that branch points at. On main, printByAuthor does not exist yet: open Library.java in your editor and the method is gone. Switch back to add-search and it returns. Nothing is lost; each branch keeps its own version.

Commit before you switch
If you have changes you have not committed and the switch would overwrite them, Git refuses to switch and names the files in the way. Commit the work, or throw it away with the commands at the end of this page, then switch.

Try it in the lab below. It starts from the project's first three commits, with HEAD on main. Make a branch, commit on it, switch back, and watch which label moves each time. The lab draws the history the way git log --oneline --graph --all does, newest at the top. Then try these: commit twice on a branch, switch to main and merge it; commit on both branches before merging; merge the same branch a second time. The next section explains what you see.

Commit graph labnewest commit at the top
Switch tono other branch yet
Merge into mainnothing to merge yet
The command for each click appears here, with the first lines of Git's reply.

Three commits on main, and HEAD -> main says you are on main. Make a commit, start a branch, switch and merge, and watch which labels move.

  1. e2d4128, HEAD -> main: Ignore compiled classes and build output. Parent 74e31d6.
  2. 74e31d6: Add a third book. Parent 6d404fe.
  3. 6d404fe: Add a small library app. No parent.
Key idea

A branch is a label on a commit, and HEAD marks the branch you are on. Committing moves only that branch. Switching moves only HEAD, and changes your files to match.

The same idea in Java

If a branch is only a label, you can model one in Java: a map from branch names to commits, where each commit remembers its parent. The record below does that, and the loop at the end walks from the commit main points at back through the parents, the way git log does. Decide what it prints before you run it.

Predict the outputBranches.java
1import java.util.HashMap;
2import java.util.Map;
3
4public class Main {
5 public static void main(String[] args) {
6 Commit first = new Commit("6d404fe", "Add a small library app", null);
7 Commit second = new Commit("74e31d6", "Add a third book", first);
8
9 Map<String, Commit> branches = new HashMap<>();
10 branches.put("main", second);
11
12 // git switch -c add-search: a second name for the same commit
13 branches.put("add-search", branches.get("main"));
14
15 // git commit, while on add-search
16 Commit search = new Commit("874c10a", "Add a search by author", branches.get("add-search"));
17 branches.put("add-search", search);
18
19 // git log --oneline main
20 for (Commit c = branches.get("main"); c != null; c = c.parent()) {
21 System.out.println(c.id() + " " + c.message());
22 }
23 }
24}
25
26record Commit(String id, String message, Commit parent) {
27}

The loop starts at the commit main points at and follows parent() until there is none. What does it print?

It prints the two older commits, newest first. git switch -c put a second name on the same commit, and committing on add-search moved only that name. main still points where it did, and following parents from there never reaches 874c10a. Each commit points back at its parent, the way each node in a linked list points at the next one.

Merging a branch back#

The search works, so it belongs on main. To merge, switch to the branch that should receive the work (you are already on main), then name the branch to bring in. --all makes git log include the commits of every branch:

Terminal
read along
1$ git log --oneline --all
2874c10a (add-search) Add a search by author
3e2d4128 (HEAD -> main) Ignore compiled classes and build output
474e31d6 Add a third book
56d404fe Add a small library app
6$ git merge add-search
7Updating e2d4128..874c10a
8Fast-forward
9 Library.java | 8 ++++++++
10 Main.java | 3 +++
11 2 files changed, 11 insertions(+)
12$ git log --oneline
13874c10a (HEAD -> main, add-search) Add a search by author
14e2d4128 Ignore compiled classes and build output
1574e31d6 Add a third book
166d404fe Add a small library app
17$ git branch -d add-search
18Deleted branch add-search (was 874c10a).

Updating e2d4128..874c10a and Fast-forward describe the easy case. main had no commits of its own since add-search started, so Git moved the main label forward to 874c10a. No new commit was needed. This is a fast-forward merge. Then git branch -d deletes the add-search label. Its commit is not deleted: it is part of main now. Git refuses to delete a branch whose commits have not been merged, so -d is safe to use.

When both branches moved: a merge commit

Often mainmoves on while you work on a branch: you fix something there, or a teammate's work arrives. Now neither label can slide forward. Git compares the two branch tips with the commit where they split, combines the changes from both sides, and records the result as a merge commit, a commit with two parents. When the branches changed different files, or parts of a file with unchanged lines between them, this happens by itself, and Git prints Merge made by the 'ort' strategy.(ort is the name of Git's standard way of merging). The lab above does the same when both branches have new commits.

In a terminal, Git first opens your editor with a message such as Merge branch 'add-search' already written. Save and close it to accept the message (in Vim: Esc, then :wq and Enter), or add --no-edit to the merge command to skip the editor.

Quick check
When does git merge finish without making a new commit?

Fast-forward or not? Write Git's check in Java

Git fast-forwards when the commit your branch points at can be reached from the other branch's commit by following parents. In other words, your branch's commit is an ancestor of theirs. Write that check for the commit model from the Predict above.

ExerciseFastForward.java
Finish isAncestor. Start at newer and keep stepping to parent(); return true as soon as you meet a commit whose id equals older's, and false if you run out of parents. Leave the main method as it is: it should print true, then false once the main branch has a commit of its own.

After rename, main points at a commit that the search branch never saw, so Git cannot slide mainforward and would make a merge commit instead. A merge commit has more than one parent, so Git's own check follows all of a commit's parents.

Merge conflicts#

Git combines changes to separate parts of a file without help. When both branches changed the same lines, or lines right next to each other, Git cannot know which version you want, so it stops and asks you. That is a merge conflict. Conflicts are a normal part of working with branches, and fixing one takes a minute once you know where to look.

Here is one, made on purpose. On a branch called friendly-title, the heading line in Main.java changes from My library to Welcome to the library. Meanwhile, on main, the same line changes to Library catalogue:

Terminal
read along
1$ git switch -c friendly-title
2Switched to a new branch 'friendly-title'
3$ git add Main.java
4$ git commit -m "Use a friendlier title"
5[friendly-title f5009d2] Use a friendlier title
6 1 file changed, 1 insertion(+), 1 deletion(-)
7$ git switch main
8Switched to branch 'main'
9$ git add Main.java
10$ git commit -m "Rename the title to Library catalogue"
11[main fed5dfe] Rename the title to Library catalogue
12 1 file changed, 1 insertion(+), 1 deletion(-)
13$ git log --oneline --graph --all
14* fed5dfe (HEAD -> main) Rename the title to Library catalogue
15| * f5009d2 (friendly-title) Use a friendlier title
16|/
17* 874c10a Add a search by author
18* e2d4128 Ignore compiled classes and build output
19* 74e31d6 Add a third book
20* 6d404fe Add a small library app

--graph draws the history: each * is a commit, and the lines show which commit came from which. Both new commits grew from 874c10a, one on each branch. Now merge:

Terminal
read along
1$ git merge friendly-title
2Auto-merging Main.java
3CONFLICT (content): Merge conflict in Main.java
4Automatic merge failed; fix conflicts and then commit the result.
5$ git status
6On branch main
7You have unmerged paths.
8 (fix conflicts and run "git commit")
9 (use "git merge --abort" to abort the merge)
10
11Unmerged paths:
12 (use "git add <file>..." to mark resolution)
13 both modified: Main.java
14
15no changes added to commit (use "git add" and/or "git commit -a")

Git merged what it could and stopped at Main.java, which status lists as both modified. Open it in your editor, and you find both versions of the line between markers:

Main.java (during the merge)
read along
1public class Main {
2 public static void main(String[] args) {
3 Library library = new Library();
4 library.add(new Book("Dune", "Frank Herbert"));
5 library.add(new Book("Emma", "Jane Austen"));
6 library.add(new Book("Persuasion", "Jane Austen"));
7
8<<<<<<< HEAD
9 System.out.println("Library catalogue");
10=======
11 System.out.println("Welcome to the library");
12>>>>>>> friendly-title
13 library.printAll();
14
15 System.out.println("By Jane Austen:");
16 library.printByAuthor("Jane Austen");
17 }
18}

Between <<<<<<< HEAD and ======= is the version on the branch you are on (HEAD, which is main). Between ======= and >>>>>>> friendly-title is the version from the branch you are merging in. To resolve the conflict, edit that part into the code you want, whether that is one side, the other, or something new, and delete the three marker lines. Here the two titles are combined:

Main.java (resolved)
read along
1public class Main {
2 public static void main(String[] args) {
3 Library library = new Library();
4 library.add(new Book("Dune", "Frank Herbert"));
5 library.add(new Book("Emma", "Jane Austen"));
6 library.add(new Book("Persuasion", "Jane Austen"));
7
8 System.out.println("Welcome to the library catalogue");
9 library.printAll();
10
11 System.out.println("By Jane Austen:");
12 library.printByAuthor("Jane Austen");
13 }
14}
  1. 1Edit every conflicted file until it holds the code you want and no marker lines are left.
  2. 2Compile, run and test, so you know the combined code works.
  3. 3Stage each fixed file with git add. That tells Git the conflict is resolved.
  4. 4Commit, which records the merge commit.
Terminal
read along
1$ git add Main.java
2$ git status
3On branch main
4All conflicts fixed but you are still merging.
5 (use "git commit" to conclude merge)
6
7Changes to be committed:
8 modified: Main.java
9$ git commit --no-edit
10[main 85d5402] Merge branch 'friendly-title'
11$ git log --oneline --graph
12* 85d5402 (HEAD -> main) Merge branch 'friendly-title'
13|\
14| * f5009d2 (friendly-title) Use a friendlier title
15* | fed5dfe Rename the title to Library catalogue
16|/
17* 874c10a Add a search by author
18* e2d4128 Ignore compiled classes and build output
19* 74e31d6 Add a third book
20* 6d404fe Add a small library app

--no-edit keeps the message Git prepared, Merge branch 'friendly-title'. In the graph, the merge commit 85d5402 has two parents: fed5dfe from main and f5009d2 from friendly-title. The program now greets you with the combined title:

Terminal
read along
1$ javac *.java
2$ java Main
3Welcome to the library catalogue
4Dune by Frank Herbert
5Emma by Jane Austen
6Persuasion by Jane Austen
7By Jane Austen:
8Emma
9Persuasion
Check for leftover markers
A marker line left in a Java file stops it from compiling: javac reports illegal start of expression on the line with <<<<<<< HEAD. Search the file for <<<<<<< before you run git add.
Changed your mind halfway?
git merge --abort stops the merge and puts your files back as they were before it started. It works best when you merge with nothing uncommitted, which is one more reason to commit before you merge.

Remotes: sharing your work#

So far the history lives only in your .git folder. A remote is another copy of the repository, usually on a server, that you send commits to and fetch commits from. It is how a team shares one project, and it doubles as a backup of your own. GitHub is the best-known home for remotes; GitLab and Bitbucket are similar, and some companies run their own servers.

On GitHub you create an empty repository on the website, and it shows you the repository's address, such as https://github.com/you/library.git. So that you can follow along without an account, the transcripts below use a folder on the same computer as the remote. Git accepts a folder path as a remote's address just as it accepts a web address, so every command is the one you would type for GitHub, with the address swapped.

A server keeps a bare repository: the history only, with no working files, because nobody edits files on the server. git init --bare makes one. Then connect your project to it and send your commits:

Terminal
read along
1$ git init --bare ../server/library.git
2Initialized empty Git repository in /home/you/server/library.git/
3$ git remote add origin ../server/library.git
4$ git remote -v
5origin ../server/library.git (fetch)
6origin ../server/library.git (push)
7$ git push -u origin main
8Enumerating objects: 24, done.
9Counting objects: 100% (24/24), done.
10Delta compression using up to 8 threads
11Compressing objects: 100% (24/24), done.
12Writing objects: 100% (24/24), 2.34 KiB | 1.17 MiB/s, done.
13Total 24 (delta 12), reused 0 (delta 0), pack-reused 0 (from 0)
14To ../server/library.git
15 * [new branch] main -> main
16branch 'main' set up to track 'origin/main'.
17$ git status
18On branch main
19Your branch is up to date with 'origin/main'.
20
21nothing to commit, working tree clean

git remote add origin ../server/library.git records the address under the name origin, the usual name for a project's main remote. With GitHub it would be git remote add origin https://github.com/you/library.git. git push -u origin main sends main and every commit it needs. The lines that count objects are progress reports. What matters is at the end: * [new branch] main -> main means the remote now has a branch main, and -u connected your main to it. From now on, plain git push and git pull know where to go, and git status compares your branch with origin/main, Git's record of where main is on the remote.

Signing in to GitHub
When the remote is on GitHub, Git asks you to prove who you are the first time you push. GitHub no longer accepts your account password at that prompt. Its documentation explains the options: sign in through the browser window that Git Credential Manager opens, or use a personal access token or an SSH key.

Cloning: a copy for a teammate

A teammate, Grace, gets her own copy with git clone. It downloads the whole repository, history and all, into a new folder, and sets origin to point back where it came from. From GitHub she would type git clone https://github.com/you/library.git. Here her copy lives in another folder:

Terminal
read along
1$ cd ../grace
2$ git clone /home/you/server/library.git
3Cloning into 'library'...
4done.
5$ cd library
6$ git remote -v
7origin /home/you/server/library.git (fetch)
8origin /home/you/server/library.git (push)
9$ git log --oneline
1085d5402 (HEAD -> main, origin/main, origin/HEAD) Merge branch 'friendly-title'
11fed5dfe Rename the title to Library catalogue
12...

Her main and origin/main sit on the same commit, because she has only just cloned. origin/HEADmarks the remote's default branch.

Push and pull

Grace adds The Hobbit to Main.java, commits, and pushes:

Terminal (Grace's copy)
read along
1$ git add Main.java
2$ git commit -m "Add The Hobbit"
3[main 8116e92] Add The Hobbit
4 1 file changed, 1 insertion(+)
5$ git push
6Enumerating objects: 5, done.
7Counting objects: 100% (5/5), done.
8Delta compression using up to 8 threads
9Compressing objects: 100% (3/3), done.
10Writing objects: 100% (3/3), 315 bytes | 315.00 KiB/s, done.
11Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
12To /home/you/server/library.git
13 85d5402..8116e92 main -> main

85d5402..8116e92 main -> main means the remote's main moved from the old commit to her new one. Back in your folder:

Terminal
read along
1$ cd ../../library
2$ git status
3On branch main
4Your branch is up to date with 'origin/main'.
5
6nothing to commit, working tree clean
7$ git pull
8remote: Enumerating objects: 5, done.
9remote: Counting objects: 100% (5/5), done.
10remote: Compressing objects: 100% (3/3), done.
11remote: Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
12Unpacking objects: 100% (3/3), 295 bytes | 29.00 KiB/s, done.
13From ../server/library
14 85d5402..8116e92 main -> origin/main
15Updating 85d5402..8116e92
16Fast-forward
17 Main.java | 1 +
18 1 file changed, 1 insertion(+)
19$ git log --oneline
208116e92 (HEAD -> main, origin/main, origin/HEAD) Add The Hobbit
2185d5402 Merge branch 'friendly-title'
22fed5dfe Rename the title to Library catalogue
23...

Notice that git statussaid your branch was up to date before the pull. It compares your branch with what Git last heard from the remote, and Git does not contact the remote until you ask, so it could not know about Grace's push yet. git pull then fetched her commit (the From line) and merged it into your main, which here was a fast-forward.

Key idea

Commits travel between repositories only when you ask: git push sends yours, and git pullbrings in everyone else's. Pull before you start work, and push when a piece is done.

When the remote has moved on

Sooner or later, someone pushes while you are working. Grace pushes another book. From here on, the progress lines are cut from the transcripts and marked ...:

Terminal (Grace's copy)
read along
1$ cd ../grace/library
2$ git add Main.java
3$ git commit -m "Add Beloved"
4[main ea0bc64] Add Beloved
5 1 file changed, 1 insertion(+)
6$ git push
7...
8To /home/you/server/library.git
9 8116e92..ea0bc64 main -> main

Meanwhile you commit a comment in Book.java and try to push it:

Terminal
read along
1$ cd ../../library
2$ git add Book.java
3$ git commit -m "Describe Book in a comment"
4[main 375fcee] Describe Book in a comment
5 1 file changed, 1 insertion(+)
6$ git push
7To ../server/library.git
8 ! [rejected] main -> main (fetch first)
9error: failed to push some refs to '../server/library.git'
10hint: Updates were rejected because the remote contains work that you do not
11hint: have locally. This is usually caused by another repository pushing to
12hint: the same ref. If you want to integrate the remote changes, use
13hint: 'git pull' before pushing again.
14hint: See the 'Note about fast-forwards' in 'git push --help' for details.

The push is rejected because the remote has a commit you do not have, Grace's Add Beloved, and accepting your push would throw it away. The hint says what to do: pull first. This time both sides have new commits, so the pull must merge them, and Git first wants to know how you like that done:

Terminal
read along
1$ git pull
2...
3From ../server/library
4 8116e92..ea0bc64 main -> origin/main
5hint: You have divergent branches and need to specify how to reconcile them.
6hint: You can do so by running one of the following commands sometime before
7hint: your next pull:
8hint:
9hint: git config pull.rebase false # merge
10hint: git config pull.rebase true # rebase
11hint: git config pull.ff only # fast-forward only
12hint:
13hint: You can replace "git config" with "git config --global" to set a default
14hint: preference for all repositories. You can also pass --rebase, --no-rebase,
15hint: or --ff-only on the command line to override the configured default per
16hint: invocation.
17fatal: Need to specify how to reconcile divergent branches.
18$ git config pull.rebase false
19$ git pull
20Merge made by the 'ort' strategy.
21 Main.java | 1 +
22 1 file changed, 1 insertion(+)
23$ git log --oneline --graph
24* e024343 (HEAD -> main) Merge branch 'main' of ../server/library
25|\
26| * ea0bc64 (origin/main, origin/HEAD) Add Beloved
27* | 375fcee Describe Book in a comment
28|/
29* 8116e92 Add The Hobbit
30...
31$ git push
32...
33To ../server/library.git
34 ea0bc64..e024343 main -> main

pull.rebase false tells git pull to merge, the way everything on this page has been combined. Add --globalto make it your choice in every repository, as the hint says. The pull made a merge commit with Git's message for it, and the push that follows goes through.

Pull requests: proposing a change#

On most teams, changes reach main through a pull request rather than a direct push. You push a branch and ask for it to be merged. The pull request is a page on GitHub that shows your commits and their diff. Teammates read it, comment on lines, and approve it or ask for changes, and automated checks, such as a run of the tests, report back there. When everyone is satisfied, someone merges it on GitHub.

The first half happens in your terminal: a branch with a new feature, pushed with -u:

Terminal
read along
1$ git switch -c book-count
2Switched to a new branch 'book-count'
3$ git add Library.java Main.java
4$ git commit -m "Print the number of books"
5[book-count 7fa8025] Print the number of books
6 2 files changed, 5 insertions(+)
7$ git push -u origin book-count
8...
9To ../server/library.git
10 * [new branch] book-count -> book-count
11branch 'book-count' set up to track 'origin/book-count'.

When the remote is GitHub, the reply to this push also contains a link for opening a pull request from the new branch. The rest happens on the website:

  1. 1Open the repository on GitHub. A banner offers Compare & pull request for the branch you just pushed.
  2. 2Check that the base branch is main, where the changes should go, and the compare branch is book-count, where they are now. Write a title and a description that says what changed and why, then press Create pull request.
  3. 3Teammates read the changes and leave a review: a comment, an approval, or a request for changes. To change something, commit on the same branch and push again; the pull request picks up the new commits.
  4. 4Once it is approved and the checks pass, someone presses Merge pull request.
  5. 5Back in your terminal, run git switch main and git pull to bring the merged work into your copy, then delete the branch with git branch -d book-count.
Checks that run by themselves
In Docker and GitHub Actions you set up GitHub to compile the project and run its tests on every push and every pull request, so a broken build shows up on the pull request before anyone merges it.

Undoing mistakes safely#

Git can bring back almost anything that was committed. Changes you never committed are different: Git never had a copy, so a command that throws them away cannot return them. Choose the undo that fits the mistake:

The mistakeThe commandWhat it does
You changed a file and want the last saved version backgit restore Main.javaThrows away the changes to that file that you have not staged.
You staged a file by accidentgit restore --staged notes.txtTakes it out of the staging area. The file itself is untouched.
The last commit has a mistake, and you have not pushed itgit commit --amendReplaces the last commit with a corrected one.
A commit you already pushed was wronggit revert 81b033dAdds a new commit that undoes it.

Throw away an edit: git restore

While experimenting, you deleted a line from Main.java, and now you want the file back as it was. Look at the change first, then restore the file, as the hint in git status suggests:

Terminal
read along
1$ git status
2On branch main
3Your branch is up to date with 'origin/main'.
4
5Changes not staged for commit:
6 (use "git add <file>..." to update what will be committed)
7 (use "git restore <file>..." to discard changes in working directory)
8 modified: Main.java
9
10no changes added to commit (use "git add" and/or "git commit -a")
11$ git diff
12diff --git a/Main.java b/Main.java
13index cb4c9b2..5f44250 100644
14--- a/Main.java
15+++ b/Main.java
16@@ -8,7 +8,6 @@ public class Main {
17 library.add(new Book("Beloved", "Toni Morrison"));
18
19 System.out.println("Welcome to the library catalogue");
20- library.printAll();
21
22 System.out.println("By Jane Austen:");
23 library.printByAuthor("Jane Austen");
24$ git restore Main.java
25$ git status
26On branch main
27Your branch is up to date with 'origin/main'.
28
29nothing to commit, working tree clean

git restore Main.java copies the staged version of the file (here the same as the last commit) over the one in your folder, and the deleted line is back.

git restore has no undo
Every change you made to that file since you last staged it is gone, because Git never had a copy of it. That is why the transcript runs git diff first: make sure you are throwing away only what you mean to.

Unstage a file: git restore --staged

Next you add a fifth book, Middlemarch, to Main.java. Staging it with git add . is quick, and sometimes too quick: it also staged notes.txt, a scratch file of ideas you keep in the folder.

Terminal
read along
1$ git add .
2$ git status
3On branch main
4Your branch is up to date with 'origin/main'.
5
6Changes to be committed:
7 (use "git restore --staged <file>..." to unstage)
8 modified: Main.java
9 new file: notes.txt
10$ git restore --staged notes.txt
11$ git status
12On branch main
13Your branch is up to date with 'origin/main'.
14
15Changes to be committed:
16 (use "git restore --staged <file>..." to unstage)
17 modified: Main.java
18
19Untracked files:
20 (use "git add <file>..." to include in what will be committed)
21 notes.txt

notes.txt is untracked again and its contents are untouched, while Main.java stays staged. --staged only changes what is staged, never your files, so this one is always safe.

Fix the last commit: git commit --amend

The commit message has a typo. The commit has not been pushed, so replace it:

Terminal
read along
1$ git commit -m "Add Midlemarch"
2[main 76aa5a3] Add Midlemarch
3 1 file changed, 1 insertion(+)
4$ git log --oneline -3
576aa5a3 (HEAD -> main) Add Midlemarch
6e024343 (origin/main, origin/HEAD) Merge branch 'main' of ../server/library
7375fcee Describe Book in a comment
8$ git commit --amend -m "Add Middlemarch"
9[main 81b033d] Add Middlemarch
10 Date: Mon Oct 19 10:40:00 2026 +0100
11 1 file changed, 1 insertion(+)
12$ git log --oneline -3
1381b033d (HEAD -> main) Add Middlemarch
14e024343 (origin/main, origin/HEAD) Merge branch 'main' of ../server/library
15375fcee Describe Book in a comment
16$ git push
17...
18To ../server/library.git
19 e024343..81b033d main -> main

76aa5a3 is gone from the log, and 81b033d stands in its place. Amending makes a new commit with the same parent, holding the corrected message plus anything you staged first, which is how you add a forgotten file to the last commit. The Dateline shows that the new commit keeps the original commit's date. Then the push goes out as usual.

Only amend what you have not pushed
Amending swaps a commit for a new one with a different id. If the old one was already pushed, your main and the remote's no longer agree, git push is rejected, and teammates may already have built on the old commit. Once a commit is pushed, undo it with git revert.

Undo a pushed commit: git revert

After pushing, you learn that Middlemarch belongs on another list. The commit is shared now, so instead of removing it, add a commit that does the opposite:

Terminal
read along
1$ git revert --no-edit HEAD
2[main 24115c6] Revert "Add Middlemarch"
3 Date: Mon Oct 19 10:45:00 2026 +0100
4 1 file changed, 1 deletion(-)
5$ git log --oneline -4
624115c6 (HEAD -> main) Revert "Add Middlemarch"
781b033d (origin/main, origin/HEAD) Add Middlemarch
8e024343 Merge branch 'main' of ../server/library
9375fcee Describe Book in a comment
10$ git push
11...
12To ../server/library.git
13 81b033d..24115c6 main -> main

The new commit 24115c6 removes the line that 81b033d added. Both stay in the history, which tells the true story: a book was added and later taken out. For everyone else, the push is an ordinary one, and their next pull brings in the revert like any other commit. In a command, HEAD stands for the commit you are on; to undo an older commit, give its id instead, as in git revert 81b033d. Without --no-edit, Git opens your editor with the message Revert "Add Middlemarch" ready to save.

The command to treat with care: git reset --hard

Many answers online suggest git reset --hard. It moves your branch back to an earlier commit and makes every tracked file in your folder match that commit. Every uncommitted change in those files is destroyed, with no undo. If the commits you moved back over had already been pushed, your branch also stops matching the remote. Reach for restore, --amend and revert first. They cover the everyday mistakes without that risk.

Quick check
You pushed a commit that breaks the build, and two teammates have already pulled it. What is the safest way to undo it?
Key idea

Before you push, you can still rewrite your own work with --amend. After you push, add new commits with git revert instead. And anything you never committed is the one thing Git cannot give back.

Try it

Answer each one in your head or on paper first, then open the answer.

1
Name the three places a change passes through on its way into the history, and the command that moves it from one to the next.
2
After you compile, git status lists Main.class as untracked. Why should it stay out of the repository, and how do you make Git stop mentioning it?
3
You are on main. You run git switch -c fix and make one commit. Which branch moved, where does main point, and where does HEAD point?
4
A merge stops with CONFLICT (content): Merge conflict in Main.java. What are your next steps?
5
git status says Your branch is up to date with 'origin/main', yet a teammate pushed a commit a minute ago. How can both be true?
6
Your last commit, not yet pushed, has a typo in its message. A commit you pushed yesterday added a bug. Which command fixes each?

Practice

Work through these on your own. Each one builds on this page.

1
Put a project of your own under Git, such as the console app from the capstone. Write a .gitignore first, make the first commit, then at least three small commits with clear messages. Check that git log --oneline reads like a list of what you did.
2
In a scratch repository, make a conflict on purpose: change the same line differently on two branches, merge, and resolve it. Then make a second conflict and back out of it with git merge --abort. Compare git log --oneline --graph --all after each.
3
Practise remotes without an account. Make a bare repository in another folder, push your project to it, clone it into a second folder, and pass a commit from one copy to the other with push and pull. Then commit in both copies before pushing, and work through the rejected push the way this page did.
4
If you use GitHub, push a branch of your project and open a pull request into main on your own repository. Read the Files changed tab as a reviewer would, merge it, then pull the result and delete the branch on your computer.
5
Extend the Java model from this page so that merge commits have two parents: give Commit a List<Commit> of parents, and change isAncestor so it searches through all of a commit's parents. Test it on a history with a merge in it.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email