import subprocess, tempfile, os, textwrap
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)
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.50.1 (Apple Git-155)
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.
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) cc74c9d] Initial analysis 2 files changed, 3 insertions(+) create mode 100644 README.md create mode 100644 analysis.py cc74c9d 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.
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.
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.")
68e6639 Round the mean to one decimal 9f4dfc4 Add a fourth order and compute the mean cc74c9d 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.
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.
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"))
1c76d58 Add .gitignore 68e6639 Round the mean to one decimal 9f4dfc4 Add a fourth order and compute the mean cc74c9d Initial analysis What did the most recent code change look like? commit 1c76d58: 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.