# 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.

## 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.

## 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`
- `git@github.com:owner/repo.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.

## 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.
