20 min read

> "The best time to start using version control was when you wrote your first program. The second best time is now."

Learning Objectives

  • Initialize a Git repository and track changes with commits
  • Use branches to work on features without breaking main code
  • Merge branches and resolve simple merge conflicts
  • Push to and pull from remote repositories (GitHub)
  • Write meaningful commit messages and follow Git best practices

Chapter 25: Version Control with Git: Collaborating Like a Professional

"The best time to start using version control was when you wrote your first program. The second best time is now." — Every professional developer who's ever lost work

Chapter Overview

Here's a scene that plays out in every programming course, every semester, without fail.

It's 11 PM. Your project is due at midnight. You've been working on a new feature for the past three hours. You make one more change, hit save, run the program — and everything breaks. Not just the new feature. Everything. Your program won't even start. You can't figure out what you changed. You had a working version an hour ago, but you saved over it. You frantically press Ctrl+Z in your editor, but you closed and reopened the file since then. Your undo history is gone.

Now picture the alternative. You made a snapshot of your working code an hour ago. One command, and you're back to the working version. You can see exactly what you changed since then, line by line. You can even keep the broken version around to study later while you submit the working one. No panic. No sweat.

That's version control. And the tool that makes it possible is called Git.

Git is not a nice-to-have. It's not an advanced tool you'll learn "someday." Every professional software team on Earth uses version control, and the vast majority use Git specifically. GitHub, the most popular platform for hosting Git repositories, has over 100 million developers. When you apply for your first programming job or internship, "experience with Git" will be on the requirements list. When you contribute to an open-source project, you'll use Git. When you collaborate with a partner on a class project, Git will save your grade (and possibly your friendship).

This chapter teaches you to use Git from the ground up. By the end, you'll be making commits, creating branches, merging code, and pushing to GitHub — the same workflow that professional developers use every day.

In this chapter, you will learn to: - Initialize a Git repository and track changes with commits - Use branches to work on features without breaking main code - Merge branches and resolve simple merge conflicts - Push to and pull from remote repositories (GitHub) - Write meaningful commit messages and follow Git best practices

🏃 Fast Track: If you've used Git before, skim 25.1-25.3, slow down at 25.5 (branching) and 25.6 (merging/conflicts), and make sure you understand the workflow diagram in 25.4.

🔬 Deep Dive: Section 25.6 (merge conflicts) and 25.7 (remotes/GitHub) are where collaboration gets real. Section 25.9 covers the professional habits that separate beginners from competent practitioners.


25.1 Why Version Control? (The Nightmare Without It)

Let's look inside a project folder belonging to someone who doesn't use version control:

my_project/
├── report.py
├── report_backup.py
├── report_v2.py
├── report_v2_fixed.py
├── report_v3_FINAL.py
├── report_v3_FINAL_actually_final.py
├── report_v3_FINAL_actually_final_USE_THIS_ONE.py
└── report_old_DO_NOT_DELETE.py

You've seen this. You've probably done this. We all have. It's the universal coping mechanism for people who don't have version control: manually copying files, appending version numbers or desperate pleas to the filenames, and praying you can figure out which one is the real, current, working version.

This approach fails in three specific ways:

Problem 1: You can't see what changed. You have seven copies of the file. What's different between report_v2.py and report_v2_fixed.py? You'd have to read them side by side, line by line, to find out. With version control, you can see exactly which lines changed, when, and who changed them — in seconds.

Problem 2: You can't go back reliably. You saved over the working version. Or you made changes to three files simultaneously and now you don't remember which combination of file versions actually works together. With version control, every snapshot captures the state of all your files at a specific point in time. Going back means going back to a known-good state of the entire project.

Problem 3: Collaboration is a disaster. Elena's colleague edits the report script on Monday. Elena edits the same file on Tuesday without knowing about Monday's changes. One of them saves over the other's work. With version control, both changes are tracked, and the system helps you combine them — or warns you when they conflict.

