heddle-cli 0.11.0

An AI-native version control system
Documentation
// SPDX-License-Identifier: Apache-2.0

use std::{
    path::{Path, PathBuf},
    process::Command,
};

use serde_json::Value;
use sley::{
    CommitObject, EntryKind, GitObjectType, ObjectId, RefPrecondition, ReferenceTarget,
    Repository as SleyRepository,
    plumbing::{sley_object::EncodedObject, sley_refs::ReflogEntry},
};
use tempfile::TempDir;

#[path = "support/git_https.rs"]
mod git_https;

use git_https::PrivateCaGitServer;

struct PullFixture {
    ca_path: PathBuf,
    checkout: PathBuf,
    _server: PrivateCaGitServer,
    source_path: PathBuf,
    temp: TempDir,
}

impl PullFixture {
    fn new() -> Self {
        let temp = TempDir::new().expect("tempdir");
        let source_path = temp.path().join("source.git");
        let source = SleyRepository::init_bare(&source_path).expect("source repository");
        let first = write_commit(&source, None, b"one\n");
        publish_main(&source, None, first);
        source
            .apply_config_edit_plan(
                sley::ConfigEditPlan::new(source.common_dir().join("config")).with_operation(
                    sley::ConfigEdit::set("http.receivepack", "true")
                        .expect("receive-pack config edit"),
                ),
            )
            .expect("enable receive-pack");
        std::fs::write(source_path.join("HEAD"), b"ref: refs/heads/main\n").expect("source HEAD");
        let checkout = temp.path().join("checkout");
        let cloned = heddle(
            &temp,
            temp.path(),
            &["clone", path(&source_path), path(&checkout)],
        );
        assert!(cloned.status.success(), "local clone: {}", stderr(&cloned));

        let server = PrivateCaGitServer::spawn(temp.path());
        let local = SleyRepository::discover(&checkout).expect("checkout repository");
        local
            .apply_config_edit_plan(
                sley::ConfigEditPlan::new(local.common_dir().join("config")).with_operation(
                    sley::ConfigEdit::set("remote.origin.url", server.url())
                        .expect("remote URL edit"),
                ),
            )
            .expect("set HTTPS remote");
        let second = write_commit(&source, Some(first), b"two\n");
        publish_main(&source, Some(first), second);
        let ca_path = temp.path().join("private-ca.pem");
        std::fs::write(&ca_path, server.ca_pem()).expect("write private CA");
        Self {
            ca_path,
            checkout,
            _server: server,
            source_path,
            temp,
        }
    }

    fn pull(&self, ca: bool) -> std::process::Output {
        let mut command = heddle_command(&self.temp, &self.checkout);
        command.args(["--output", "json", "pull"]);
        if ca {
            command.env("HEDDLE_REMOTE_TLS_CA_CERT", &self.ca_path);
        }
        command.output().expect("run Heddle pull")
    }

    fn push_new_commit(&self) -> (std::process::Output, ObjectId) {
        let checkout = SleyRepository::discover(&self.checkout).expect("checkout repository");
        let old = checkout
            .find_reference("refs/heads/main")
            .expect("main lookup")
            .expect("main ref")
            .peeled_oid(&checkout)
            .expect("peel main")
            .expect("main OID");
        let new = write_commit(&checkout, Some(old), b"three\n");
        publish_main(&checkout, Some(old), new);

        let mut command = heddle_command(&self.temp, &self.checkout);
        command
            .args(["--output", "json", "push"])
            .env("HEDDLE_REMOTE_TLS_CA_CERT", &self.ca_path);
        (command.output().expect("run Heddle push"), new)
    }
}

#[test]
fn git_overlay_pull_honours_remote_tls_ca_cert() {
    let fixture = PullFixture::new();
    let pulled = fixture.pull(true);
    assert!(
        pulled.status.success(),
        "private-CA pull failed: {}",
        stderr(&pulled)
    );
    let output: Value = serde_json::from_slice(&pulled.stdout).expect("pull JSON");
    assert_eq!(output["transport"], "git");
    assert_eq!(
        std::fs::read_to_string(fixture.checkout.join("tracked.txt")).expect("checkout file"),
        "two\n"
    );
}

