quinjet 0.0.48

A fast, live, keyboard-first Git source-control interface for the terminal
use super::*;

struct RemoteFixture {
    local: Scratch,
    remote: Scratch,
    peer: Scratch,
}

impl RemoteFixture {
    fn tracked() -> Result<Self> {
        let local = Scratch::repository()?;
        let remote = bare_repository()?;
        let remote_path = remote.path.display().to_string();
        local.git(&["remote", "add", "origin", &remote_path])?;
        local.git(&["push", "--set-upstream", "origin", "main"])?;
        let peer = Scratch::directory()?;
        clone_repository(&remote.path, &peer.path)?;
        peer.git(&["config", "user.name", "Quinjet Peer"])?;
        peer.git(&["config", "user.email", "peer@example.com"])?;
        peer.git(&["config", "commit.gpgsign", "false"])?;
        Ok(Self {
            local,
            remote,
            peer,
        })
    }

    fn peer_commit(&self, path: &str, contents: &str, message: &str) -> Result<String> {
        self.peer.write(path, contents)?;
        self.peer.git(&["add", path])?;
        self.peer.git(&["commit", "--message", message])?;
        self.peer.git(&["push", "origin", "main"])?;
        self.peer.git(&["rev-parse", "HEAD"])
    }

    fn remote_main(&self) -> Result<String> {
        self.remote.git(&["rev-parse", "refs/heads/main"])
    }
}

fn bare_repository() -> Result<Scratch> {
    let remote = Scratch::directory()?;
    remote.git(&["init", "--bare", "--initial-branch=main"])?;
    Ok(remote)
}

fn clone_repository(source: &Path, target: &Path) -> Result<()> {
    let mut command = ProcessCommand::new("git");
    command.arg("clone").arg("--quiet").arg(source).arg(target);
    isolate_git(&mut command);
    drop(Run::from(command.output().context("failed to clone the peer")?)?.success()?);
    Ok(())
}

fn assert_failure(run: &Run) -> Result<()> {
    ensure!(run.code == 1, "expected exit 1, got {}", run.code);
    ensure!(
        run.stdout.is_empty(),
        "failure wrote stdout: {}",
        run.stdout
    );
    ensure!(
        run.stderr.contains("error:"),
        "failure omitted its error: {}",
        run.stderr
    );
    Ok(())
}

#[test]
fn first_push_sets_origin_as_the_upstream() -> Result<()> {
    let local = Scratch::repository()?;
    let remote = bare_repository()?;
    let remote_path = remote.path.display().to_string();
    local.git(&["remote", "add", "origin", &remote_path])?;
    let head = local.git(&["rev-parse", "HEAD"])?;

    let pushed = local.quinjet(&["push", "--json"])?.success()?;

    ensure!(pushed.stderr.is_empty(), "{}", pushed.stderr);
    ensure!(pushed.json()?["message"] == "Push complete");
    ensure!(local.git(&["rev-parse", "--abbrev-ref", "@{upstream}"])? == "origin/main");
    ensure!(remote.git(&["rev-parse", "refs/heads/main"])? == head);
    ensure!(local.git(&["rev-parse", "HEAD"])? == head);
    Ok(())
}

#[test]
fn subsequent_push_updates_the_existing_upstream() -> Result<()> {
    let fixture = RemoteFixture::tracked()?;
    fixture.local.write("local.txt", "local\n")?;
    fixture.local.git(&["add", "local.txt"])?;
    fixture.local.git(&["commit", "--message=local"])?;
    let head = fixture.local.git(&["rev-parse", "HEAD"])?;

    let pushed = fixture.local.quinjet(&["push"])?.success()?;

    ensure!(pushed.stdout == "Push complete\n", "{}", pushed.stdout);
    ensure!(pushed.stderr.is_empty(), "{}", pushed.stderr);
    ensure!(fixture.remote_main()? == head);
    ensure!(
        fixture
            .local
            .git(&["rev-parse", "--abbrev-ref", "@{upstream}"])?
            == "origin/main"
    );
    Ok(())
}

