# RepoForge
RepoForge provides repository archive discovery and safe refresh helpers used by
`thesa`.
Made by Trevor Knott for Knott Dynamics. RepoForge is part of the thesa archive
stack alongside `modelforge`, `siteforge`, Scrin, and Aisling.
The crate is intentionally focused. It scans an archive root for Git worktrees,
derives stable `owner/repo` slugs from GitHub remotes or archive paths, and
refreshes clones with `git pull --ff-only` without touching dirty worktrees.
RepoForge is a library crate, not a full CLI. It gives applications the safe Git
operations and deterministic archive identifiers they need while leaving UI,
manifest writing, and policy decisions to the caller.
## What It Does
- Finds Git worktrees recursively under an archive root.
- Converts GitHub remotes into stable `owner/repo` slugs.
- Falls back to archive path slugs when remotes are missing or non-GitHub.
- Restricts discovery to one GitHub owner/user/org when requested.
- Filters discovered archives by slug or path text.
- Refuses dirty worktrees before refresh.
- Runs `git pull --ff-only` for clean worktrees.
- Returns structured updated, unchanged, and failed outcomes.
## What It Does Not Do
- Does not delete repositories.
- Does not reset, checkout, rebase, merge, stash, or force-pull.
- Does not write archive manifests or checksums.
- Does not assume a terminal UI.
- Does not require GitHub API credentials.
## Install
```toml
[dependencies]
repoforge = "0.1"
```
The package and library crate name are both `repoforge`.
## Archive Layout
RepoForge is designed for archive trees like this:
```text
archives/
octocat/
Hello-World/
.git/
rust-lang/
rust/
.git/
```
When `remote.origin.url` is a GitHub URL, the slug is derived from the remote.
For example `https://github.com/octocat/Hello-World.git` becomes
`octocat/Hello-World`.
If there is no GitHub remote, RepoForge falls back to the path layout. For
example `archives/octocat/Hello-World` becomes `octocat/Hello-World`.
## Discovery
```rust
use std::path::Path;
let archives = repoforge::discover_git_archives(Path::new("./archives"), None)?;
for archive in archives {
println!("{} -> {}", archive.slug, archive.path.display());
}
# Ok::<(), repoforge::RepoForgeError>(())
```
Use a filter to limit results by slug or path:
```rust
let archives = repoforge::discover_git_archives(
std::path::Path::new("./archives"),
Some("octocat"),
)?;
# Ok::<(), repoforge::RepoForgeError>(())
```
Discovery is recursive but stops descending when it finds a Git worktree.
`.git`, `.thesa`, and `target` directories are skipped.
Discover only one GitHub owner/user/org:
```rust
let archives = repoforge::discover_git_archives_for_owner(
std::path::Path::new("./archives"),
"octocat",
None,
)?;
```
Owner matching is case-insensitive and uses the derived `owner/repo` slug. This
works for GitHub remote-derived slugs and archive layouts like
`archives/<owner>/<repo>`.
## API Surface
- `discover_git_archives(root, filter)`: find archive worktrees.
- `discover_git_archives_for_owner(root, owner, filter)`: find worktrees for one GitHub owner/user/org.
- `refresh_git_archives(archives, concurrency)`: refresh many worktrees.
- `refresh_one_git_archive(archive)`: refresh one worktree.
- `parse_github_remote_slug(remote_url)`: parse GitHub remotes into `owner/repo`.
- `split_owner_repo_slug(slug)`: borrow validated `owner` and `repo` parts from an `owner/repo` slug.
- `github_web_url_for_slug(slug)`: build canonical GitHub web URLs for display links.
- `fallback_archive_slug(root, repo_path)`: derive a slug from archive layout.
- `ensure_clean_git_worktree(archive)`: fail if local changes exist.
- `git_head(archive)`: read the current `HEAD` hash.
- `run_git_capture(repo_path, args)`: run low-level Git commands with RepoForge errors.
Core data types:
- `GitArchiveCandidate`: discovered worktree path, slug, and optional remote.
- `GitArchiveCandidate::slug_parts()`: borrow validated `(owner, repo)` slug parts.
- `GitArchiveCandidate::owner()`: borrow the slug owner.
- `GitArchiveCandidate::repo_name()`: borrow the slug repository name.
- `GitArchiveCandidate::github_url()`: build a canonical GitHub web URL from the slug.
- `GitArchiveCandidate::matches_owner(owner)`: case-insensitive owner match.
- `GitRefreshSummary`: aggregate counts, successful entries, and failures.
- `GitRefreshStatus`: `Updated` or `Unchanged`.
- `RepoForgeError`: typed errors for non-directory roots, missing Git, dirty trees, Git command failures, and I/O.
## Refresh
```rust,no_run
let archives = repoforge::discover_git_archives(std::path::Path::new("./archives"), None)?;
let summary = repoforge::refresh_git_archives(&archives, 4);
println!(
"updated={} unchanged={} failed={}",
summary.updated,
summary.unchanged,
summary.failed.len()
);
if summary.has_failures() {
for failure in &summary.failed {
eprintln!("{}: {}", failure.slug, failure.message);
}
}
# Ok::<(), repoforge::RepoForgeError>(())
```
Refresh behavior:
- Runs `git status --porcelain` before pulling.
- Dirty worktrees fail fast and are not pulled.
- Runs `git pull --ff-only` for clean worktrees.
- Reports `Updated` when `HEAD` changes.
- Reports `Unchanged` when the pull succeeds without changing `HEAD`.
- Uses bounded concurrency with `refresh_git_archives`.
## Slug Helpers
```rust
assert_eq!(
repoforge::parse_github_remote_slug("git@github.com:octocat/Hello-World.git"),
Some("octocat/Hello-World".to_string())
);
```
Supported GitHub remote forms include HTTPS and SSH:
- `https://github.com/owner/repo.git`
- `http://github.com/owner/repo.git`
- `https://www.github.com/owner/repo.git`
- `git@github.com:owner/repo.git`
- `ssh://git@github.com/owner/repo.git`
- `git+ssh://git@github.com/owner/repo.git`
## Safety Contract
RepoForge does not delete, reset, checkout, stash, or force-pull anything. It
only uses read-only Git inspection commands plus `git pull --ff-only` after
confirming the worktree is clean.
Callers such as `thesa` remain responsible for archive metadata sidecars,
checksums, and higher-level reporting.
## Package Contents
Cargo releases are explicitly whitelisted to avoid shipping sessions, notes,
build output, or local archives. The package contains only:
- `Cargo.toml`
- `Cargo.lock`
- `README.md`
- `LICENSE`
- `CHANGELOG.md`
- `src/**`
## Role In Thesa
`thesa` uses RepoForge for Git archive discovery, GitHub slugging, dirty-tree
protection, and fast-forward-only refreshes. `thesa` then wraps those outcomes
with Scrin/Aisling terminal reporting and `.thesa` integrity manifests.