#[test]
fn git_overlay_tls_failure_names_remote_ca_configuration() {
    let fixture = PullFixture::new();
    let pulled = fixture.pull(false);
    assert!(!pulled.status.success(), "untrusted private CA must fail");
    assert!(
        stderr(&pulled).contains("HEDDLE_REMOTE_TLS_CA_CERT"),
        "TLS failure must name the CA configuration: {}",
        stderr(&pulled)
    );
}

#[test]
fn git_overlay_push_honours_remote_tls_ca_cert() {
    let fixture = PullFixture::new();
    let pulled = fixture.pull(true);
    assert!(pulled.status.success(), "fixture pull: {}", stderr(&pulled));
    let (pushed, expected) = fixture.push_new_commit();
    assert!(
        pushed.status.success(),
        "private-CA push failed: {}",
        stderr(&pushed)
    );
    let source = SleyRepository::open(&fixture.source_path).expect("source repository");
    let actual = source
        .find_reference("refs/heads/main")
        .expect("main lookup")
        .expect("main ref")
        .peeled_oid(&source)
        .expect("peel main");
    assert_eq!(actual, Some(expected));
}

fn write_commit(repo: &SleyRepository, parent: Option<ObjectId>, content: &[u8]) -> ObjectId {
    let blob = repo.write_blob(content).expect("blob");
    let empty = repo
        .write_tree(sley::TreeEditor::new())
        .expect("empty tree");
    let mut tree = repo.edit_tree(&empty).expect("edit tree");
    tree.upsert("tracked.txt", EntryKind::Blob, blob);
    let tree = repo.write_tree(tree).expect("tree");
    let identity = b"Heddle Test <heddle@example.com> 0 +0000".to_vec();
    let commit = CommitObject {
        tree,
        parents: parent.into_iter().collect(),
        author: identity.clone(),
        committer: identity,
        encoding: None,
        message: content.to_vec(),
    };
    repo.write_object(EncodedObject::new(GitObjectType::Commit, commit.write()))
        .expect("commit")
}

fn publish_main(repo: &SleyRepository, old: Option<ObjectId>, new: ObjectId) {
    let references = repo.references();
    let mut transaction = references.transaction();
    transaction.update_to(
        "refs/heads/main",
        ReferenceTarget::Direct(new),
        RefPrecondition::Any,
        Some(ReflogEntry {
            old_oid: old.unwrap_or_else(|| ObjectId::null(repo.object_format())),
            new_oid: new,
            committer: b"Heddle Test <heddle@example.com> 0 +0000".to_vec(),
            message: b"update main".to_vec(),
        }),
    );
    transaction.commit().expect("publish main");
}

fn heddle(temp: &TempDir, cwd: &Path, args: &[&str]) -> std::process::Output {
    heddle_command(temp, cwd)
        .args(args)
        .output()
        .expect("run Heddle")
}

fn heddle_command(temp: &TempDir, cwd: &Path) -> Command {
    let config = temp.path().join("config.toml");
    std::fs::write(
        &config,
        "[principal]\nname = \"Heddle Test\"\nemail = \"heddle@example.com\"\n",
    )
    .expect("write Heddle config");
    let mut command = Command::new(env!("CARGO_BIN_EXE_heddle"));
    command
        .current_dir(cwd)
        .env("PATH", "")
        .env("HOME", temp.path())
        .env("HEDDLE_CONFIG", config)
        .env("NO_COLOR", "1")
        .env_remove("HEDDLE_REMOTE_TLS_CA_CERT")
        .env_remove("SSL_CERT_FILE")
        .env_remove("SSL_CERT_DIR");
    command
}

fn path(path: &Path) -> &str {
    path.to_str().expect("UTF-8 path")
}

fn stderr(output: &std::process::Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}