Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,16 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node.js from .nvmrc
if: ${{ hashFiles('package.json') != '' && hashFiles('.nvmrc') != '' }}
uses: actions/setup-node@v4
# 3.11 is the minimum version this project supports. If it passes here,
# it will pass on the newer versions people run locally.
- name: Set up Python
uses: actions/setup-python@v5
with:
node-version-file: .nvmrc
python-version: "3.11"
cache: pip
cache-dependency-path: pyproject.toml

- name: Set up default Node.js
if: ${{ hashFiles('package.json') != '' && hashFiles('.nvmrc') == '' }}
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install Node dependencies
if: ${{ hashFiles('package.json') != '' }}
- name: Install dependencies
run: scripts/install-dependencies.sh

- name: Run quality checks
Expand Down
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Python
__pycache__/
*.py[cod]
.venv/
venv/
*.egg-info/

# Build output
dist/
build/

# Tooling caches
.pytest_cache/
.ruff_cache/

# Repositories created by minigit itself. The object store is minigit's own
# storage; it should not be committed into Git.
.minigit/

# Editors and OS
.DS_Store
.idea/
.vscode/
123 changes: 78 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,103 @@
# 2026 Tech Project Template
# mini-git

Template repository for student tech projects.
A version control system built from scratch in Python: a content-addressable
object store, a staging area, commit history as a DAG, branching, merging, and
push/pull over a TCP socket.

The CI workflow and local hook scripts are examples for a Node.js project. Update them to match the language, package manager, linter, and test runner used by the project created from this template.
The core logic is written by hand. No `libgit2`, no shelling out to system Git,
no existing VCS libraries for storage, diffing, or merging. Hashing,
compression, sockets, argument parsing, and testing libraries are fine.

This template includes pull requests, branch protection, CI checks, and optional Git hooks to give student developers practice with workflows used on real engineering teams. These checks also help keep the repository cleaner by catching formatting, linting, and test issues before code is merged.
## Getting Started

## Repository Setup

After creating a repository from this template:

1. Configure branch protection for `main` using [docs/branch-protection.md](docs/branch-protection.md).

2. Optional: install local Git hooks:

```bash
scripts/install-git-hooks.sh
```

3. Replace or adjust the example quality checks as needed. The included Node.js checks run these commands when they are defined in `package.json`:

- `npm run lint`
- `npm run test`

## Pull Request Flow

- Work on a feature branch.
- Open a pull request into `main`.
- Complete the pull request checklist.
- Wait for the required quality check to pass.
- Get review approval before merging.
```bash
git clone https://github.com/cssu/mini-git.git
cd mini-git
scripts/init.sh
```

## Local Quality Checks
That creates a virtual environment, installs everything, and installs the Git
hooks. You need Python 3.11 or newer.

Run the same checks locally:
Then:

```bash
minigit --help # after `source .venv/bin/activate`
scripts/quality-check.sh
```

The pre-commit hook runs this script before each commit.
## Dev Commands

## Scripts
Everything lives in `scripts/`. They all work from any directory in the repo
and find the virtual environment themselves, so activating it is optional.

These files are small command-line programs. They are written as `.sh` shell scripts so they can be run locally from the terminal and also reused by GitHub Actions.
| Command | What it does |
| --- | --- |
| `scripts/init.sh` | One-time setup: virtual environment, dependencies, Git hooks. |
| `scripts/install-dependencies.sh` | Reinstalls dependencies. Run it after someone changes `pyproject.toml`. |
| `scripts/test.sh` | Runs the tests. Arguments pass through to pytest: `scripts/test.sh -k objects`. |
| `scripts/quality-check.sh` | Lint, formatting, and tests. This is exactly what CI runs. |
| `scripts/format.sh` | Fixes formatting and auto-fixable lint problems. |
| `scripts/build.sh` | Builds an installable package into `dist/`. |
| `scripts/install-git-hooks.sh` | Points Git at the hooks in `githooks/`. `init.sh` already does this. |

- `scripts/install-dependencies.sh`
## Layout

