- Drop fields and functions nothing reads: job and proc tips, fetch time, PR url/author (and their GraphQL fields), spine summaries, diff::commit, unread data attributes, unused CSS and tokens. - One home each for state wording (naming main as the repo does), lane lookup, unseen, outdated, main's tip, the stop wait and the token header. - Narrow visibility; rename render::View to Filter; queue ranking moves to the CLI, its only user. - Token test covers every route; shared checkout snapshot in the fixture. - Client: errors outside Run/Send are toasts, history only after a good load, a drag stays in one file, the agent marker is a ring. - design.md, the skill and AGENTS.md describe what is built.
223 lines
7.6 KiB
Rust
223 lines
7.6 KiB
Rust
#![allow(dead_code)]
|
|
|
|
//! A fixture clone built with the git CLI: an `origin` bare repository, a
|
|
//! clone with remote, local, merged, conflicting and worktree branches.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
pub struct Fixture {
|
|
_dir: tempfile::TempDir,
|
|
pub clone: PathBuf,
|
|
}
|
|
|
|
static CLOCK: AtomicU64 = AtomicU64::new(1_790_000_000);
|
|
|
|
/// Runs git in `dir` with a fixed identity and a clock that moves one minute
|
|
/// per call, so commit order is deterministic.
|
|
pub fn git(dir: &Path, args: &[&str]) -> String {
|
|
let t = CLOCK.fetch_add(60, Ordering::SeqCst);
|
|
let date = format!("@{t} +0000");
|
|
let out = Command::new("git")
|
|
.args(args)
|
|
.current_dir(dir)
|
|
.env("GIT_AUTHOR_NAME", "alice")
|
|
.env("GIT_AUTHOR_EMAIL", "alice@example.com")
|
|
.env("GIT_COMMITTER_NAME", "alice")
|
|
.env("GIT_COMMITTER_EMAIL", "alice@example.com")
|
|
.env("GIT_AUTHOR_DATE", &date)
|
|
.env("GIT_COMMITTER_DATE", &date)
|
|
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
|
.env("GIT_CONFIG_NOSYSTEM", "1")
|
|
.output()
|
|
.expect("git runs");
|
|
assert!(
|
|
out.status.success() || args.first() == Some(&"merge-tree"),
|
|
"git {args:?} failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
String::from_utf8(out.stdout)
|
|
.expect("utf8")
|
|
.trim()
|
|
.to_owned()
|
|
}
|
|
|
|
/// `git merge-tree` exit code is 1 on conflict; returns the conflicting paths.
|
|
pub fn merge_tree_conflicts(dir: &Path, a: &str, b: &str) -> Vec<String> {
|
|
let out = git(
|
|
dir,
|
|
&[
|
|
"merge-tree",
|
|
"--write-tree",
|
|
"--name-only",
|
|
"--no-messages",
|
|
a,
|
|
b,
|
|
],
|
|
);
|
|
let mut paths: Vec<String> = out.lines().skip(1).map(str::to_owned).collect();
|
|
paths.sort();
|
|
paths.dedup();
|
|
paths
|
|
}
|
|
|
|
fn write(dir: &Path, file: &str, body: &str) {
|
|
let p = dir.join(file);
|
|
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
std::fs::write(p, body).unwrap();
|
|
}
|
|
|
|
pub fn commit(dir: &Path, file: &str, body: &str, msg: &str) {
|
|
write(dir, file, body);
|
|
git(dir, &["add", "-A"]);
|
|
git(dir, &["commit", "-q", "-m", msg]);
|
|
}
|
|
|
|
impl Fixture {
|
|
pub fn new() -> Fixture {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let origin = dir.path().join("origin.git");
|
|
let clone = dir.path().join("clone");
|
|
git(
|
|
dir.path(),
|
|
&[
|
|
"init",
|
|
"-q",
|
|
"--bare",
|
|
"-b",
|
|
"main",
|
|
origin.to_str().unwrap(),
|
|
],
|
|
);
|
|
git(
|
|
dir.path(),
|
|
&[
|
|
"clone",
|
|
"-q",
|
|
origin.to_str().unwrap(),
|
|
clone.to_str().unwrap(),
|
|
],
|
|
);
|
|
let c = clone.as_path();
|
|
git(c, &["symbolic-ref", "HEAD", "refs/heads/main"]);
|
|
commit(c, "app.txt", "one\ntwo\nthree\n", "m1");
|
|
commit(c, "Cargo.lock", "lock v1\n", "m2");
|
|
git(c, &["push", "-q", "-u", "origin", "main"]);
|
|
|
|
// Old fork, pushed, two commits.
|
|
git(c, &["checkout", "-q", "-b", "feat/old"]);
|
|
commit(c, "old.txt", "a\n", "old 1");
|
|
commit(c, "old.txt", "a\nb\n", "old 2");
|
|
git(c, &["push", "-q", "-u", "origin", "feat/old"]);
|
|
|
|
// Main moves on: changes line two and the lock file.
|
|
git(c, &["checkout", "-q", "main"]);
|
|
commit(c, "app.txt", "one\nTWO main\nthree\n", "m3");
|
|
commit(c, "Cargo.lock", "lock v2 main\n", "m4");
|
|
git(c, &["push", "-q", "origin", "main"]);
|
|
|
|
// Conflicts with main on app.txt, forked before m3.
|
|
git(c, &["checkout", "-q", "-b", "feat/conflict", "HEAD~2"]);
|
|
commit(c, "app.txt", "one\nTWO branch\nthree\n", "conflict 1");
|
|
git(c, &["push", "-q", "-u", "origin", "feat/conflict"]);
|
|
|
|
// Conflicts with main only on the lock file.
|
|
git(c, &["checkout", "-q", "-b", "deps/lock", "main~1"]);
|
|
commit(c, "Cargo.lock", "lock v2 deps\n", "lock bump");
|
|
git(c, &["push", "-q", "-u", "origin", "deps/lock"]);
|
|
|
|
// Merged into main with a merge commit: nothing ahead.
|
|
git(c, &["checkout", "-q", "-b", "feat/merged", "main"]);
|
|
commit(c, "merged.txt", "m\n", "merged 1");
|
|
git(c, &["push", "-q", "-u", "origin", "feat/merged"]);
|
|
git(c, &["checkout", "-q", "main"]);
|
|
git(
|
|
c,
|
|
&[
|
|
"merge",
|
|
"-q",
|
|
"--no-ff",
|
|
"-m",
|
|
"merge feat/merged",
|
|
"feat/merged",
|
|
],
|
|
);
|
|
git(c, &["push", "-q", "origin", "main"]);
|
|
|
|
// Long lane: forty commits, so some fold.
|
|
git(c, &["checkout", "-q", "-b", "feat/long", "main"]);
|
|
for i in 0..40 {
|
|
commit(c, "long.txt", &format!("{i}\n"), &format!("long {i}"));
|
|
}
|
|
git(c, &["push", "-q", "-u", "origin", "feat/long"]);
|
|
|
|
// Pushed, then one more local commit: one unpushed.
|
|
git(c, &["checkout", "-q", "-b", "feat/unpushed", "main"]);
|
|
commit(c, "u.txt", "1\n", "unpushed 1");
|
|
git(c, &["push", "-q", "-u", "origin", "feat/unpushed"]);
|
|
commit(c, "u.txt", "1\n2\n", "unpushed 2");
|
|
|
|
// Local only, checked out in a second worktree.
|
|
git(c, &["checkout", "-q", "-b", "agent/x", "main"]);
|
|
commit(c, "agent.txt", "x\n", "agent 1");
|
|
commit(c, "agent.txt", "x\ny\n", "agent 2");
|
|
git(c, &["checkout", "-q", "main"]);
|
|
let wt = dir.path().join("wt-agent");
|
|
git(
|
|
c,
|
|
&["worktree", "add", "-q", wt.to_str().unwrap(), "agent/x"],
|
|
);
|
|
|
|
// Forks where feat/old forks, then merges main in: it must still
|
|
// attach to main in the rail.
|
|
git(c, &["checkout", "-q", "-b", "feat/synced", "feat/old~2"]);
|
|
commit(c, "synced.txt", "s\n", "synced 1");
|
|
git(c, &["merge", "-q", "--no-edit", "main"]);
|
|
commit(c, "synced.txt", "s\nt\n", "synced 2");
|
|
git(c, &["push", "-q", "-u", "origin", "feat/synced"]);
|
|
git(c, &["checkout", "-q", "main"]);
|
|
|
|
// Renames a file and edits it: the stat must show one renamed file.
|
|
git(c, &["checkout", "-q", "-b", "feat/rename", "main"]);
|
|
git(c, &["mv", "app.txt", "application.txt"]);
|
|
commit(
|
|
c,
|
|
"application.txt",
|
|
"one\nTWO main\nthree\nfour\n",
|
|
"rename app",
|
|
);
|
|
git(c, &["push", "-q", "-u", "origin", "feat/rename"]);
|
|
git(c, &["checkout", "-q", "main"]);
|
|
|
|
// Cut from origin/main, so it tracks main: the usual agent branch.
|
|
git(
|
|
c,
|
|
&["checkout", "-q", "-b", "agent/from-main", "origin/main"],
|
|
);
|
|
commit(c, "from.txt", "f\n", "from main 1");
|
|
git(c, &["checkout", "-q", "main"]);
|
|
|
|
// No history in common with main, like gh-pages.
|
|
git(c, &["checkout", "-q", "--orphan", "gh-pages"]);
|
|
git(c, &["rm", "-rq", "--cached", "."]);
|
|
commit(c, "index.html", "site\n", "site");
|
|
git(c, &["push", "-q", "-u", "origin", "gh-pages"]);
|
|
git(c, &["checkout", "-q", "-f", "main"]);
|
|
git(c, &["clean", "-fdq"]);
|
|
|
|
git(c, &["remote", "set-head", "origin", "main"]);
|
|
Fixture { _dir: dir, clone }
|
|
}
|
|
}
|
|
|
|
/// The user's checkout as git shows it (HEAD, status, index): what
|
|
/// branchgraph's own writes must never change.
|
|
pub fn checkout_state(c: &Path) -> (String, String, Vec<u8>) {
|
|
(
|
|
git(c, &["rev-parse", "HEAD"]),
|
|
git(c, &["status", "--porcelain"]),
|
|
std::fs::read(c.join(".git/index")).unwrap(),
|
|
)
|
|
}
|