#[test]
fn push_without_an_upstream_or_origin_fails_without_moving_head() -> Result<()> {
    let local = Scratch::repository()?;
    let head = local.git(&["rev-parse", "HEAD"])?;
    let status = local.git(&["status", "--porcelain"])?;

    let pushed = local.quinjet(&["push"])?;

    assert_failure(&pushed)?;
    ensure!(pushed.stderr.contains("no upstream and no `origin`"));
    ensure!(local.git(&["rev-parse", "HEAD"])? == head);
    ensure!(local.git(&["status", "--porcelain"])? == status);
    ensure!(local.git(&["remote"])?.is_empty());
    Ok(())
}

#[test]
fn push_rejects_non_fast_forward_without_changing_either_tip() -> Result<()> {
    let fixture = RemoteFixture::tracked()?;
    let remote = fixture.peer_commit("peer.txt", "peer\n", "peer")?;
    fixture.local.write("local.txt", "local\n")?;
    fixture.local.git(&["add", "local.txt"])?;
    fixture.local.git(&["commit", "--message=local"])?;
    let local = fixture.local.git(&["rev-parse", "HEAD"])?;

    let pushed = fixture.local.quinjet(&["push", "--json"])?;

    assert_failure(&pushed)?;
    ensure!(
        pushed.stderr.to_ascii_lowercase().contains("rejected"),
        "{}",
        pushed.stderr
    );
    ensure!(fixture.remote_main()? == remote);
    ensure!(fixture.local.git(&["rev-parse", "HEAD"])? == local);
    ensure!(fixture.local.git(&["status", "--porcelain"])?.is_empty());
    Ok(())
}

#[test]
fn fetch_discovers_a_new_remote_branch() -> Result<()> {
    let fixture = RemoteFixture::tracked()?;
    fixture.peer.git(&["switch", "--create", "topic"])?;
    fixture.peer.write("topic.txt", "topic\n")?;
    fixture.peer.git(&["add", "topic.txt"])?;
    fixture.peer.git(&["commit", "--message=topic"])?;
    fixture
        .peer
        .git(&["push", "--set-upstream", "origin", "topic"])?;
    let topic = fixture.peer.git(&["rev-parse", "HEAD"])?;
    ensure!(
        fixture
            .local
            .git_run(&[
                "show-ref",
                "--verify",
                "--quiet",
                "refs/remotes/origin/topic"
            ])?
            .code
            != 0
    );

    let fetched = fixture.local.quinjet(&["fetch", "--json"])?.success()?;

    ensure!(fetched.stderr.is_empty(), "{}", fetched.stderr);
    ensure!(fetched.json()?["message"] == "Fetch complete");
    ensure!(
        fixture
            .local
            .git(&["rev-parse", "refs/remotes/origin/topic"])?
            == topic
    );
    ensure!(fixture.local.git(&["branch", "--show-current"])? == "main");
    Ok(())
}

#[test]
fn fetch_prunes_a_deleted_remote_branch() -> Result<()> {
    let fixture = RemoteFixture::tracked()?;
    fixture.peer.git(&["switch", "--create", "topic"])?;
    fixture
        .peer
        .git(&["push", "--set-upstream", "origin", "topic"])?;
    fixture.local.git(&["fetch", "origin"])?;
    fixture.peer.git(&["push", "origin", "--delete", "topic"])?;
    ensure!(
        fixture
            .local
            .git_run(&[
                "show-ref",
                "--verify",
                "--quiet",
                "refs/remotes/origin/topic"
            ])?
            .code
            == 0
    );

    let fetched = fixture.local.quinjet(&["fetch"])?.success()?;

    ensure!(fetched.stdout == "Fetch complete\n", "{}", fetched.stdout);
    ensure!(fetched.stderr.is_empty(), "{}", fetched.stderr);
    ensure!(
        fixture
            .local
            .git_run(&[
                "show-ref",
                "--verify",
                "--quiet",
                "refs/remotes/origin/topic"
            ])?
            .code
            != 0
    );
    Ok(())
}

