What is Git?
Git is a version control system — it tracks every change you make to your code, lets you travel back in time, and lets multiple people work on the same project without overwriting each other. It's the single most important tool in a developer's workflow.
Imagine writing a 500-line Java program over two weeks. On day 14 you introduce a bug. Without Git, you have no way to know what changed or when. With Git, you can see exactly which line you changed on which day, compare old and new versions, and restore any previous state in seconds.
Git was created in 2005 by Linus Torvalds — the same person who created Linux — specifically to manage the Linux kernel codebase, which thousands of developers contribute to simultaneously. Today it's used by virtually every software team on the planet.
Git vs. GitHub — not the same thing
This is the most common beginner confusion:
| Git | GitHub |
|---|---|
| git | github.com |
| Software on your computer | A website that hosts Git repos online |
| Tracks changes locally | Shares your code with the world/team |
| Works completely offline | Requires an internet connection |
| Free, open-source | Free tier + paid plans |
Think of Git as the engine and GitHub as the garage where you park and share your car. You could use Git without GitHub — but GitHub is where collaboration happens.
Why every developer uses Git
- Safety net — you can always undo any mistake
- History — every change is logged with who, what, and when
- Collaboration — multiple developers edit the same code without conflicts
- Branching — work on new features without touching working code
- Deployment — many hosting services deploy directly from a Git repo
Every professional developer job listing mentions Git. Not knowing it is a dealbreaker for team roles. By the end of this course, Git will feel as natural as saving a file.
Installation & Setup
Install Git on your machine and tell it who you are. Every commit you make will be signed with your name and email — this is how teams know who changed what.
Install Git
macOS
Install Homebrew first (brew.sh), then run: brew install git. Alternatively, install Xcode Command Line Tools: xcode-select --install
Windows
Download Git for Windows from git-scm.com. During install, select "Git from the command line and also from 3rd-party software". This also installs Git Bash — a terminal that works like Linux.
Linux (Ubuntu/Debian)
sudo apt-get update && sudo apt-get install git
Verify the install
Open your terminal and run git --version. You should see something like git version 2.42.0.
Configure your identity
Before your first commit, tell Git who you are. This information is embedded in every commit you create:
# Set your name and email — use the same email as your GitHub account $ git config --global user.name "Alex Johnson" $ git config --global user.email "alex@example.com" # Set VS Code (or any editor) as Git's default editor $ git config --global core.editor "code --wait" # Make 'main' the default branch name (modern standard) $ git config --global init.defaultBranch main # Verify your settings $ git config --list user.name=Alex Johnson user.email=alex@example.com core.editor=code --wait init.defaultBranch=main
The --global flag saves these settings for all repos on your machine. If you work on a specific project that needs different credentials (e.g. a work vs. personal project), run the same commands inside that folder without --global.
Create a GitHub account
Go to github.com and sign up with the same email you used above. Choose a professional username — you'll be putting this on your resume.
Your First Repo
A repository (repo) is a folder that Git is tracking. You create one with a single command — and from that moment, Git watches every change inside it. Let's make your first one.
# Create a new folder and navigate into it $ mkdir my-java-project $ cd my-java-project # Initialize Git — this creates a hidden .git folder $ git init Initialized empty Git repository in /Users/alex/my-java-project/.git/ # Check the current state of your repo $ git status On branch main No commits yet nothing to commit (create/copy files and use "git add" to track)
What just happened?
Running git init created a hidden folder called .git inside your project. This is Git's database — it stores your entire history, configuration, and everything Git needs. You should never edit files inside .git manually.
Anatomy of git status
git status is your most-used command. It tells you exactly where you stand: which files have changed, which are staged, and what's ready to commit. Get into the habit of running it constantly.
# Create a file $ echo "public class Main {}" > Main.java # Check status again — Git sees the new file $ git status On branch main No commits yet Untracked files: (use "git add <file>" to include in what will be committed) Main.java nothing added to commit but untracked files present
New files are "untracked" — Git sees them but isn't recording their changes yet. You have to explicitly tell Git to start tracking a file with git add. This two-step design (add, then commit) gives you precise control over what goes into each commit.
Initialize your Task Manager repo
Take the Task Manager project from the Java beginner course:
- Navigate to the project folder in your terminal
- Run
git init - Run
git statusand read every line of the output - What files does Git see? Are they tracked or untracked?
git init actually do to your folder?Staging & Committing
A commit is a saved snapshot of your project. Before committing, you stage changes — choosing exactly what goes into that snapshot. This two-step system is Git's most important concept.
The three areas of Git
Every file in a Git project lives in one of three places:
| Area | What it is | How to move here |
|---|---|---|
| Working directory | Your actual files on disk — where you edit | (files start here) |
| Staging area (index) | A draft of your next commit — curated changes | git add <file> |
| Repository (.git) | Permanent history — committed snapshots | git commit |
# See what changed $ git status Untracked files: Main.java, Task.java # Stage a specific file $ git add Main.java # Stage all changes in the current folder $ git add . # See what's staged (green = staged, red = unstaged) $ git status Changes to be committed: new file: Main.java new file: Task.java # Commit with a message describing WHAT and WHY $ git commit -m "Add initial Task Manager files" [main (root-commit) a3f2c91] Add initial Task Manager files 2 files changed, 47 insertions(+) create mode 100644 Main.java create mode 100644 Task.java # Unstage a file before committing (doesn't delete the change) $ git restore --staged Task.java # See WHAT changed in a file (diff of working dir vs staged) $ git diff # See diff of staged vs last commit $ git diff --staged
Writing good commit messages
A commit message is a note to your future self and your teammates. Bad messages like "fix stuff" or "wip" are useless in a log. Good messages follow this pattern:
❌ git commit -m "fix" ❌ git commit -m "changes" ❌ git commit -m "asdfgh" ❌ git commit -m "WIP dont push" ✓ git commit -m "Add input validation to TaskManager.addTask()" ✓ git commit -m "Fix NPE when task file doesn't exist on first run" ✓ git commit -m "Rename completeTask() to markComplete() for clarity" ✓ git commit -m "Remove debug print statements before release"
The biggest beginner mistake is coding for 4 hours then doing one massive commit. Commit every time you complete a small, logical unit of work. Small commits are easier to understand, easier to revert, and tell a clear story in the log.
Make your first 3 commits
In your Task Manager project:
- Stage and commit the
Task.javafile alone with message"Add Task model class" - Stage and commit
TaskManager.javawith message"Add TaskManager with file persistence" - Add a
README.mdfile with a title and short description, then commit it - Run
git logto see your three commits
Viewing History
Git keeps every version of your project forever. Knowing how to read the history — and navigate it — turns Git from a backup tool into a time machine.
# Full log — each commit on multiple lines $ git log commit a3f2c91b7e4d8f3a2c1b9e7f5d4a3b2c1d9e7f5 Author: Alex Johnson <alex@example.com> Date: Mon Jan 15 14:32:00 2024 Add input validation to TaskManager.addTask() commit b2e1d9f8c7a6b5e4d3c2b1a9f8e7d6c5b4a3f2 Author: Alex Johnson <alex@example.com> Date: Mon Jan 15 11:15:00 2024 Add TaskManager with file persistence # Compact one-line log $ git log --oneline a3f2c91 Add input validation to TaskManager.addTask() b2e1d9f Add TaskManager with file persistence c1d9e7f Add Task model class d4a3b2c Initial commit — add README # Visual branch graph (very useful with branches!) $ git log --oneline --graph --all * a3f2c91 Add input validation * b2e1d9f Add TaskManager * c1d9e7f Add Task model * d4a3b2c Initial commit # See exactly what changed in a specific commit $ git show a3f2c91 diff --git a/TaskManager.java b/TaskManager.java --- a/TaskManager.java +++ b/TaskManager.java - public void addTask(String title) { - tasks.add(new Task(title, "MEDIUM")); + public void addTask(String title, String priority) { + if (title.isBlank()) { System.out.println("Empty title."); return; } + tasks.add(new Task(title.trim(), priority)); # See who changed which line in a file (blame!) $ git blame TaskManager.java a3f2c91 (Alex Jan 15) public void addTask(String title, String priority) { b2e1d9f (Alex Jan 15) private ArrayList<Task> tasks = new ArrayList<>();
That long hex string like a3f2c91b7e4d... is a unique ID for every commit — called a SHA hash. You never need to type the full thing; the first 7 characters are enough for Git to identify it. In commands like git show or git revert you just use the short form.
Branches
Branches are Git's killer feature. A branch is an independent line of development — you can work on a new feature in complete isolation, and your working code on main is never touched until you're ready.
Think of branches like parallel universes. On main, your task manager works perfectly. You create a branch called feature/due-dates and start adding due date functionality. If it breaks things, no problem — main is completely unaffected. When it's ready, you merge it back.
# See all branches (* marks your current branch) $ git branch * main # Create a new branch $ git branch feature/due-dates # Switch to it $ git checkout feature/due-dates Switched to branch 'feature/due-dates' # Better: create AND switch in one command (modern syntax) $ git switch -c feature/search Switched to a new branch 'feature/search' # Now make changes and commit — only on this branch $ git add . $ git commit -m "Add searchTasks() method with case-insensitive matching" [feature/search 7f3a1b2] Add searchTasks() method # Switch back to main — your changes are NOT here $ git switch main Switched to branch 'main' # See all branches including remote $ git branch -a * main feature/due-dates feature/search # Delete a branch (after merging) $ git branch -d feature/search Deleted branch feature/search (was 7f3a1b2).
Use slashes to organise branches: feature/add-search, bugfix/null-pointer, hotfix/login-crash, release/v1.2.0. Hyphens, not spaces. All lowercase. This is the standard on professional teams.
main and run git switch -c bugfix/login. Where are you now?Merging
Merging brings a branch's changes back into another branch. It's how your feature work eventually lands in main. There are two kinds — and knowing the difference matters.
Fast-forward merge
When main hasn't changed since you branched off, Git can simply move the pointer forward. No new commit is created — it's the cleanest kind of merge.
# You're on main. You want to bring in feature/search. $ git switch main $ git merge feature/search Updating b2e1d9f..7f3a1b2 Fast-forward TaskManager.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) # After a clean merge, delete the branch $ git branch -d feature/search Deleted branch feature/search. # A merge commit (when both branches have diverged) $ git merge feature/due-dates Merge made by the 'ort' strategy. Task.java | 8 ++++++++ TaskManager.java | 6 ++++++ 2 files changed, 14 insertions(+) # See the merge commit in log $ git log --oneline --graph * c9f1a2b Merge branch 'feature/due-dates' into main |\ | * 8e4d3c2 Add overdue task filtering | * 7a1b9f8 Add dueDate field to Task class * | 6b2c4d1 Fix file loading on empty repo |/ * a3f2c91 Add input validation
Rebase — a cleaner alternative
Rebase replays your branch commits on top of the latest main, creating a perfectly linear history. It's preferred for feature branches before merging.
# On your feature branch, rebase onto latest main $ git switch feature/stats $ git rebase main Successfully rebased and updated refs/heads/feature/stats. # Now merge — it'll be a clean fast-forward $ git switch main $ git merge feature/stats Fast-forward
Only rebase branches that you alone are working on. Rebasing rewrites commit history — if someone else has based work on your branch, rebasing destroys their ability to sync. The rule: never rebase main. Rebase feature branches before merging them in.
Merge Conflicts
A conflict happens when two branches both changed the same line of code. Git doesn't know which version to keep — it asks you to decide. Conflicts sound scary; they're actually straightforward once you've seen one.
Git marks conflicts directly inside the affected file with special markers. You open the file, pick the correct version (or combine them), remove the markers, and commit.
Resolving the conflict
Open the file in your editor
VS Code and IntelliJ both highlight conflict markers and offer "Accept Current / Accept Incoming / Accept Both" buttons — much easier than editing manually.
Choose what to keep
Delete the conflict markers (<<<<<<<, =======, >>>>>>>) and write the final version you want. Often you combine both sides.
Stage the resolved file
git add TaskManager.java
Commit the merge
git commit -m "Merge feature/autosave — keep save call and print"
$ git merge feature/autosave CONFLICT (content): Merge conflict in TaskManager.java Automatic merge failed; fix conflicts and then commit the result. # See which files have conflicts $ git status both modified: TaskManager.java # ... edit the file, resolve the conflict ... $ git add TaskManager.java $ git commit -m "Merge feature/autosave — combine save and print" [main d3a2b1c] Merge feature/autosave # Abort a merge if things go wrong (returns to pre-merge state) $ git merge --abort
Every developer encounters conflicts. A team working on the same codebase will hit them regularly. Handling them calmly and correctly is a core professional skill — not a sign that something went wrong.
Push & Pull
So far everything has been local. Now we connect your repo to GitHub — the moment your code becomes shareable, collaborative, and backed up in the cloud.
Create a repo on GitHub
Go to github.com → click the + button → New repository. Name it task-manager. Leave it empty — don't add a README (you already have one locally).
Connect your local repo to GitHub
GitHub will show you the commands. The key one is: git remote add origin https://github.com/yourusername/task-manager.git
Push your commits
git push -u origin main — the -u links your local main to the remote main so future pushes are just git push.
# Add GitHub as the "origin" remote $ git remote add origin https://github.com/alexj/task-manager.git # First push: -u sets upstream tracking $ git push -u origin main Enumerating objects: 9, done. To https://github.com/alexj/task-manager.git * [new branch] main -> main Branch 'main' set up to track remote branch 'main' from 'origin'. # After that, just: $ git push # Push a feature branch to GitHub $ git push -u origin feature/due-dates # Pull = fetch + merge. Gets latest changes from GitHub. $ git pull remote: Enumerating objects: 3, done. Updating a3f2c91..9d1e4b7 Fast-forward README.md | 5 +++++ # Clone an existing repo to your machine $ git clone https://github.com/alexj/task-manager.git Cloning into 'task-manager'... done. # See your remotes $ git remote -v origin https://github.com/alexj/task-manager.git (fetch) origin https://github.com/alexj/task-manager.git (push)
If a teammate pushed while you were working, your push will be rejected. Always run git pull first to get their changes, resolve any conflicts, then push. Making this a habit prevents 90% of collaboration headaches.
Pull Requests
A Pull Request (PR) is a GitHub feature for proposing changes. You push a branch, open a PR, and teammates review your code before it merges into main. It's the core of professional team collaboration.
Pull Requests are how code review works on every professional team. Even solo developers use them to keep a clean history and have a chance to review their own work before merging. When you apply for a job, interviewers will look at your GitHub PRs.
The Pull Request workflow
Create a branch and make your commits
git switch -c feature/sort-tasks → write code → git add . && git commit -m "..."
Push the branch to GitHub
git push -u origin feature/sort-tasks
Open a Pull Request on GitHub
GitHub shows a banner "Compare & pull request." Click it. Write a title and description explaining what you changed and why. Assign reviewers if working in a team.
Code review
Reviewers leave comments on specific lines. You address feedback by pushing new commits to the same branch — the PR updates automatically.
Merge the PR
Once approved, click "Merge pull request" on GitHub. Then clean up: git switch main && git pull && git branch -d feature/sort-tasks
A good PR description explains: what this change does, why it was needed, and how to test it. Include screenshots for UI changes. This becomes searchable history — six months later, someone (including you) will search "why was this changed?" and your PR description is the answer.
Open your first real PR
- Add any small improvement to your Task Manager (sorting, search, statistics)
- Create a branch, commit your changes, push to GitHub
- Open a Pull Request — write a title and at least 2 sentences describing what you built
- Merge it yourself — then pull the merged changes locally
Forking & Open Source
Forking lets you copy someone else's public repository to your account so you can modify it freely — even without permission from the owner. It's how all open-source contribution works.
Fork the repo
On any public GitHub repo, click the Fork button. GitHub creates a copy at github.com/your-username/repo-name. You own this copy.
Clone your fork
git clone https://github.com/your-username/repo-name.git
Add the original as "upstream"
git remote add upstream https://github.com/original-owner/repo-name.git
This lets you pull in updates from the original repo later.
Make your changes on a branch
Never commit directly to main in a fork. Create a branch, make your changes, push to your fork.
Open a PR to the original repo
GitHub lets you open a PR from your fork's branch to the original repo's main. The maintainer reviews and merges — or asks for changes.
# Fetch updates from the original repo $ git fetch upstream From https://github.com/original/repo * [new branch] main -> upstream/main # Merge upstream changes into your local main $ git switch main $ git merge upstream/main Fast-forward # Push the updated main to your fork $ git push
Your first open-source contribution doesn't have to be fixing a complex bug. Fix a typo in documentation. Improve an error message. Add a missing example to a README. Maintainers appreciate all of it, and every merged PR is a public reference that you can work on real code with other developers.
Git Workflows
A workflow is a team agreement about how to use branches. Different teams use different workflows — knowing the common ones means you can join a new team and be productive from day one.
GitHub Flow — simple, widely used
Used by most small-to-medium teams and open-source projects. The rule: main is always deployable. All work happens on feature branches.
main ──●──────────────────────────────────●── \ / feature ──●──●──● (PR review) merge # 1. Branch off main $ git switch -c feature/sort-tasks # 2. Work and commit $ git commit -m "Add sort by priority" $ git commit -m "Add sort by due date" # 3. Push and open PR $ git push -u origin feature/sort-tasks # 4. PR reviewed, approved, merged → delete branch $ git switch main && git pull && git branch -d feature/sort-tasks
Git Flow — for versioned releases
Used by larger teams shipping versioned software. More complex but gives structure for hotfixes and releases.
| Branch | Purpose | Merges into |
|---|---|---|
| main | Production releases only — tagged v1.0, v1.1... | — |
| develop | Integration branch — all features merge here | main (at release) |
| feature/x | Individual features | develop |
| release/x.y | Final stabilisation before release | main + develop |
| hotfix/x | Urgent production fix | main + develop |
As a junior developer joining a team, you'll be told which workflow to use. For your own projects, GitHub Flow is almost always the right choice — it's simple, and simple means fewer mistakes.
main branch?Undoing Mistakes
Git's best feature is being able to undo anything. Committed a bug? Deleted the wrong file? Pushed to the wrong branch? There's a Git command for each scenario — and none of them require panic.
── BEFORE STAGING ───────────────────────────── # Discard changes to a file (back to last commit) $ git restore Main.java # Discard ALL unstaged changes $ git restore . ── AFTER STAGING ────────────────────────────── # Unstage a file (keeps your edits) $ git restore --staged Main.java ── AFTER COMMITTING (local only) ────────────── # Undo last commit — keep changes staged $ git reset --soft HEAD~1 # Undo last commit — keep changes unstaged $ git reset HEAD~1 # Fix a typo in the last commit message (before push) $ git commit --amend -m "Correct message here" # Add a forgotten file to the last commit $ git add ForgottenFile.java $ git commit --amend --no-edit ── AFTER PUSHING (safest undo) ──────────────── # Revert creates a NEW commit that undoes a bad one # (safe because it doesn't rewrite history) $ git revert a3f2c91 [main 8b1d4e2] Revert "Add broken feature" # Temporarily stash work-in-progress $ git stash Saved working directory and index state WIP on main # ... do something else ... then restore: $ git stash pop # Last resort: restore a specific file from a commit $ git checkout a3f2c91 -- Task.java
git push --force rewrites remote history. If anyone else has pulled the branch, their repo is now out of sync. Force pushing to main on a team is one of the fastest ways to cause chaos. The safe alternative is always git revert.
main that introduced a bug. What's the safest way to undo it?.gitignore & Best Practices
Not everything belongs in your repo. Build artifacts, secrets, IDE files, and operating system files should be excluded. The .gitignore file tells Git exactly what to ignore.
# .gitignore file — place in your project root # Compiled Java bytecode — never commit .class files *.class *.jar target/ build/ out/ # IDE files — other developers use their own settings .idea/ *.iml .vscode/ *.eclipse # Operating system files .DS_Store Thumbs.db # NEVER commit secrets or credentials *.env config/secrets.properties application-prod.properties
Best practices every developer follows
| Practice | Why |
|---|---|
| Commit early and often | Small commits are easier to understand, review, and revert |
| Never commit secrets | GitHub repos are public by default; leaked API keys get abused within minutes |
| Write meaningful commit messages | Your team (and future you) will read these at 11pm debugging a production issue |
| Keep main always working | If you break main, you block everyone. Use branches. |
| Pull before you push | Prevents rejected pushes and reduces conflicts |
| Delete merged branches | Keeps the branch list clean and focused |
| Add .gitignore at project start | Adding it later means cleaning up already-committed junk |
| Review your diff before committing | Catch debug prints, TODO comments, accidental changes |
Secrets committed to GitHub are permanently exposed — even if you delete the file later, the history still contains them. If you accidentally commit a secret, you must rotate it (generate a new one) immediately. Use environment variables or a .env file that's in your .gitignore.
Add a proper .gitignore
- Go to gitignore.io (or github.com/github/gitignore) and generate a
.gitignorefor Java + your IDE - Add it to your Task Manager project
- Run
git status— notice how .class files and IDE folders are now hidden - Commit the .gitignore:
"Add .gitignore for Java and IntelliJ"
config/secrets.properties file containing database passwords. What must you do?Capstone: Real Repo
Put everything together. You're going to take your Java Task Manager, give it a professional Git history, publish it to GitHub with a full README, and go through a complete feature-branch-and-PR workflow.
A GitHub repository that any developer — or interviewer — could clone, understand, and contribute to. Clean commits. Good messages. Meaningful branches. A real README. This is what professional repos look like.
Part 1: clean up your history
Initialize the repo (if not done)
git init in your Task Manager folder. Add a .gitignore for Java.
Make atomic commits
Stage and commit each logical piece separately: the Task model, the TaskManager class, the file I/O, the README. Each commit should have a meaningful message.
Push to GitHub
Create a repo on GitHub, add remote, push. Your project is now live.
Part 2: feature branch workflow
Create a feature branch
Pick any improvement: sort by priority, search tasks, statistics report, or due date support. Branch: git switch -c feature/your-feature
Implement and commit
Make at least 2 commits on the branch, each representing a logical step.
Open a Pull Request
Push the branch, open a PR. Write a description: what it does, why you built it, how to test it.
Merge and clean up
Merge the PR. Pull locally. Delete the feature branch. Push the deletion.
Part 3: the README
A good README is the first thing anyone sees. Write one that includes:
- Project title and one-sentence description
- How to build and run it (
javac *.java && java Main) - List of features
- Example output / screenshot
- What you'd build next
$ git log --oneline f1a2b3c Merge pull request #3: Add task search e9d8c7b Add case-insensitive keyword matching to searchTasks() d6e5f4a Add searchTasks() method stub with tests c3b2a1f Merge pull request #2: Add task sorting b9a8f7e Sort by due date using Comparator.comparing() a7b6c5d Add sort by priority — HIGH first 9e8d7c6 Merge pull request #1: Add file persistence 8f7e6d5 Add loadFromFile() with CSV parsing 7a6b5c4 Add saveToFile() using PrintWriter 6c5b4a3 Add TaskManager class with in-memory operations 5b4a3f2 Add Task model with CSV serialization 4a3b2c1 Add .gitignore for Java and IntelliJ 3f2e1d0 Add README with build instructions and feature list
You now know every Git concept a junior developer needs on day one. Next on your roadmap: Maven/Gradle (managing dependencies and builds — your pom.xml goes in the repo), then JUnit 5 (test files live in the repo too), then Spring Boot (the whole project lives in GitHub with CI/CD running your tests automatically on every PR).