💡 Intuition: Think of version control as an unlimited undo button for your entire project. Not just one file — every file, going back to the very first line you ever wrote. And unlike Ctrl+Z, it survives closing your editor, restarting your computer, and even switching to a different machine entirely.

Version control is a system that records changes to files over time so that you can recall specific versions later. Git is the most widely used version control system in the world. It was created by Linus Torvalds in 2005 (the same person who created Linux) because he needed a fast, reliable way to manage the Linux kernel — a project with thousands of contributors making changes simultaneously.

🔗 Connection — Spaced Review (Ch 12): In Chapter 12, you learned to organize code into modules and packages — splitting one big file into models.py, storage.py, display.py, and cli.py. Version control becomes essential when your project has multiple files. A single change might touch three files, and you need to track all three as one logical unit. That's exactly what a Git commit does.


25.2 Installing and Configuring Git

25.2.1 Installation

Git is a command-line tool that you run in your terminal. Here's how to install it:

Windows: - Download the installer from https://git-scm.com/downloads - Run the installer and accept the default options (they're sensible) - After installation, open a new terminal and verify:

git --version

Output:

git version 2.43.0.windows.1

macOS: - Open Terminal and type git --version. If Git isn't installed, macOS will prompt you to install the Xcode Command Line Tools. Accept the prompt. - Alternatively, install via Homebrew: brew install git

Linux (Debian/Ubuntu):

sudo apt update && sudo apt install git

25.2.2 First-Time Configuration

Before you make your first commit, you need to tell Git who you are. This information gets attached to every commit you make — it's how collaborators know who made which changes.

git config --global user.name "Elena Vasquez"
git config --global user.email "elena.vasquez@example.com"

These commands set your name and email globally — meaning they'll be used for every Git repository on this computer. You only need to do this once.

You can verify your settings:

git config --list

Output:

user.name=Elena Vasquez
user.email=elena.vasquez@example.com

⚠️ Common Pitfall: Use the same email address for git config that you'll use for your GitHub account (we'll set one up in 25.7). If they don't match, GitHub won't be able to link your commits to your profile, and your contributions won't show up on your GitHub activity graph.


25.3 Your First Repository

A repository (often shortened to repo) is a project folder that Git is tracking. It contains your files plus a hidden .git directory where Git stores the entire history of every change ever made.

Let's create one. We'll use a simple project — a Python script that Elena is building to generate report headers.

25.3.1 Initializing a Repository

mkdir report_project
cd report_project
git init

Output:

Initialized empty Git repository in /home/elena/report_project/.git/

That's it. The git init command creates a hidden .git folder inside report_project. Your directory now looks like this:

report_project/
└── .git/          ← Git's internal data (don't touch this!)

The .git folder is where Git stores everything — every version of every file, every commit message, every branch. You'll never need to look inside it. Just know that if you delete .git, you lose your entire project history.

25.3.2 Creating and Tracking Files

Now let's create a Python file and start tracking it:

# report_header.py
"""Generate formatted report headers for Harbor Community Services."""

from datetime import datetime


def create_header(title: str, author: str) -> str:
    """Return a formatted report header string."""
    date_str = datetime.now().strftime("%Y-%m-%d")
    width = max(len(title), len(f"Author: {author}"), len(f"Date: {date_str}"))
    border = "=" * (width + 4)
    return (
        f"{border}\n"
        f"  {title}\n"
        f"  Author: {author}\n"
        f"  Date: {date_str}\n"
        f"{border}"
    )


if __name__ == "__main__":
    header = create_header("Weekly Impact Report", "Elena Vasquez")
    print(header)

Save this file. Now check the status of your repository:

git status

Output:

On branch main
No commits yet
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        report_header.py

nothing added to commit but untracked files present (use "git add" to track)

Git sees the file but isn't tracking it yet. The file is untracked — Git knows it exists but isn't recording changes to it. We need to explicitly tell Git to start tracking it.

25.3.3 The Add-Commit Workflow

This is the fundamental two-step workflow in Git:

# Step 1: Stage the file (tell Git you want to include it in the next snapshot)
git add report_header.py

