Running Git Commands from Python
Once scaffold writes all the project files, it needs to turn the directory into an actual git repository - run git init, stage everything, and make the first commit. I went with running the git binary as a subprocess rather than using a library like GitPython. It meant only less dependency, and I already knew the exact git command I needed. No reason to learn a new library's API for something the command line already handles well.
The subprocess call
Here's the helper that wraps every git command scaffoldr runs:
def _git(args: list[str], cwd: Path) -> None:
result = subprocess.run(
["git", *args],
cwd=cwd,
capture_output=True,
text=True,
)
if result.returncode != 0:
typer.echo(f"git error: {result.stderr.strip()}", err=True)
raise typer.Exit(code=1)
subprocess.run executes a command as if you'd typed it in a terminal. The first argument is a list - ["git", *args] - where *args unpacks whatever command-specific arguments get passed in. cwd tells it which directory to run the command in.
Why cwd matters
subprocess.run runs the command starting from Python's own working directory by default - wherever you launched scaffoldr from, not the directory you're turning into a repo. Passing cwd tells it to run the command as if you'd cd'd into that directory first. Without this, git init would initialize a repo in the wrong place.
Capturing output intead of letting it print
capture_output=True redirects the subprocess's stdout and stderr into the result object instead of letting them print directly to the terminal. text=True decodes the output as a string instead of raw bytes, so you can call .strip() directly on result.stderr.
This matters because raw subprocess output would be messy - git's own stdout/stderr mixed in with whatever else scaffoldr is printing at the same time. Capturing it means the error message stays clean and readable: we can print just the relevant part.
Checking for failure
result.returncode is the exit code the git process finished with. 0 means success - this is a convention nearly every command-line program follows, not something specific to git. Any non-zero value means something went wrong.
If returncode != 0, scaffoldr prints the captured stderr and exits with typer.Exit(code=1). This propagates the failure up - if git init fails, scaffold doesn't continue trying to git add and git commit on a repo that was never created.
Running the sequence
Back in scaffold, the actual git setup is three calls:
_git(["init"], cwd=root)
_git(["add", "."], cwd=root)
_git(["commit", "-m", "chore: initial scaffold"], cwd=root)
Initialize the repo, stage every file, commit. Each calls reuses the same _git helper, so error handling for all three is identical - no repeated subprocess.run boilerplate for each command.
What's next
With local scaffolding done - files written, git repo initialized - the next step was making scaffoldr new actually talk to GitHub. That means authenticating, creating a repo through the API, and connecting the local repo to it. That's the next post.
The code is on GitHub
