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 · Start from a working main branch¶
Set up a repository with one solid commit on the main branch, our stable starting point.
repo = new_repo()
write(repo, "model.py", "features = ['age', 'income']\nprint('training on', features)\n")
git(repo, "add", "."); git(repo, "commit", "-m", "Baseline model with two features")
print(git(repo, "log", "--oneline"))
print("current branch:", git(repo, "branch", "--show-current"))
d2a465b Baseline model with two features
current branch: main
DEMO 2 · Branch off to try an idea¶
Create a branch to test a new feature without risking main. git switch -c makes a branch and moves onto it. Commits here do not affect main at all.
git(repo, "switch", "-c", "add-feature")
write(repo, "model.py", "features = ['age', 'income', 'region']\nprint('training on', features)\n")
git(repo, "commit", "-am", "Add region as a third feature")
print("on branch:", git(repo, "branch", "--show-current"))
print(git(repo, "log", "--oneline"))
print("\nmeanwhile main is untouched:")
print(git(repo, "log", "main", "--oneline"))
on branch: add-feature 7204735 Add region as a third feature d2a465b Baseline model with two features meanwhile main is untouched: d2a465b Baseline model with two features
DEMO 3 · Merge the finished idea back¶
Happy with the experiment? Switch back to main and merge the branch in. The new work joins the stable line, and the history records that it happened.
git(repo, "switch", "main")
# --no-ff records the merge as its own commit, so the history shows the branch happened
print(git(repo, "merge", "--no-ff", "add-feature", "-m", "Merge: add region feature"))
print("\nmain now includes the feature, with a merge commit on top:")
print(git(repo, "log", "--oneline", "--graph"))
Merge made by the 'ort' strategy. model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) main now includes the feature, with a merge commit on top: * a1d20e5 Merge: add region feature |\ | * 7204735 Add region as a third feature |/ * d2a465b Baseline model with two features
DEMO 4 · A merge conflict, and how to resolve it¶
When two branches change the same line differently, git cannot guess which is right, so it stops and asks you. That is a merge conflict, and it is a normal, safe event. Here we create one on purpose.
# two branches edit the SAME line of a shared file
write(repo, "config.py", "threshold = 0.50\n")
git(repo, "add", "."); git(repo, "commit", "-m", "Set threshold to 0.50")
git(repo, "switch", "-c", "tune-threshold")
write(repo, "config.py", "threshold = 0.65\n"); git(repo, "commit", "-am", "Raise threshold to 0.65")
git(repo, "switch", "main")
write(repo, "config.py", "threshold = 0.40\n"); git(repo, "commit", "-am", "Lower threshold to 0.40")
print(git(repo, "merge", "tune-threshold")) # <- conflict
print("\nthe file now shows both sides, marked by git:")
print(open(os.path.join(repo, "config.py")).read())
Auto-merging config.py CONFLICT (content): Merge conflict in config.py Automatic merge failed; fix conflicts and then commit the result. the file now shows both sides, marked by git: <<<<<<< HEAD threshold = 0.40 ======= threshold = 0.65 >>>>>>> tune-threshold
DEMO 5 · Decide, then finish the merge¶
You resolve a conflict by editing the file to the version you want, deleting git's marker lines, then staging and committing. You are the judge; git just kept both options safe until you chose.
write(repo, "config.py", "threshold = 0.60\n") # the human decides the final value
git(repo, "add", "config.py")
git(repo, "commit", "-m", "Resolve threshold conflict: settle on 0.60")
print(git(repo, "log", "--oneline", "--graph"))
print("\nfinal config:", open(os.path.join(repo, "config.py")).read().strip())
* 5d37eda Resolve threshold conflict: settle on 0.60 |\ | * 7591990 Raise threshold to 0.65 * | fd33fdb Lower threshold to 0.40 |/ * ff74c2a Set threshold to 0.50 * a1d20e5 Merge: add region feature |\ | * 7204735 Add region as a third feature |/ * d2a465b Baseline model with two features final config: threshold = 0.60
Wrap-up¶
A branch is a safe sandbox; merge brings finished work back to main; and a conflict is just git refusing to guess when two changes collide, handing you the decision. This branch-commit-merge cycle, multiplied across a team on a shared host like GitHub, is how modern software and analysis get built together. Last, we tie versioning back to reproducible experiments.