use crate::review_session::ReviewSession;
use crate::source::{DiffSource, GitSource};
use anyhow::{Context, Result};
use review_protocol::{PrRef, ReviewProvider};
use std::path::PathBuf;
use std::process::Command;
pub struct OpenedReview {
pub source: Box<dyn DiffSource>,
pub guard: WorktreeGuard,
pub session: ReviewSession,
}
pub fn open(spec: &str) -> Result<OpenedReview> {
progress("resolving review backend");
let provider = make_provider("github").context("no review backend available")?;
provider.check_ready().map_err(|e| anyhow::anyhow!("{e}"))?;
let pr_ref = parse_spec(spec);
progress(&format!("looking up PR ({spec})"));
let pr = provider
.get_pull_request(pr_ref)
.map_err(|e| anyhow::anyhow!("{e}"))?;
progress(&format!(
"PR #{} {} ({} → {})",
pr.number, pr.title, pr.branch, pr.base
));
let cache_root = cache_root_for_review()?;
progress("setting up worktree");
let handle = provider
.ensure_worktree(&pr, cache_root.to_string_lossy().as_ref())
.map_err(|e| anyhow::anyhow!("{e}"))?;
let workdir = PathBuf::from(&handle.path);
let mut pr = pr;
if pr.base_sha.is_empty() {
progress(&format!("resolving base sha ({})", pr.base));
pr.base_sha = resolve_base_sha(&workdir, &pr.base, &pr.head_sha)
.with_context(|| format!("resolve base sha for {}", pr.base))?;
}
progress("loading diff");
let source = GitSource::range(&workdir, &pr.base_sha, &pr.head_sha)?;
let guard = WorktreeGuard {
path: if handle.cleanup_on_drop {
Some(workdir)
} else {
None
},
};
let session_provider = make_provider("github").expect("backend already validated above");
let session = ReviewSession::new(pr, session_provider);
Ok(OpenedReview {
source: Box::new(source),
guard,
session,
})
}
pub struct WorktreeGuard {
path: Option<PathBuf>,
}
impl Drop for WorktreeGuard {
fn drop(&mut self) {
if let Some(p) = self.path.take() {
let _ = Command::new("git")
.args(["worktree", "remove", "--force"])
.arg(&p)
.status();
let _ = std::fs::remove_dir_all(&p);
}
}
}
fn progress(msg: &str) {
use std::io::Write;
let _ = writeln!(std::io::stderr(), "lziff: {msg}…");
let _ = std::io::stderr().flush();
}
fn resolve_base_sha(workdir: &std::path::Path, base_ref: &str, head_sha: &str) -> Result<String> {
eprintln!("lziff: git fetch --no-tags origin {base_ref}…");
let _ = Command::new("git")
.current_dir(workdir)
.args(["fetch", "--no-tags", "origin", base_ref])
.status();
for base_spec in ["FETCH_HEAD", &format!("origin/{base_ref}"), base_ref] {
let out = Command::new("git")
.current_dir(workdir)
.args(["merge-base", base_spec, head_sha])
.output();
if let Ok(out) = out {
if out.status.success() {
let sha = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !sha.is_empty() {
return Ok(sha);
}
}
}
}
for base_spec in ["FETCH_HEAD", &format!("origin/{base_ref}"), base_ref] {
let out = Command::new("git")
.current_dir(workdir)
.args(["rev-parse", base_spec])
.output();
if let Ok(out) = out {
if out.status.success() {
let sha = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !sha.is_empty() {
return Ok(sha);
}
}
}
}
anyhow::bail!("could not resolve base ref `{base_ref}` to a sha")
}
fn parse_spec(spec: &str) -> PrRef {
if spec.starts_with("http://") || spec.starts_with("https://") {
return PrRef::Url(spec.to_string());
}
if let Ok(n) = spec.parse::<u64>() {
return PrRef::Number(n);
}
PrRef::Branch(spec.to_string())
}
fn cache_root_for_review() -> Result<PathBuf> {
let dir = dirs::cache_dir()
.ok_or_else(|| anyhow::anyhow!("no cache dir available on this platform"))?
.join("lziff")
.join("review");
std::fs::create_dir_all(&dir)
.with_context(|| format!("create cache dir {}", dir.display()))?;
Ok(dir)
}
fn make_provider(id: &str) -> Option<Box<dyn ReviewProvider>> {
match id {
#[cfg(feature = "github")]
"github" => Some(lziff_github::make_provider()),
_ => None,
}
}
pub fn make_picker_provider() -> Option<Box<dyn ReviewProvider>> {
make_provider("github")
}