```
minigit/
cli.py the shared `minigit` command; each module registers subcommands here
errors.py the exception types every module shares
objects.py Module 1 - object storage
index.py Module 2 - index and working tree
commits.py Module 3 - commits and branching
remote.py Module 4 - remotes and networking
tests/
scripts/ dev commands
githooks/ the pre-commit hook
docs/
```

Installs the packages needed by a Node.js project. In most Node projects, dependencies are listed in `package.json` and installed with a package manager like `npm`, `pnpm`, or `yarn`. This script checks which lockfile the project has and uses the matching package manager.
Each module file has a docstring saying what it owns, what it depends on, and
what depends on it. Module boundaries follow the interface contract; changing a
signature another module calls is a conversation with the team, not a solo
decision.

This is mainly used by CI before running tests, because a fresh GitHub Actions runner does not already have the project's dependencies installed.
## How We Work

- `scripts/quality-check.sh`
- Branch off `main`. Never commit to `main` directly.
- Open a pull request and fill in the checklist.
- CI has to pass and one teammate has to approve before it merges.
- Squash merge, then delete the branch.

Runs the project's automated checks. In this template, it looks for `lint` and `test` scripts in `package.json`.
`main` is protected, so this is enforced rather than suggested.

A lint command checks code style and catches common mistakes. A test command runs the project's automated tests. If either command fails, the script fails too, which helps stop broken code from being merged.
This repository is public, so never commit anything secret: no tokens, no
credentials, no personal data. Git history is public too, so removing a secret
in a later commit does not unpublish it.

- `scripts/install-git-hooks.sh`
## Checks

Tells Git to use the hook files in the `githooks/` folder. You usually run this once after cloning or creating the repository.
Three things run your code, and they run the same checks:

Git hooks are scripts that Git can run automatically at certain moments, such as right before making a commit.
- **The pre-commit hook** runs lint and formatting before each commit. It skips
tests to keep committing fast. If you need to bypass it for a
work-in-progress commit, `git commit --no-verify` works, but CI will still
catch what you skipped.
- **`scripts/quality-check.sh`** runs lint, formatting, and tests. Run it before
opening a pull request.
- **CI** runs `scripts/quality-check.sh` on every pull request and on every push
to `main`.

- `githooks/pre-commit`
A lint check catches unused imports, undefined names, and similar mistakes. A
formatting check keeps everyone's code looking the same so diffs show real
changes instead of whitespace. Tests are the ones you write in `tests/`.

Runs before Git creates a commit, but only after the hooks have been installed with `scripts/install-git-hooks.sh`.
## Docs

