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 BasithUpdated 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.
- Explain what a repository, a commit and the staging area are, and save your work with
git addandgit commit. - Read
git status,git diffandgit log --onelineto see what changed and where you are. - Keep compiled classes, build output and editor settings out of the repository with a
.gitignorefile. - 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 --amendorgit revert, and say whygit reset --hardis dangerous.
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
.gitinside 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.
| Place | What it holds | How a change gets there |
|---|---|---|
| Working folder | Your files as they are right now, including changes Git has not saved | You edit and save files in your editor |
| Staging area | The changes you have picked for the next commit | git add |
| Repository | Every commit: the whole history of the project | git commit |
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:
$ git --versiongit version 2.49.0Version 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:
$ git config --global user.name "Ada Lovelace"$ git config --global user.email "ada@example.com"$ git config --global init.defaultBranch mainUse 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.
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.
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { Library library = new Library(); library.add(new Book("Dune", "Frank Herbert")); library.add(new Book("Emma", "Jane Austen")); System.out.println("My library"); library.printAll(); }} class Library { private final List<Book> books = new ArrayList<>(); public void add(Book book) { books.add(book); } public void printAll() { for (Book book : books) { System.out.println(book.title() + " by " + book.author()); } }} record Book(String title, String author) {}Open a terminal in the folder that holds the three files. git init turns the folder into a repository:
$ lsBook.java Library.java Main.java$ git initInitialized empty Git repository in /home/you/library/.git/$ ls -a. .. .git Book.java Library.java Main.javals -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.
$ git statusOn branch main No commits yet Untracked files: (use "git add <file>..." to include in what will be committed) Book.java Library.java Main.java nothing 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:
$ git add Book.java Library.java Main.java$ git statusOn branch main No commits yet Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: Book.java new file: Library.java new file: Main.javaThe files have moved under “Changes to be committed”. Now take the snapshot. -m gives the commit its message:
$ git commit -m "Add a small library app"[main (root-commit) 6d404fe] Add a small library app 3 files changed, 28 insertions(+) create mode 100644 Book.java create mode 100644 Library.java create mode 100644 Main.javaRead 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 ideaA 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 withgit commit.
Reading the history: git log
$ git logcommit 6d404fe8346c64b0a52659b7f7c32b16c69e7ac5 (HEAD -> main)Author: Ada Lovelace <ada@example.com>Date: Mon Oct 19 09:08:00 2026 +0100 Add a small library app$ git log --oneline6d404fe (HEAD -> main) Add a small library appgit 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.
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:
$ git statusOn branch mainChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: Main.java no 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:
$ git diffdiff --git a/Main.java b/Main.javaindex 23081fe..2259ba8 100644--- a/Main.java+++ b/Main.java@@ -3,6 +3,7 @@ public class Main { Library library = new Library(); library.add(new Book("Dune", "Frank Herbert")); library.add(new Book("Emma", "Jane Austen"));+ library.add(new Book("Persuasion", "Jane Austen")); System.out.println("My library"); 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:
$ git add Main.java$ git statusOn branch mainChanges to be committed: (use "git restore --staged <file>..." to unstage) modified: Main.java$ git diff$ git diff --stageddiff --git a/Main.java b/Main.javaindex 23081fe..2259ba8 100644--- a/Main.java+++ b/Main.java@@ -3,6 +3,7 @@ public class Main { Library library = new Library(); library.add(new Book("Dune", "Frank Herbert")); library.add(new Book("Emma", "Jane Austen"));+ library.add(new Book("Persuasion", "Jane Austen"));...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 add.git commit.Commit the change and check the log:
$ git commit -m "Add a third book"[main 74e31d6] Add a third book 1 file changed, 1 insertion(+)$ git log --oneline74e31d6 (HEAD -> main) Add a third book6d404fe Add a small library appThe 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.
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 statusbefore you commit. It takes a second and catches files you did not mean to include.
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:
$ javac *.java$ java MainMy libraryDune by Frank HerbertEmma by Jane AustenPersuasion by Jane Austen$ git statusOn branch mainUntracked files: (use "git add <file>..." to include in what will be committed) Book.class Library.class Main.class nothing 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:
# Compiled bytecode*.class # Build output from Maven and Gradletarget/build/ # IntelliJ IDEA settings.idea/- Lines that start with
#are comments. *.classmatches 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, andbuild/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:
$ git statusOn branch mainUntracked files: (use "git add <file>..." to include in what will be committed) .gitignore nothing added to commit but untracked files present (use "git add" to track)$ git add .gitignore$ git commit -m "Ignore compiled classes and build output"[main e2d4128] Ignore compiled classes and build output 1 file changed, 9 insertions(+) create mode 100644 .gitignore$ git log --onelinee2d4128 (HEAD -> main) Ignore compiled classes and build output74e31d6 Add a third book6d404fe Add a small library appThe .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.
.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..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.
$ git branch* main$ git switch -c add-searchSwitched to a new branch 'add-search'$ git branch* add-search maingit 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");.
public void printByAuthor(String author) { for (Book book : books) { if (book.author().equals(author)) { System.out.println(book.title()); } } }Then stage and commit, as usual:
$ git statusOn branch add-searchChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: Library.java modified: Main.java no changes added to commit (use "git add" and/or "git commit -a")$ git add Library.java Main.java$ git commit -m "Add a search by author"[add-search 874c10a] Add a search by author 2 files changed, 11 insertions(+)$ git log --oneline874c10a (HEAD -> add-search) Add a search by authore2d4128 (main) Ignore compiled classes and build output74e31d6 Add a third book6d404fe Add a small library appLook 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:
$ javac *.java$ java MainMy libraryDune by Frank HerbertEmma by Jane AustenPersuasion by Jane AustenBy Jane Austen:EmmaPersuasion$ git switch mainSwitched to branch 'main'$ javac *.java$ java MainMy libraryDune by Frank HerbertEmma by Jane AustenPersuasion by Jane AustenWhen 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.
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.
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.
- e2d4128, HEAD -> main: Ignore compiled classes and build output. Parent 74e31d6.
- 74e31d6: Add a third book. Parent 6d404fe.
- 6d404fe: Add a small library app. No parent.
Key ideaA 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.
import java.util.HashMap;import java.util.Map; public class Main { public static void main(String[] args) { Commit first = new Commit("6d404fe", "Add a small library app", null); Commit second = new Commit("74e31d6", "Add a third book", first); Map<String, Commit> branches = new HashMap<>(); branches.put("main", second); // git switch -c add-search: a second name for the same commit branches.put("add-search", branches.get("main")); // git commit, while on add-search Commit search = new Commit("874c10a", "Add a search by author", branches.get("add-search")); branches.put("add-search", search); // git log --oneline main for (Commit c = branches.get("main"); c != null; c = c.parent()) { System.out.println(c.id() + " " + c.message()); } }} record Commit(String id, String message, Commit parent) {}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:
$ git log --oneline --all874c10a (add-search) Add a search by authore2d4128 (HEAD -> main) Ignore compiled classes and build output74e31d6 Add a third book6d404fe Add a small library app$ git merge add-searchUpdating e2d4128..874c10aFast-forward Library.java | 8 ++++++++ Main.java | 3 +++ 2 files changed, 11 insertions(+)$ git log --oneline874c10a (HEAD -> main, add-search) Add a search by authore2d4128 Ignore compiled classes and build output74e31d6 Add a third book6d404fe Add a small library app$ git branch -d add-searchDeleted 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.
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.
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:
$ git switch -c friendly-titleSwitched to a new branch 'friendly-title'$ git add Main.java$ git commit -m "Use a friendlier title"[friendly-title f5009d2] Use a friendlier title 1 file changed, 1 insertion(+), 1 deletion(-)$ git switch mainSwitched to branch 'main'$ git add Main.java$ git commit -m "Rename the title to Library catalogue"[main fed5dfe] Rename the title to Library catalogue 1 file changed, 1 insertion(+), 1 deletion(-)$ git log --oneline --graph --all* fed5dfe (HEAD -> main) Rename the title to Library catalogue| * f5009d2 (friendly-title) Use a friendlier title|/* 874c10a Add a search by author* e2d4128 Ignore compiled classes and build output* 74e31d6 Add a third book* 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:
$ git merge friendly-titleAuto-merging Main.javaCONFLICT (content): Merge conflict in Main.javaAutomatic merge failed; fix conflicts and then commit the result.$ git statusOn branch mainYou have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: Main.java no 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:
public class Main { public static void main(String[] args) { Library library = new Library(); library.add(new Book("Dune", "Frank Herbert")); library.add(new Book("Emma", "Jane Austen")); library.add(new Book("Persuasion", "Jane Austen")); <<<<<<< HEAD System.out.println("Library catalogue");======= System.out.println("Welcome to the library");>>>>>>> friendly-title library.printAll(); System.out.println("By Jane Austen:"); library.printByAuthor("Jane Austen"); }}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:
public class Main { public static void main(String[] args) { Library library = new Library(); library.add(new Book("Dune", "Frank Herbert")); library.add(new Book("Emma", "Jane Austen")); library.add(new Book("Persuasion", "Jane Austen")); System.out.println("Welcome to the library catalogue"); library.printAll(); System.out.println("By Jane Austen:"); library.printByAuthor("Jane Austen"); }}- 1Edit every conflicted file until it holds the code you want and no marker lines are left.
- 2Compile, run and test, so you know the combined code works.
- 3Stage each fixed file with
git add. That tells Git the conflict is resolved. - 4Commit, which records the merge commit.
$ git add Main.java$ git statusOn branch mainAll conflicts fixed but you are still merging. (use "git commit" to conclude merge) Changes to be committed: modified: Main.java$ git commit --no-edit[main 85d5402] Merge branch 'friendly-title'$ git log --oneline --graph* 85d5402 (HEAD -> main) Merge branch 'friendly-title'|\| * f5009d2 (friendly-title) Use a friendlier title* | fed5dfe Rename the title to Library catalogue|/* 874c10a Add a search by author* e2d4128 Ignore compiled classes and build output* 74e31d6 Add a third book* 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:
$ javac *.java$ java MainWelcome to the library catalogueDune by Frank HerbertEmma by Jane AustenPersuasion by Jane AustenBy Jane Austen:EmmaPersuasionjavac reports illegal start of expression on the line with <<<<<<< HEAD. Search the file for <<<<<<< before you run git add.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:
$ git init --bare ../server/library.gitInitialized empty Git repository in /home/you/server/library.git/$ git remote add origin ../server/library.git$ git remote -vorigin ../server/library.git (fetch)origin ../server/library.git (push)$ git push -u origin mainEnumerating objects: 24, done.Counting objects: 100% (24/24), done.Delta compression using up to 8 threadsCompressing objects: 100% (24/24), done.Writing objects: 100% (24/24), 2.34 KiB | 1.17 MiB/s, done.Total 24 (delta 12), reused 0 (delta 0), pack-reused 0 (from 0)To ../server/library.git * [new branch] main -> mainbranch 'main' set up to track 'origin/main'.$ git statusOn branch mainYour branch is up to date with 'origin/main'. nothing to commit, working tree cleangit 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.
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:
$ cd ../grace$ git clone /home/you/server/library.gitCloning into 'library'...done.$ cd library$ git remote -vorigin /home/you/server/library.git (fetch)origin /home/you/server/library.git (push)$ git log --oneline85d5402 (HEAD -> main, origin/main, origin/HEAD) Merge branch 'friendly-title'fed5dfe Rename the title to Library catalogue...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:
$ git add Main.java$ git commit -m "Add The Hobbit"[main 8116e92] Add The Hobbit 1 file changed, 1 insertion(+)$ git pushEnumerating objects: 5, done.Counting objects: 100% (5/5), done.Delta compression using up to 8 threadsCompressing objects: 100% (3/3), done.Writing objects: 100% (3/3), 315 bytes | 315.00 KiB/s, done.Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)To /home/you/server/library.git 85d5402..8116e92 main -> main85d5402..8116e92 main -> main means the remote's main moved from the old commit to her new one. Back in your folder:
$ cd ../../library$ git statusOn branch mainYour branch is up to date with 'origin/main'. nothing to commit, working tree clean$ git pullremote: Enumerating objects: 5, done.remote: Counting objects: 100% (5/5), done.remote: Compressing objects: 100% (3/3), done.remote: Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)Unpacking objects: 100% (3/3), 295 bytes | 29.00 KiB/s, done.From ../server/library 85d5402..8116e92 main -> origin/mainUpdating 85d5402..8116e92Fast-forward Main.java | 1 + 1 file changed, 1 insertion(+)$ git log --oneline8116e92 (HEAD -> main, origin/main, origin/HEAD) Add The Hobbit85d5402 Merge branch 'friendly-title'fed5dfe Rename the title to Library catalogue...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 ideaCommits travel between repositories only when you ask:
git pushsends yours, andgit 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 ...:
$ cd ../grace/library$ git add Main.java$ git commit -m "Add Beloved"[main ea0bc64] Add Beloved 1 file changed, 1 insertion(+)$ git push...To /home/you/server/library.git 8116e92..ea0bc64 main -> mainMeanwhile you commit a comment in Book.java and try to push it:
$ cd ../../library$ git add Book.java$ git commit -m "Describe Book in a comment"[main 375fcee] Describe Book in a comment 1 file changed, 1 insertion(+)$ git pushTo ../server/library.git ! [rejected] main -> main (fetch first)error: failed to push some refs to '../server/library.git'hint: Updates were rejected because the remote contains work that you do nothint: have locally. This is usually caused by another repository pushing tohint: the same ref. If you want to integrate the remote changes, usehint: 'git pull' before pushing again.hint: 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:
$ git pull...From ../server/library 8116e92..ea0bc64 main -> origin/mainhint: You have divergent branches and need to specify how to reconcile them.hint: You can do so by running one of the following commands sometime beforehint: your next pull:hint:hint: git config pull.rebase false # mergehint: git config pull.rebase true # rebasehint: git config pull.ff only # fast-forward onlyhint:hint: You can replace "git config" with "git config --global" to set a defaulthint: preference for all repositories. You can also pass --rebase, --no-rebase,hint: or --ff-only on the command line to override the configured default perhint: invocation.fatal: Need to specify how to reconcile divergent branches.$ git config pull.rebase false$ git pullMerge made by the 'ort' strategy. Main.java | 1 + 1 file changed, 1 insertion(+)$ git log --oneline --graph* e024343 (HEAD -> main) Merge branch 'main' of ../server/library|\| * ea0bc64 (origin/main, origin/HEAD) Add Beloved* | 375fcee Describe Book in a comment|/* 8116e92 Add The Hobbit...$ git push...To ../server/library.git ea0bc64..e024343 main -> mainpull.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:
$ git switch -c book-countSwitched to a new branch 'book-count'$ git add Library.java Main.java$ git commit -m "Print the number of books"[book-count 7fa8025] Print the number of books 2 files changed, 5 insertions(+)$ git push -u origin book-count...To ../server/library.git * [new branch] book-count -> book-countbranch '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:
- 1Open the repository on GitHub. A banner offers
Compare & pull requestfor the branch you just pushed. - 2Check that the base branch is
main, where the changes should go, and the compare branch isbook-count, where they are now. Write a title and a description that says what changed and why, then pressCreate pull request. - 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.
- 4Once it is approved and the checks pass, someone presses
Merge pull request. - 5Back in your terminal, run
git switch mainandgit pullto bring the merged work into your copy, then delete the branch withgit branch -d book-count.
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 mistake | The command | What it does |
|---|---|---|
| You changed a file and want the last saved version back | git restore Main.java | Throws away the changes to that file that you have not staged. |
| You staged a file by accident | git restore --staged notes.txt | Takes it out of the staging area. The file itself is untouched. |
| The last commit has a mistake, and you have not pushed it | git commit --amend | Replaces the last commit with a corrected one. |
| A commit you already pushed was wrong | git revert 81b033d | Adds 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:
$ git statusOn branch mainYour branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: Main.java no changes added to commit (use "git add" and/or "git commit -a")$ git diffdiff --git a/Main.java b/Main.javaindex cb4c9b2..5f44250 100644--- a/Main.java+++ b/Main.java@@ -8,7 +8,6 @@ public class Main { library.add(new Book("Beloved", "Toni Morrison")); System.out.println("Welcome to the library catalogue");- library.printAll(); System.out.println("By Jane Austen:"); library.printByAuthor("Jane Austen");$ git restore Main.java$ git statusOn branch mainYour branch is up to date with 'origin/main'. nothing to commit, working tree cleangit 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 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.
$ git add .$ git statusOn branch mainYour branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: Main.java new file: notes.txt$ git restore --staged notes.txt$ git statusOn branch mainYour branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: Main.java Untracked files: (use "git add <file>..." to include in what will be committed) notes.txtnotes.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:
$ git commit -m "Add Midlemarch"[main 76aa5a3] Add Midlemarch 1 file changed, 1 insertion(+)$ git log --oneline -376aa5a3 (HEAD -> main) Add Midlemarche024343 (origin/main, origin/HEAD) Merge branch 'main' of ../server/library375fcee Describe Book in a comment$ git commit --amend -m "Add Middlemarch"[main 81b033d] Add Middlemarch Date: Mon Oct 19 10:40:00 2026 +0100 1 file changed, 1 insertion(+)$ git log --oneline -381b033d (HEAD -> main) Add Middlemarche024343 (origin/main, origin/HEAD) Merge branch 'main' of ../server/library375fcee Describe Book in a comment$ git push...To ../server/library.git e024343..81b033d main -> main76aa5a3 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.
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:
$ git revert --no-edit HEAD[main 24115c6] Revert "Add Middlemarch" Date: Mon Oct 19 10:45:00 2026 +0100 1 file changed, 1 deletion(-)$ git log --oneline -424115c6 (HEAD -> main) Revert "Add Middlemarch"81b033d (origin/main, origin/HEAD) Add Middlemarche024343 Merge branch 'main' of ../server/library375fcee Describe Book in a comment$ git push...To ../server/library.git 81b033d..24115c6 main -> mainThe 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.
Key ideaBefore you push, you can still rewrite your own work with
--amend. After you push, add new commits withgit revertinstead. 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.
git status lists Main.class as untracked. Why should it stay out of the repository, and how do you make Git stop mentioning it?main. You run git switch -c fix and make one commit. Which branch moved, where does main point, and where does HEAD point?CONFLICT (content): Merge conflict in Main.java. What are your next steps?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?Practice
Work through these on your own. Each one builds on this page.
.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.git merge --abort. Compare git log --oneline --graph --all after each.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.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