←Back to the bookChapter 156 · Notebook 3 · Git Basics
▶ Open in Colab⬇ Download notebookView on GitHub
Chapter 156 · Tools & Workflow · Notebook 3 of 5

Git Basics: A History You Can Trust

Git records the full history of your project as a series of snapshots called commits. It answers what changed, when, why, and by whom, and lets you go back to any point. Here we run real git commands against a throwaway repository.
In [1]:
import subprocess, tempfile, os, textwrap
# one fixed date for every commit, so the commit hashes come out the same on every run and yours match these
GIT_DATES = {"GIT_AUTHOR_DATE": "2026-01-15T09:00:00+00:00", "GIT_COMMITTER_DATE": "2026-01-15T09:00:00+00:00"}
def sh(*args, cwd):
    "Run a shell command in a directory and return its combined output."
    r = subprocess.run(args, cwd=cwd, capture_output=True, text=True, env={**os.environ, **GIT_DATES})
    return (r.stdout + r.stderr).strip()
def git(repo, *args):
    return sh("git", *args, cwd=repo)
def new_repo():
    "Create a fresh, isolated git repository in a temp folder (never touches your real work)."
    d = tempfile.mkdtemp(prefix="repro_")
    git(d, "init", "-q", "-b", "main")
    git(d, "config", "user.name", "Analyst"); git(d, "config", "user.email", "analyst@example.com")
    return d
def write(repo, name, text):
    with open(os.path.join(repo, name), "w") as f: f.write(textwrap.dedent(text).lstrip("\n"))
print("git", git(tempfile.gettempdir(), "--version").split()[-1] if False else subprocess.run(["git","--version"],capture_output=True,text=True).stdout.strip())
git git version 2.43.0

DEMO 1 · Create a repository and make the first commit¶

Three commands cover the everyday loop. git init starts tracking a folder. git add stages the changes you want to record. git commit saves them as a snapshot with a message explaining why.

In [2]:
repo = new_repo()
write(repo, "analysis.py", "revenue = [120, 80, 300]\nprint('total:', sum(revenue))\n")
write(repo, "README.md", "# Sales Analysis\n")
git(repo, "add", ".")
print(git(repo, "commit", "-m", "Initial analysis"))
print("\n" + git(repo, "log", "--oneline"))
[main (root-commit) fee3ff4] Initial analysis
 2 files changed, 3 insertions(+)
 create mode 100644 README.md
 create mode 100644 analysis.py

fee3ff4 Initial analysis

DEMO 2 · See what changed with status and diff¶

Edit a file and git can tell you exactly what is different from the last commit. git status lists changed files; git diff shows the line-by-line changes, plus in green, minus in red.

In [3]:
write(repo, "analysis.py", "revenue = [120, 80, 300, 40]\nprint('total:', sum(revenue))\nprint('mean:', sum(revenue)/len(revenue))\n")
print("STATUS:"); print(git(repo, "status", "--short"))
print("\nDIFF:"); print(git(repo, "diff"))
STATUS:
M analysis.py

DIFF:
diff --git a/analysis.py b/analysis.py
index 8b04a9d..fd091b3 100644
--- a/analysis.py
+++ b/analysis.py
@@ -1,2 +1,3 @@
-revenue = [120, 80, 300]
+revenue = [120, 80, 300, 40]
 print('total:', sum(revenue))
+print('mean:', sum(revenue)/len(revenue))

DEMO 3 · Commit the change, and grow the history¶

Stage and commit the edit with a message that says why. Now the log has two entries, and you can return to either. A good commit message is a gift to your future self.

In [4]:
git(repo, "add", "analysis.py")
git(repo, "commit", "-m", "Add a fourth order and compute the mean")
write(repo, "analysis.py", "revenue = [120, 80, 300, 40]\nprint('total:', sum(revenue))\nprint('mean:', round(sum(revenue)/len(revenue), 1))\n")
git(repo, "commit", "-am", "Round the mean to one decimal")
print(git(repo, "log", "--oneline"))
print("\n3 commits: a history you can read, blame, and roll back to.")
5dd4711 Round the mean to one decimal
21ea085 Add a fourth order and compute the mean
fee3ff4 Initial analysis

3 commits: a history you can read, blame, and roll back to.

DEMO 4 · .gitignore: keep the wrong things out¶

Not everything belongs in version control: big data files, secrets, and generated outputs should stay out. A .gitignore lists patterns git will refuse to track, so you never commit a password or a gigabyte of data by accident.

In [5]:
write(repo, "secrets.env", "API_KEY=do-not-commit-me\n")
os.makedirs(os.path.join(repo, "data"), exist_ok=True)
write(repo, "data/big.csv", "x,y\n1,2\n")
print("Before .gitignore, git wants to track these:")
print(git(repo, "status", "--short"))
write(repo, ".gitignore", "secrets.env\ndata/\n")
git(repo, "add", ".gitignore"); git(repo, "commit", "-m", "Add .gitignore")
print("\nAfter .gitignore, the secret and the data are invisible to git:")
print(git(repo, "status", "--short") or "(nothing to commit, working tree clean)")
Before .gitignore, git wants to track these:
?? data/
?? secrets.env
After .gitignore, the secret and the data are invisible to git:
(nothing to commit, working tree clean)

DEMO 5 · Time travel: inspect any past version¶

Because every commit is a full snapshot, you can look at the project as it was at any point. git log with a format shows the history; git show reveals what a specific commit changed.

In [6]:
print(git(repo, "log", "--pretty=format:%h  %s"))
print("\nWhat did the most recent code change look like?")
print(git(repo, "show", "--stat", "HEAD", "--pretty=format:commit %h: %s"))
8d28c7f  Add .gitignore
5dd4711  Round the mean to one decimal
21ea085  Add a fourth order and compute the mean
fee3ff4  Initial analysis

What did the most recent code change look like?
commit 8d28c7f: Add .gitignore
 .gitignore | 2 ++
 1 file changed, 2 insertions(+)

Wrap-up¶

The core loop is edit, add, commit, and it builds a trustworthy history you can inspect with status, diff, and log, and protect with .gitignore. Every snapshot is recoverable, so experimentation is safe. That safety is what makes the next step, branching, possible.

←Back to the bookChapter 156 · Notebook 3 · Git Basics