a73x

2cc57a53

Extract event-creation boilerplate into dag helpers

a73x   2026-03-22 08:29

Add build_event, append_action, and create_root_action convenience
wrappers in dag.rs. Replace 10 boilerplate blocks in issue.rs and 2 in
patch.rs that manually loaded signing keys, built Events, and appended.

Closes issue cb052012.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/src/dag.rs b/src/dag.rs
index e9e42be..179f370 100644
--- a/src/dag.rs
+++ b/src/dag.rs
@@ -2,8 +2,8 @@ use git2::{ObjectType, Oid, Repository, Sort};

use crate::error::Error;
use crate::event::{Action, Event};
use crate::identity::author_signature;
use crate::signing::sign_event;
use crate::identity::{author_signature, get_author};
use crate::signing::{self, sign_event};

/// The manifest blob content included in every event commit tree.
const MANIFEST_JSON: &[u8] = br#"{"version":1,"format":"git-collab"}"#;
@@ -132,6 +132,42 @@ pub fn append_event(
    Ok(oid)
}

/// Build an Event from the given action, filling in timestamp and author
/// automatically. The clock field is set to 0 (callers like `create_root_event`
/// and `append_event` overwrite it).
pub fn build_event(repo: &Repository, action: Action) -> Result<Event, Error> {
    let author = get_author(repo)?;
    Ok(Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action,
        clock: 0,
    })
}

/// Convenience wrapper: load signing key, build event, and append it to an
/// existing DAG ref in one call.
pub fn append_action(
    repo: &Repository,
    ref_name: &str,
    action: Action,
) -> Result<Oid, Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let event = build_event(repo, action)?;
    append_event(repo, ref_name, &event, &sk)
}

/// Convenience wrapper: load signing key, build event, and create a root
/// (orphan) DAG commit. Returns the new commit OID (entity ID).
pub fn create_root_action(
    repo: &Repository,
    action: Action,
) -> Result<Oid, Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let event = build_event(repo, action)?;
    create_root_event(repo, &event, &sk)
}

/// Walk the DAG from the given ref in topological order (oldest first).
/// Returns (commit_oid, event) pairs.
pub fn walk_events(repo: &Repository, ref_name: &str) -> Result<Vec<(Oid, Event)>, Error> {
diff --git a/src/issue.rs b/src/issue.rs
index fbc2b3b..0385825 100644
--- a/src/issue.rs
+++ b/src/issue.rs
@@ -2,9 +2,7 @@ use git2::Repository;

use crate::cli::{self, SortMode};
use crate::dag;
use crate::event::{Action, Event};
use crate::identity::get_author;
use crate::signing;
use crate::event::Action;
use crate::state::{self, IssueState};

pub fn open(
@@ -13,19 +11,14 @@ pub fn open(
    body: &str,
    relates_to: Option<&str>,
) -> Result<String, crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueOpen {
    let oid = dag::create_root_action(
        repo,
        Action::IssueOpen {
            title: title.to_string(),
            body: body.to_string(),
            relates_to: relates_to.map(|s| s.to_string()),
        },
        clock: 0,
    };
    let oid = dag::create_root_event(repo, &event, &sk)?;
    )?;
    let id = oid.to_string();
    let ref_name = format!("refs/collab/issues/{}", id);
    repo.reference(&ref_name, oid, false, "issue open")?;
@@ -144,34 +137,18 @@ pub fn show(repo: &Repository, id_prefix: &str) -> Result<IssueState, crate::err
}

pub fn label(repo: &Repository, id_prefix: &str, label: &str) -> Result<(), crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueLabel {
            label: label.to_string(),
        },
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::IssueLabel {
        label: label.to_string(),
    })?;
    Ok(())
}

pub fn unlabel(repo: &Repository, id_prefix: &str, label: &str) -> Result<(), crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueUnlabel {
            label: label.to_string(),
        },
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::IssueUnlabel {
        label: label.to_string(),
    })?;
    Ok(())
}