# Step 2: Commit (take the snapshot)
git commit -m "Add report header generator"

Output:

[main (root-commit) a1b2c3d] Add report header generator
 1 file changed, 22 insertions(+)
 create mode 100644 report_header.py

You've just made your first commit — a permanent snapshot of your project at this moment in time. Let's break down what happened:

  • git add report_header.py moved the file to the staging area (more on this in 25.4)
  • git commit -m "Add report header generator" created a snapshot with a descriptive message
  • Git assigned a unique identifier (a1b2c3d) — called a commit hash — to this snapshot
  • The message 1 file changed, 22 insertions(+) tells you what the commit contains

25.3.4 Viewing History

Now let's see our commit history:

git log

Output:

commit a1b2c3df4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b (HEAD -> main)
Author: Elena Vasquez <elena.vasquez@example.com>
Date:   Mon Mar 10 14:30:00 2026 -0500

    Add report header generator

One commit so far. Let's make some changes and create another.

Add a new function to report_header.py:

def create_footer(organization: str = "Harbor Community Services") -> str:
    """Return a formatted report footer string."""
    date_str = datetime.now().strftime("%Y-%m-%d %H:%M")
    return (
        f"\n{'─' * 40}\n"
        f"Generated by {organization}\n"
        f"Report timestamp: {date_str}\n"
    )

Now check what Git sees:

git status

Output:

On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
        modified:   report_header.py

Git detected that the file has been modified. You can see exactly what changed:

git diff

Output:

diff --git a/report_header.py b/report_header.py
index 3a4b5c6..7d8e9f0 100644
--- a/report_header.py
+++ b/report_header.py
@@ -18,5 +18,15 @@
     )


+def create_footer(organization: str = "Harbor Community Services") -> str:
+    """Return a formatted report footer string."""
+    date_str = datetime.now().strftime("%Y-%m-%d %H:%M")
+    return (
+        f"\n{'─' * 40}\n"
+        f"Generated by {organization}\n"
+        f"Report timestamp: {date_str}\n"
+    )
+
+
 if __name__ == "__main__":
     header = create_header("Weekly Impact Report", "Elena Vasquez")

Lines starting with + are additions. Lines starting with - are deletions. This is called a diff — it shows exactly what changed between two versions. Stage and commit:

git add report_header.py
git commit -m "Add report footer generator function"

Now git log shows two commits — a growing timeline of your project's evolution.

✅ Best Practice: Commit early, commit often. A good rule of thumb: commit whenever your code is in a working state and you've completed a logical unit of work. Don't wait until you've written 500 lines across 10 files — commit after each meaningful change.


25.4 The Staging Area (Understanding Add then Commit)

This is the concept that confuses most beginners: why do you need two steps? Why can't git commit just save everything that changed?

The answer is the staging area (also called the index). The staging area is a preparation zone between your working directory and the repository's permanent history. It lets you choose exactly which changes go into a commit.

25.4.1 The Three States of Git

Every file in a Git project is in one of three states:

┌─────────────────────────────────────────────────────────────────┐
│                     Git Workflow Diagram                        │
│                                                                 │
│  ┌──────────────┐    git add    ┌──────────────┐   git commit  │
│  │              │ ────────────► │              │ ────────────►  │
│  │   WORKING    │               │   STAGING    │               │
│  │  DIRECTORY   │               │    AREA      │               │
│  │              │ ◄──────────── │              │               │
│  │ (your files  │  (edit files) │ (selected    │               │
│  │  as you see  │               │  changes     │               │
│  │  them)       │               │  ready to    │               │
│  └──────────────┘               │  commit)     │               │
│                                 └──────────────┘               │
│                                                                 │
│                                        │                        │
│                                        ▼                        │
│                                ┌──────────────┐                │
│                                │              │                │
│                                │  REPOSITORY  │                │
│                                │  (.git)      │                │
│                                │              │                │
│                                │ (permanent   │                │
│                                │  history of  │                │
│                                │  all commits)│                │
│                                └──────────────┘                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
  1. Working Directory — your actual files on disk, in the state you can see and edit
  2. Staging Area — a selection of changes you've chosen to include in the next commit
  3. Repository — the permanent history of all commits (stored in .git)