#[test]
fn pull_fast_forwards_to_a_peer_commit() -> Result<()> {
    let fixture = RemoteFixture::tracked()?;
    let before = fixture.local.git(&["rev-parse", "HEAD"])?;
    let peer = fixture.peer_commit("peer.txt", "from peer\n", "peer")?;

    let pulled = fixture.local.quinjet(&["pull", "--json"])?.success()?;

    ensure!(pulled.stderr.is_empty(), "{}", pulled.stderr);
    ensure!(pulled.json()?["message"] == "Pull complete");
    ensure!(fixture.local.git(&["rev-parse", "HEAD"])? == peer);
    ensure!(fixture.local.git(&["rev-parse", "HEAD"])? != before);
    ensure!(fs::read_to_string(fixture.local.path.join("peer.txt"))? == "from peer\n");
    ensure!(fixture.local.git(&["status", "--porcelain"])?.is_empty());
    Ok(())
}

#[test]
fn pull_without_an_upstream_fails_without_changing_the_repository() -> Result<()> {
    let local = Scratch::repository()?;
    let head = local.git(&["rev-parse", "HEAD"])?;
    let tree = local.git(&["write-tree"])?;

    let pulled = local.quinjet(&["pull", "--json"])?;

    assert_failure(&pulled)?;
    ensure!(local.git(&["rev-parse", "HEAD"])? == head);
    ensure!(local.git(&["write-tree"])? == tree);
    ensure!(local.git(&["status", "--porcelain"])?.is_empty());
    Ok(())
}

#[test]
fn sync_merges_nonconflicting_divergence_and_pushes_the_result() -> Result<()> {
    let fixture = RemoteFixture::tracked()?;
    let peer = fixture.peer_commit("peer.txt", "peer\n", "peer")?;
    fixture.local.write("local.txt", "local\n")?;
    fixture.local.git(&["add", "local.txt"])?;
    fixture.local.git(&["commit", "--message=local"])?;
    let local = fixture.local.git(&["rev-parse", "HEAD"])?;
    fixture.local.git(&["config", "pull.rebase", "false"])?;

    let synced = fixture.local.quinjet(&["sync", "--json"])?.success()?;

    ensure!(synced.stderr.is_empty(), "{}", synced.stderr);
    ensure!(synced.json()?["message"] == "Synchronization complete");
    let merged = fixture.local.git(&["rev-parse", "HEAD"])?;
    ensure!(fixture.remote_main()? == merged);
    ensure!(
        fixture
            .local
            .git(&["merge-base", "--is-ancestor", &peer, &merged])?
            .is_empty()
    );
    ensure!(
        fixture
            .local
            .git(&["merge-base", "--is-ancestor", &local, &merged])?
            .is_empty()
    );
    ensure!(
        fixture
            .local
            .git(&["rev-list", "--parents", "-n", "1", "HEAD"])?
            .split_whitespace()
            .count()
            == 3
    );
    ensure!(fixture.local.path.join("peer.txt").is_file());
    ensure!(fixture.local.path.join("local.txt").is_file());
    ensure!(fixture.local.git(&["status", "--porcelain"])?.is_empty());
    Ok(())
}

#[test]
fn sync_stops_after_a_pull_conflict_without_pushing() -> Result<()> {
    let fixture = RemoteFixture::tracked()?;
    let remote = fixture.peer_commit("README.md", "peer\n", "peer conflict")?;
    fixture.local.write("README.md", "local\n")?;
    fixture.local.git(&["add", "README.md"])?;
    fixture.local.git(&["commit", "--message=local conflict"])?;
    let local = fixture.local.git(&["rev-parse", "HEAD"])?;
    fixture.local.git(&["config", "pull.rebase", "false"])?;

    let synced = fixture.local.quinjet(&["sync"])?;

    assert_failure(&synced)?;
    ensure!(fixture.remote_main()? == remote);
    ensure!(fixture.local.git(&["rev-parse", "HEAD"])? == local);
    ensure!(
        fixture
            .local
            .git(&["diff", "--name-only", "--diff-filter=U"])?
            == "README.md"
    );
    Ok(())
}