diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffe3cab..a3820d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f91ae7 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 643d64c..6b9dd0d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/branch-protection.md b/docs/branch-protection.md index ea7b6f0..5dd390e 100644 --- a/docs/branch-protection.md +++ b/docs/branch-protection.md @@ -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. diff --git a/minigit/__init__.py b/minigit/__init__.py new file mode 100644 index 0000000..e6f563c --- /dev/null +++ b/minigit/__init__.py @@ -0,0 +1,3 @@ +"""minigit - a version control system built from scratch.""" + +__version__ = "0.1.0" diff --git a/minigit/cli.py b/minigit/cli.py new file mode 100644 index 0000000..8a546de --- /dev/null +++ b/minigit/cli.py @@ -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="") + _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()) diff --git a/minigit/commits.py b/minigit/commits.py new file mode 100644 index 0000000..6b89555 --- /dev/null +++ b/minigit/commits.py @@ -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. +""" diff --git a/minigit/errors.py b/minigit/errors.py new file mode 100644 index 0000000..bb29083 --- /dev/null +++ b/minigit/errors.py @@ -0,0 +1,38 @@ +"""Exception types shared across every module. + +These are part of the interface contract. When one module fails in a way +another module needs to handle, it raises one of these rather than a +module-specific exception, so callers only have to know about this file. +""" + + +class MiniGitError(Exception): + """Base class for every error raised by minigit.""" + + +class ObjectNotFoundError(MiniGitError): + """No object with the requested hash exists in the object store.""" + + +class ObjectCorruptError(MiniGitError): + """An object was found, but its bytes do not match its hash.""" + + +class RefNotFoundError(MiniGitError): + """A ref (branch, HEAD target, tag) does not exist.""" + + +class MergeConflictError(MiniGitError): + """A merge could not complete automatically. + + Carries the paths that conflicted so the caller can report them or write + conflict markers into the working directory. + """ + + def __init__(self, paths: list[str], message: str | None = None) -> None: + self.paths = list(paths) + super().__init__(message or f"merge conflict in: {', '.join(self.paths)}") + + +class NetworkProtocolError(MiniGitError): + """A push or pull failed: bad handshake, bad auth, or a rejected update.""" diff --git a/minigit/index.py b/minigit/index.py new file mode 100644 index 0000000..3c59474 --- /dev/null +++ b/minigit/index.py @@ -0,0 +1,10 @@ +"""Module 2 - Index and working tree. + +Owns the bridge between live files on disk and the immutable object store: +the index file, staging, diffing, and checkout. + +Depends on: module 1. +Serves: module 3. + +Build the `WorkingTree` class here, per the interface contract. +""" diff --git a/minigit/objects.py b/minigit/objects.py new file mode 100644 index 0000000..860cc9a --- /dev/null +++ b/minigit/objects.py @@ -0,0 +1,10 @@ +"""Module 1 - Object storage. + +Owns the on-disk `.minigit/objects/` directory. The only module that reads or +writes object bytes to disk. + +Depends on: nothing. +Serves: modules 2, 3, and 4. + +Build the `ObjectStore` class here, per the interface contract. +""" diff --git a/minigit/remote.py b/minigit/remote.py new file mode 100644 index 0000000..b6f12ca --- /dev/null +++ b/minigit/remote.py @@ -0,0 +1,10 @@ +"""Module 4 - Remotes and networking. + +Owns push and pull over a TCP socket: object and ref exchange between two +repositories, authentication, and push conflict detection. + +Depends on: modules 1 and 3. +Serves: nobody - this is the top of the stack. + +Build the `RemoteClient` class here, per the interface contract. +""" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0829d4f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "minigit" +version = "0.1.0" +description = "A version control system built from scratch." +readme = "README.md" +requires-python = ">=3.11" + +# The core logic must be written from scratch, so there are no runtime +# dependencies. Only the Python standard library is allowed here: hashlib, +# zlib, socket, argparse, json, and friends. +dependencies = [] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "ruff>=0.6", + "build>=1.2", +] + +[project.scripts] +minigit = "minigit.cli:main" + +[tool.hatch.build.targets.wheel] +packages = ["minigit"] + +[tool.ruff] +line-length = 100 + +[tool.ruff.lint] +# E/F pycodestyle + pyflakes (the usual "this is a mistake" checks) +# I import sorting +# UP prefer modern Python syntax +# B common bug patterns +select = ["E", "F", "I", "UP", "B"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/scripts/_python.sh b/scripts/_python.sh new file mode 100644 index 0000000..8d03d9c --- /dev/null +++ b/scripts/_python.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Shared helper. Not meant to be run directly - the other scripts source it. +# +# Sets two variables: +# REPO_ROOT - the top of the repository +# PYTHON - the interpreter to use +# +# It prefers the project's own .venv so you never have to remember to activate +# it, and falls back to whatever python3 is on PATH (which is what CI uses). + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +if [[ -x "$REPO_ROOT/.venv/bin/python" ]]; then + PYTHON="$REPO_ROOT/.venv/bin/python" +elif command -v python3 > /dev/null 2>&1; then + PYTHON="python3" +else + echo "No Python interpreter found. Install Python 3.11+ and run scripts/init.sh." >&2 + exit 1 +fi diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..98e6810 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Builds a distributable package into dist/ - a .whl anyone can `pip install` +# and a .tar.gz of the source. Use this for the demo and the final handoff. +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/_python.sh" + +rm -rf dist +"$PYTHON" -m build + +echo +echo "Built:" +ls -1 dist diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 0000000..6b9e128 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Reformats the code and fixes the lint problems that can be fixed +# automatically. Run this when quality-check.sh complains about formatting. +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/_python.sh" + +"$PYTHON" -m ruff check --fix . +"$PYTHON" -m ruff format . diff --git a/scripts/init.sh b/scripts/init.sh new file mode 100755 index 0000000..cd47254 --- /dev/null +++ b/scripts/init.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# One-time setup for a fresh clone: virtual environment, dependencies, Git hooks. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +if ! command -v python3 > /dev/null 2>&1; then + echo "Python 3 is not installed. Install Python 3.11 or newer, then run this again." >&2 + exit 1 +fi + +# A virtual environment keeps this project's packages out of your system Python, +# so nothing you install here can break another project. +if ! python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)'; then + echo "Python 3.11 or newer is required. Found: $(python3 --version)" >&2 + exit 1 +fi + +if [[ ! -d .venv ]]; then + echo "Creating virtual environment in .venv/" + python3 -m venv .venv +fi + +scripts/install-dependencies.sh +scripts/install-git-hooks.sh + +cat << 'EOF' + +Setup complete. + + Activate the virtual environment: source .venv/bin/activate + Run the CLI: minigit --help + Run the checks: scripts/quality-check.sh + +The scripts in scripts/ find .venv on their own, so activating is optional. +EOF diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh index 99e0881..4f80166 100755 --- a/scripts/install-dependencies.sh +++ b/scripts/install-dependencies.sh @@ -1,18 +1,11 @@ #!/usr/bin/env bash +# Installs the project and its development tools. set -euo pipefail -if [[ ! -f package.json ]]; then - exit 0 -fi +source "$(dirname "${BASH_SOURCE[0]}")/_python.sh" -if [[ -f pnpm-lock.yaml ]]; then - corepack enable - pnpm install --frozen-lockfile -elif [[ -f yarn.lock ]]; then - corepack enable - yarn install --immutable -elif [[ -f package-lock.json || -f npm-shrinkwrap.json ]]; then - npm ci -else - npm install -fi +# --editable means the installed `minigit` command runs your working copy, so +# your edits take effect without reinstalling. +# [dev] pulls in the tools listed under optional-dependencies in pyproject.toml. +"$PYTHON" -m pip install --quiet --upgrade pip +"$PYTHON" -m pip install --editable ".[dev]" diff --git a/scripts/quality-check.sh b/scripts/quality-check.sh index e868426..7d57d96 100755 --- a/scripts/quality-check.sh +++ b/scripts/quality-check.sh @@ -1,45 +1,36 @@ #!/usr/bin/env bash +# Runs the project's automated checks. This is what CI runs, so if it passes +# here it should pass on your pull request. +# +# scripts/quality-check.sh lint + formatting + tests +# scripts/quality-check.sh --pre-commit lint + formatting only (fast) +# +# The pre-commit hook uses the fast form so committing stays quick. Tests still +# run in CI, and you can run them yourself with scripts/test.sh. set -euo pipefail -has_command() { - command -v "$1" > /dev/null 2>&1 -} +source "$(dirname "${BASH_SOURCE[0]}")/_python.sh" -run_node_script_if_present() { - local script_name="$1" +pre_commit=false +if [[ "${1:-}" == "--pre-commit" ]]; then + pre_commit=true +fi - if [[ ! -f package.json ]]; then - return 0 - fi +# Lint: catches unused imports, undefined names, and other common mistakes. +echo "Linting..." +"$PYTHON" -m ruff check . - if ! node -e "const p=require('./package.json'); process.exit(p.scripts && p.scripts['$script_name'] ? 0 : 1)" 2> /dev/null; then - return 0 - fi +# Formatting: checks the code matches the project's style without changing it. +# Run `scripts/format.sh` to fix anything this reports. +echo "Checking formatting..." +"$PYTHON" -m ruff format --check . - if [[ -f pnpm-lock.yaml ]]; then - if ! has_command pnpm && has_command corepack; then - corepack enable - fi - if ! has_command pnpm; then - echo "pnpm-lock.yaml found, but pnpm is not installed." >&2 - exit 1 - fi - pnpm run "$script_name" - elif [[ -f yarn.lock ]]; then - if ! has_command yarn && has_command corepack; then - corepack enable - fi - if ! has_command yarn; then - echo "yarn.lock found, but yarn is not installed." >&2 - exit 1 - fi - yarn "$script_name" - else - npm run "$script_name" - fi -} +if [[ "$pre_commit" == true ]]; then + echo "Quality checks completed (tests skipped for pre-commit)." + exit 0 +fi -run_node_script_if_present lint -run_node_script_if_present test +echo "Running tests..." +"$PYTHON" -m pytest echo "Quality checks completed." diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..651d71f --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Runs the test suite. Extra arguments are passed straight through to pytest, +# so `scripts/test.sh -k objects` and `scripts/test.sh -x` work as expected. +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/_python.sh" + +"$PYTHON" -m pytest "$@" diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..b0ee305 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,30 @@ +"""Smoke tests: the package imports and the CLI runs. + +These exist so the test suite is green from day one. Delete them once there +are real tests to run. +""" + +import pytest + +from minigit.cli import main +from minigit.errors import MergeConflictError, MiniGitError + + +def test_cli_reports_version(capsys): + with pytest.raises(SystemExit) as exit_info: + main(["--version"]) + + assert exit_info.value.code == 0 + assert "minigit" in capsys.readouterr().out + + +def test_cli_without_a_command_prints_help(capsys): + assert main([]) == 1 + assert "usage: minigit" in capsys.readouterr().out + + +def test_merge_conflict_error_carries_paths(): + error = MergeConflictError(["src/a.py", "src/b.py"]) + + assert error.paths == ["src/a.py", "src/b.py"] + assert isinstance(error, MiniGitError)