25.4.2 Why the Staging Area Matters

Imagine you've been working for an hour. You fixed a bug in storage.py, added a new feature in display.py, and cleaned up some formatting in cli.py. These are three unrelated changes. Without a staging area, you'd have to commit them all together as one messy "did a bunch of stuff" commit. With the staging area, you can commit them separately:

# Commit 1: the bug fix
git add storage.py
git commit -m "Fix off-by-one error in task deletion"

# Commit 2: the new feature
git add display.py
git commit -m "Add color-coded priority display"

# Commit 3: the cleanup
git add cli.py
git commit -m "Clean up formatting in CLI module"

Three focused commits, each telling a clear story. Anyone reading the history knows exactly what happened and when.

You can also stage all modified files at once when a single commit captures the full story:

git add .          # Stage everything in the current directory
git commit -m "Implement task export feature across all modules"

⚠️ Common Pitfall: git add . stages everything, including files you might not want to commit (temporary files, API keys, virtual environment folders). We'll address this with .gitignore in 25.8.

🔗 Connection — Spaced Review (Ch 19): Remember the idea of choosing the right algorithm for the situation? The staging area is the same principle applied to workflow. Sometimes you want surgical precision (one file at a time), sometimes you want brute force (git add .). Context determines which is appropriate.


25.5 Branching (Working on Features Without Breaking Main)

Branching is where version control transforms from "a fancy undo button" into a genuine superpower. A branch is an independent line of development. You can create a branch, experiment freely, and if the experiment works, merge it back into your main code. If it doesn't? Delete the branch and walk away. Nothing was harmed.

25.5.1 The Main Branch

When you create a repository, Git starts you on a branch called main (older repositories may call it master). This is your default branch — it should always contain code that works.

git branch

Output:

* main

The * shows which branch you're currently on.

25.5.2 Creating and Switching Branches

Elena wants to add a table-of-contents feature to the report generator, but she's not sure it'll work out. She creates a branch:

# Create a new branch called "add-table-of-contents"
git branch add-table-of-contents

# Switch to that branch
git switch add-table-of-contents

Output:

Switched to branch 'add-table-of-contents'

Or combine both steps:

git switch -c add-table-of-contents    # Create AND switch in one command

💡 Intuition: Think of branches as parallel timelines. The main timeline keeps running undisturbed while you create a side timeline to try something. If the side timeline works out, you merge it into the main timeline. If it doesn't, the main timeline was never affected.

Now Elena writes her feature on this branch:

def create_table_of_contents(sections: list[str]) -> str:
    """Return a formatted table of contents from a list of section titles."""
    lines = ["Table of Contents", "=" * 20]
    for i, section in enumerate(sections, start=1):
        lines.append(f"  {i}. {section}")
    lines.append("")  # blank line after TOC
    return "\n".join(lines)

She adds and commits — but this commit only exists on the add-table-of-contents branch:

git add report_header.py
git commit -m "Add table of contents generator"

If she switches back to main, the table-of-contents function disappears from her file:

git switch main

The file is back to its pre-branch state. The new function only exists on the feature branch. This is the power of branches — your main branch remains clean and stable while you experiment somewhere else.

25.5.3 Viewing Branches

git branch           # List all local branches
git branch -v        # List with latest commit message

Output:

  add-table-of-contents   c4d5e6f Add table of contents generator
* main                    b2c3d4e Add report footer generator function

25.5.4 The Text Adventure Branch Strategy

Here's a real-world example. The Crypts of Pythonia team (from our running example) has three developers working on different features simultaneously:

main ─────────────────────────────────────────────────►
       │                    │                    │
       ├── add-magic-system │                    │
       │                    ├── fix-save-bug     │
       │                    │                    ├── new-dungeon-level
       │                    │                    │