This hook runs `scripts/quality-check.sh`, so developers get quick feedback before committing code that does not pass the project's checks.
- [Onboarding guide](https://docs.google.com/document/d/1D77Ncmtunxj7GVADytHo1An6BKzaV-Ep7AxAYfL405k/edit?tab=t.0) -
start here if you are new: setup, branching, workflow, pull requests, code
review, and coding standards.
- [Project description](https://docs.google.com/document/d/16OqU5R1x6is5E-2ZWk4KoWDouUU8gCKNzJvW7LTXJtI/edit?tab=t.0#heading=h.m3v17fze97bb) -
the full explanation of what we are building and why.
- [docs/branch-protection.md](docs/branch-protection.md) - the rules on `main`
and the merge settings, and how to change them.
89 changes: 35 additions & 54 deletions docs/branch-protection.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,56 @@
# Branch Protection

GitHub template repositories copy files, but they do not copy repository settings. Every repository created from this template must configure branch protection manually.
`main` is protected. You cannot push to it directly, and you cannot merge
without a passing CI run and an approving review. This document records what is
configured and how to change it.

Use this checklist after creating a new repository from the template.

## Configure `main`

Go to:

`Settings -> Branches -> Branch protection rules -> Add branch protection rule`

Set the branch name pattern to:

```text
main
```

Enable these settings:
## Rules on `main`

- Require a pull request before merging.
- Require at least 1 approving review.
- Dismiss stale pull request approvals when new commits are pushed.
- Require 1 approving review.
- Dismiss stale approvals when new commits are pushed.
- Require conversation resolution before merging.
- Require status checks to pass before merging.
- Require the `quality` status check to pass.
- Require branches to be up to date before merging.
- Require linear history.
- Include administrators.
- Include administrators, so nobody can bypass the above.
- Block force pushes.
- Block branch deletion.

## Required Status Check

Before the status check appears in the branch protection selector, the GitHub Actions workflow must run at least once.

The included `CI` workflow is an example. If a project changes the workflow or job names, require that project's equivalent lint/test status check instead.

To make it appear:

1. Push the repository to GitHub.
2. Open a pull request, or push a commit to `main`.
3. Wait for the `CI` workflow to run.
4. Return to the branch protection rule.
5. Select the required status check.

The check may appear as either:

```text
quality
```
The required status check is the `quality` job in
[`.github/workflows/ci.yml`](../.github/workflows/ci.yml), which runs
`scripts/quality-check.sh`. If that job is ever renamed, the protection rule has
to be updated to require the new name, or nothing will be enforced.

or:
## Merge Settings

```text
CI / quality
```
At `Settings -> General -> Pull Requests`:

Choose whichever one GitHub shows for this repository.
- Squash merging enabled.
- Merge commits disabled.
- Rebase merging disabled.
- Automatically delete head branches enabled.

## Recommended Repository Settings
Squash-only keeps history on `main` to one commit per pull request, which is
what "require linear history" expects.

Go to:
## Why the Repository Is Public

`Settings -> General -> Pull Requests`
GitHub Free does not allow branch protection or rulesets on private
repositories, and CSSU is on the free plan. Making the repository public was the
no-cost way to get these protections. The alternative is upgrading the
organization to GitHub Team.

Recommended settings:
Because the repository is public, nothing secret belongs in it. No tokens, no
credentials, no personal data, not even in a commit that gets reverted later.
Git history is public too.

- Enable squash merging.
- Disable merge commits.
- Disable rebase merging unless the project specifically wants it.
- Enable automatically delete head branches.
## Changing These

## Notes
`Settings -> Branches -> Branch protection rules`, then edit the `main` rule.
These are repository settings, not files, so they are not version controlled and
a change takes effect immediately for everyone. Talk to the team before
loosening anything.

These settings cannot be enforced by files in this template alone. The files in `.github/` provide pull request templates and CI checks, but GitHub branch protection must be configured in each created repository.
If you add a new required status check, the workflow has to have run at least
once before GitHub will offer its name in the selector.
3 changes: 3 additions & 0 deletions minigit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""minigit - a version control system built from scratch."""

__version__ = "0.1.0"
58 changes: 58 additions & 0 deletions minigit/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Command-line entry point.

This is the single `minigit` command the whole team shares. Each module wires
its own subcommands in through `_register_commands` below, so nobody has to
edit the same lines of argument parsing at the same time.

Every handler takes the parsed args and returns an exit code (0 = success).
"""

import argparse
import sys
from collections.abc import Sequence

from minigit import __version__
from minigit.errors import MiniGitError


def _register_commands(subparsers) -> None:
"""Attach each module's subcommands to the parser.

Module owners: add one line here calling into your own module, and keep
the argument definitions themselves in that module. For example::

def register_subcommands(subparsers):
parser = subparsers.add_parser("add", help="stage a file")
parser.add_argument("path")
parser.set_defaults(handler=cmd_add)
"""


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="minigit", description="A version control system.")
parser.add_argument("--version", action="version", version=f"minigit {__version__}")

subparsers = parser.add_subparsers(dest="command", metavar="<command>")
_register_commands(subparsers)

return parser


def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)

handler = getattr(args, "handler", None)
if handler is None:
parser.print_help()
return 1

try:
return handler(args)
except MiniGitError as exc:
print(f"minigit: {exc}", file=sys.stderr)
return 1


if __name__ == "__main__":
sys.exit(main())
10 changes: 10 additions & 0 deletions minigit/commits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Module 3 - Commits and branching.

Owns turning a staged tree into permanent history, plus every ref and branch
operation: commit creation, branch create/list/switch, and merging.

Depends on: modules 1 and 2.
Serves: module 4.

Build the `CommitManager` class here, per the interface contract.
"""
Loading
Loading