@@ -180,18 +157,10 @@ pub fn assign(
    id_prefix: &str,
    assignee: &str,
) -> Result<(), crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueAssign {
            assignee: assignee.to_string(),
        },
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::IssueAssign {
        assignee: assignee.to_string(),
    })?;
    Ok(())
}

@@ -200,18 +169,10 @@ pub fn unassign(
    id_prefix: &str,
    assignee: &str,
) -> Result<(), crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueUnassign {
            assignee: assignee.to_string(),
        },
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::IssueUnassign {
        assignee: assignee.to_string(),
    })?;
    Ok(())
}

@@ -226,35 +187,19 @@ pub fn edit(
            git2::Error::from_str("at least one of --title or --body must be provided").into(),
        );
    }
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueEdit {
            title: title.map(|s| s.to_string()),
            body: body.map(|s| s.to_string()),
        },
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::IssueEdit {
        title: title.map(|s| s.to_string()),
        body: body.map(|s| s.to_string()),
    })?;
    Ok(())
}

pub fn comment(repo: &Repository, id_prefix: &str, body: &str) -> Result<(), crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueComment {
            body: body.to_string(),
        },
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::IssueComment {
        body: body.to_string(),
    })?;
    Ok(())
}

@@ -263,18 +208,10 @@ pub fn close(
    id_prefix: &str,
    reason: Option<&str>,
) -> Result<(), crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, id) = state::resolve_issue_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueClose {
            reason: reason.map(|s| s.to_string()),
        },
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::IssueClose {
        reason: reason.map(|s| s.to_string()),
    })?;
    // Archive the ref (move to refs/collab/archive/issues/)
    if ref_name.starts_with("refs/collab/issues/") {
        state::archive_issue_ref(repo, &id)?;
@@ -289,15 +226,7 @@ pub fn delete(repo: &Repository, id_prefix: &str) -> Result<String, crate::error
}

pub fn reopen(repo: &Repository, id_prefix: &str) -> Result<(), crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::IssueReopen,
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::IssueReopen)?;
    Ok(())
}
diff --git a/src/patch.rs b/src/patch.rs
index d5bbf65..8846408 100644
--- a/src/patch.rs
+++ b/src/patch.rs
@@ -105,12 +105,9 @@ pub fn create(
    let commit = repo.find_commit(tip_oid)?;
    let tree_oid = commit.tree()?.id();

    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::PatchCreate {
    let oid = dag::create_root_action(
        repo,
        Action::PatchCreate {
            title: title.to_string(),
            body: body.to_string(),
            base_ref: base_ref.to_string(),
@@ -119,9 +116,7 @@ pub fn create(
            commit: tip_oid.to_string(),
            tree: tree_oid.to_string(),
        },
        clock: 0,
    };
    let oid = dag::create_root_event(repo, &event, &sk)?;
    )?;
    let id = oid.to_string();
    let ref_name = format!("refs/collab/patches/{}", id);
    repo.reference(&ref_name, oid, false, "patch create")?;
@@ -709,18 +704,10 @@ pub fn close(
    id_prefix: &str,
    reason: Option<&str>,
) -> Result<(), crate::error::Error> {
    let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
    let (ref_name, id) = state::resolve_patch_ref(repo, id_prefix)?;
    let author = get_author(repo)?;
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        author,
        action: Action::PatchClose {
            reason: reason.map(|s| s.to_string()),
        },
        clock: 0,
    };
    dag::append_event(repo, &ref_name, &event, &sk)?;
    dag::append_action(repo, &ref_name, Action::PatchClose {
        reason: reason.map(|s| s.to_string()),
    })?;
    // Archive the ref (move to refs/collab/archive/patches/)
    if ref_name.starts_with("refs/collab/patches/") {
        state::archive_patch_ref(repo, &id)?;