Each developer works on their own branch. Nobody steps on anyone else's toes. When a feature is complete and tested, it gets merged back into main. This is the feature branch workflow, and it's how most professional teams operate.


25.6 Merging (Bringing Branches Together)

Once your feature branch is complete and working, you'll want to combine it with main. This is called merging.

25.6.1 A Clean Merge (Fast-Forward)

If nobody else has changed main since you created your branch, Git can simply "fast-forward" — it moves main to point at your latest commit. No extra work needed.

# Step 1: Switch to the branch you want to merge INTO
git switch main

# Step 2: Merge the feature branch
git merge add-table-of-contents

Output:

Updating b2c3d4e..c4d5e6f
Fast-forward
 report_header.py | 9 +++++++++
 1 file changed, 9 insertions(+)

The Fast-forward message means Git simply moved the main pointer forward. Your feature is now part of main.

25.6.2 A Three-Way Merge

Sometimes both branches have new commits. Git creates a new merge commit that combines both sets of changes:

main:    A ── B ── C ── D
                        \
feature:         E ── F ── G

After merging:

main:    A ── B ── C ── D ── M  (merge commit)
                        \   /
feature:         E ── F ── G

Git is usually smart enough to combine changes automatically, as long as the two branches modified different parts of the code.

25.6.3 Merge Conflicts (When Git Needs Your Help)

A merge conflict happens when two branches modify the same lines in the same file. Git can't decide which version to keep, so it asks you to resolve the conflict manually.

Let's walk through an example. Elena and her colleague Marcus both edit report_header.py. Elena changes the border character on a branch called new-border:

# Elena's version (on branch "new-border"):
border = "━" * (width + 4)   # Changed from "=" to "━"

Meanwhile, Marcus changes the same line on main:

# Marcus's version (on main):
border = "#" * (width + 4)   # Changed from "=" to "#"

When Elena tries to merge her branch into main:

git switch main
git merge new-border

Output:

Auto-merging report_header.py
CONFLICT (content): Merge conflict in report_header.py
Automatic merge failed; fix conflicts and then commit the result.

Git tried to merge automatically but couldn't. If you open report_header.py, you'll see conflict markers:

<<<<<<< HEAD
    border = "#" * (width + 4)
=======
    border = "━" * (width + 4)
>>>>>>> new-border

Here's what those markers mean: - <<<<<<< HEAD — start of the version on your current branch (main) - ======= — separator between the two versions - >>>>>>> new-border — end of the version from the branch you're merging in

To resolve the conflict:

  1. Open the file in your editor
  2. Decide which version to keep (or write a combination)
  3. Delete the conflict markers (<<<<<<<, =======, >>>>>>>)
  4. Save the file
  5. Stage and commit
# After resolving — Elena decides to use her version:
    border = "━" * (width + 4)
git add report_header.py
git commit -m "Merge new-border: use box-drawing border character"

🐛 Debugging Walkthrough: Merge Conflicts

Merge conflicts are not errors — they're Git asking for your judgment. Don't panic. The process is always the same:

  1. Run git status to see which files have conflicts
  2. Open each conflicted file and look for <<<<<<< markers
  3. Decide what the correct code should be
  4. Remove all conflict markers
  5. git add the resolved files
  6. git commit to complete the merge

Tip: Most code editors (VS Code, PyCharm) highlight merge conflicts with colors and offer "Accept Current," "Accept Incoming," or "Accept Both" buttons. Use them — they save time and reduce mistakes.


25.7 Remote Repositories and GitHub

So far, everything has been local — on your own computer. But version control really shines when you connect to a remote repository, which is a copy of your repo hosted on a server. This gives you:

  • Backup: Your code survives even if your laptop catches fire
  • Collaboration: Other people can access, clone, and contribute to your project
  • Visibility: A GitHub profile shows potential employers your work

GitHub is the most popular platform for hosting Git repositories. Others include GitLab and Bitbucket, but the concepts are identical.

25.7.1 Creating a GitHub Repository

  1. Go to github.com and create a free account
  2. Click the "+" icon in the top right, then "New repository"
  3. Name it report-project
  4. Leave it public (free accounts get unlimited public repos)
  5. Don't initialize with a README (we already have a local repo)
  6. Click "Create repository"

GitHub will show you instructions for connecting your existing local repo. Here's what you'll run:

25.7.2 Connecting Local to Remote

# Tell Git about the remote repository
git remote add origin https://github.com/evasquez/report-project.git

# Push your local commits to GitHub
git push -u origin main

Let's unpack these commands:

  • git remote add origin <url> — registers a remote called origin (the conventional name for the primary remote) at the given URL
  • git push -u origin main — uploads your main branch to the remote. The -u flag sets up tracking so future pushes just need git push

Output:

Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Writing objects: 100% (9/9), 1.24 KiB | 1.24 MiB/s, done.
Total 9 (delta 2), reused 0 (delta 0)
To https://github.com/evasquez/report-project.git
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

Your code is now on GitHub. Refresh the page and you'll see your files.

25.7.3 The Push/Pull Workflow

Once connected, the daily workflow looks like this:

┌─────────────────────────────────────────────────────────────────┐
│                  Remote Workflow Diagram                        │
│                                                                 │
│              ┌────────────────────────┐                         │
│              │    REMOTE (GitHub)     │                         │
│              │                        │                         │
│              │  The shared "source    │                         │
│              │  of truth" that        │                         │
│              │  everyone syncs with   │                         │
│              └───────┬────────────────┘                         │
│                      │        ▲                                 │
│           git pull / │        │ git push                        │
│           git clone  │        │                                 │
│                      ▼        │                                 │
│              ┌────────────────────────┐                         │
│              │   LOCAL REPOSITORY     │                         │
│              │                        │                         │
│              │  Your copy — you       │                         │
│              │  commit here, then     │                         │
│              │  push to share         │                         │
│              └────────────────────────┘                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
  • git push — uploads your new commits to the remote
  • git pull — downloads new commits from the remote to your local repo
# After making new commits locally:
git push

# Before starting work each day (to get teammates' changes):
git pull

25.7.4 Cloning an Existing Repository

If someone else already has a project on GitHub and you want a copy:

git clone https://github.com/someone/their-project.git

This creates a new directory called their-project with the full repository and all its history. You don't need git init — cloning sets everything up automatically.

25.7.5 Forks and Pull Requests

Two more terms you'll encounter on GitHub:

A fork is your personal copy of someone else's repository on GitHub. You fork their repo, make changes in your fork, and then propose your changes back to the original via a pull request (PR). This is how open-source collaboration works — you don't need write access to someone's repo to suggest improvements.

The pull request workflow: 1. Fork the repository on GitHub 2. Clone your fork locally 3. Create a branch, make changes, commit 4. Push the branch to your fork 5. Open a pull request on GitHub from your branch to the original repo 6. The maintainers review your changes and merge (or request modifications)

💡 Intuition: A fork is like photocopying someone's recipe book so you can experiment with the recipes. A pull request is showing them your improved recipe and asking, "Want to add this to your book?"


25.8 .gitignore (What to Exclude)

Not every file in your project should be tracked by Git. Some files are generated automatically, contain sensitive information, or are specific to your machine. The .gitignore file tells Git to ignore them.

25.8.1 Creating a .gitignore

Create a file called .gitignore (note the leading dot) in the root of your repository:

# .gitignore for a Python project

# Byte-compiled / optimized files
__pycache__/
*.py[cod]
*$py.class

# Virtual environments
venv/
.venv/
env/

# IDE settings
.vscode/
.idea/
*.swp
*.swo

# OS-generated files
.DS_Store
Thumbs.db

# Environment variables and secrets
.env
*.secret
config_local.py

# Distribution / packaging
dist/
build/
*.egg-info/

# Test and coverage reports
.pytest_cache/
htmlcov/
.coverage

Add and commit the .gitignore file itself — it's part of your project:

git add .gitignore
git commit -m "Add .gitignore for Python project"

25.8.2 Why These Patterns?

Pattern Why Exclude It
__pycache__/ Generated automatically by Python; different on every machine
venv/ Virtual environments are large and machine-specific (Chapter 23)
.vscode/ Editor preferences vary by developer
.env Contains API keys, passwords — never commit secrets
*.pyc Compiled bytecode; regenerated every time you run your code
.DS_Store macOS system file; irrelevant to your project

⚠️ Security Warning: If you accidentally commit a file containing passwords or API keys, removing it from the current version is NOT enough. The file still exists in Git's history. If this happens, consider the secret compromised — change the password or rotate the API key immediately. See Case Study 2 for recovery techniques.


25.9 Best Practices (Professional Habits)

25.9.1 Write Meaningful Commit Messages

Your commit messages are a gift to your future self (and your collaborators). Here's the difference between helpful and useless messages:

Bad commit messages:

fix stuff
update
asdfasdf
WIP
changes
final version

Good commit messages:

Fix off-by-one error in task deletion
Add CSV export for completed tasks
Refactor report generator to use template pattern
Update README with installation instructions
Handle empty input in search function

A good commit message: - Starts with a verb in the imperative mood ("Add," "Fix," "Refactor," not "Added," "Fixed," "Refactored") - Is specific about what changed and why - Is a single line for simple changes (50 characters or less is ideal) - For complex changes, adds a blank line and a detailed explanation below

git commit -m "Fix crash when task list is empty

The delete_task() function assumed the task list always had
at least one element. Added a guard clause to handle the
empty list case and display a user-friendly message instead
of crashing with an IndexError."

25.9.2 Commit Logical Units of Work

Each commit should represent one logical change. Don't mix unrelated changes in a single commit:

# Good: three focused commits
git commit -m "Fix priority validation bug"
git commit -m "Add due date field to Task class"
git commit -m "Update README with new features"

# Bad: one giant commit
git commit -m "Fix bug, add feature, update docs"

25.9.3 Use Branches for Everything

Even when working solo, branches are valuable:

git switch -c experiment-new-ui     # Try a risky redesign
# ... work, commit, test ...
# It worked? Merge it!
git switch main
git merge experiment-new-ui
# It didn't work? No harm done.
git switch main
git branch -d experiment-new-ui     # Delete the failed branch

25.9.4 Pull Before You Push

Always git pull before git push when working with others. This ensures you have the latest changes and avoids unnecessary merge conflicts.

25.9.5 Never Commit Secrets

This is worth repeating: never commit passwords, API keys, tokens, or other secrets. Use .gitignore to exclude files containing secrets. Use environment variables instead of hardcoding credentials. If you accidentally commit a secret, treat it as compromised immediately.

📊 Professional Context: A 2024 GitGuardian report found that over 12 million secrets (API keys, passwords, certificates) were exposed in public GitHub repositories in a single year. Automated scanners constantly crawl GitHub looking for accidentally committed credentials. If you push an AWS key to a public repo, it can be exploited within minutes.


25.10 Project Checkpoint: TaskFlow v2.4

It's time to put TaskFlow under version control. This is a milestone moment — from now on, every change you make to TaskFlow will be tracked, reversible, and shareable.

Step 1: Initialize the Repository

Navigate to your TaskFlow project directory and initialize Git:

cd ~/projects/taskflow
git init

Step 2: Create a .gitignore

Before your first commit, create a .gitignore so Git doesn't track files it shouldn't:

# .gitignore
__pycache__/
*.py[cod]
venv/
.venv/
.env
.pytest_cache/
*.egg-info/
.coverage
htmlcov/

Step 3: Write a README

Every project needs a README.md — it's the first thing people see when they visit your repository. Create one:

# TaskFlow CLI

A command-line task and project manager built in Python 3.12+.

## Features

- Add, edit, and delete tasks with priorities (high/medium/low)
- Search tasks by keyword with regex support
- Category-based organization
- JSON persistence — tasks survive between sessions
- CSV and JSON export/import
- Colorful terminal output with rich
- Automated daily report generation

## Installation

1. Clone the repository:
   ```bash
   git clone https://github.com/yourusername/taskflow.git
   cd taskflow
   ```

2. Create a virtual environment and install dependencies:
   ```bash
   python -m venv venv
   source venv/bin/activate   # On Windows: venv\Scripts\activate
   pip install -r requirements.txt
   ```

3. Run TaskFlow:
   ```bash
   python -m taskflow
   ```

## Usage

```bash
python -m taskflow                  # Start interactive mode
python -m taskflow add "Buy milk"   # Quick-add a task
python -m taskflow list             # Show all tasks
python -m taskflow search "milk"    # Search tasks

Project Structure

taskflow/
├── cli.py          # Command-line interface
├── models.py       # Task and TaskList classes
├── storage.py      # JSON persistence
├── display.py      # Terminal output formatting
└── utils.py        # Helper functions

License

MIT


### Step 4: Make the Initial Commit

```bash
git add .gitignore README.md
git add cli.py models.py storage.py display.py utils.py
git add requirements.txt
git commit -m "Initial commit: TaskFlow v2.3 under version control

Add all project files, README with usage instructions,
and .gitignore for Python project hygiene."

Step 5: Create a Feature Branch

Let's add a small feature on a branch — a version command that displays TaskFlow's version number:

git switch -c add-version-command

Add this to cli.py:

# Add to the argument parser or menu system:
TASKFLOW_VERSION = "2.4.0"

def show_version() -> None:
    """Display the current TaskFlow version."""
    print(f"TaskFlow CLI v{TASKFLOW_VERSION}")
    print("Built with Python 3.12+")
    print("https://github.com/yourusername/taskflow")

Commit and merge:

git add cli.py
git commit -m "Add version command to display TaskFlow version info"

git switch main
git merge add-version-command

Step 6: Push to GitHub (Conceptual)

If you've created a GitHub account and repository:

git remote add origin https://github.com/yourusername/taskflow.git
git push -u origin main

Congratulations — TaskFlow is now a real, version-controlled, published project. Every change from here forward is tracked, every experiment is safe, and anyone in the world can find and use your work.

🎯 Threshold Concept Check: Version Control as a Time Machine

If you've followed along with this chapter, you now have a superpower that most beginner programmers don't: the ability to experiment fearlessly. Want to try a risky refactoring of your entire module structure? Make a branch. Want to rewrite your search algorithm from scratch? Make a branch. If it works, merge it. If it doesn't, you haven't lost a single character of your working code.

This is why professionals commit constantly and branch liberally. It's not because they're disciplined — it's because they're free. Version control removes the fear of breaking things, and removing fear is how you learn faster and build better software.

Before this chapter, every change you made was permanent and irreversible. From now on, nothing is. Let that sink in.


Chapter Summary

You've learned the core Git workflow that professional developers use every day:

Concept What It Does Key Command(s)
Repository A project tracked by Git git init
Staging area Select changes for the next commit git add
Commit A permanent snapshot of your project git commit -m "message"
Branch An independent line of development git branch, git switch
Merge Combine branches together git merge
Remote A server-hosted copy of your repo git remote add, git push, git pull
Clone Copy a remote repo to your machine git clone
.gitignore Tell Git which files to skip Create .gitignore file

The essential daily workflow:

git pull                    # Get latest changes
# ... write code ...
git add <files>             # Stage your changes
git commit -m "message"     # Commit with a clear message
git push                    # Share with the team

And the feature branch workflow:

git switch -c new-feature   # Create and switch to a branch
# ... write code, commit ...
git switch main             # Go back to main
git merge new-feature       # Merge the feature in
git push                    # Push the updated main

What's Next: In Chapter 26, we'll zoom out from individual tools and look at the software development lifecycle as a whole — how professional teams plan, build, test, deploy, and maintain software. Git is one piece of that puzzle; now you'll see how all the pieces fit together.