mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(cli): keep Git activity cards in source threads
Carry the original thread root separately from the triggering message so generated repository activity remains visible in the conversation that requested it.
This commit is contained in:
@@ -17,6 +17,7 @@ pub(crate) struct GitConversationContext {
|
||||
struct ActivityContext {
|
||||
channel_id: Uuid,
|
||||
source_message: EventId,
|
||||
thread_root: EventId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -41,15 +42,22 @@ impl GitEntityType {
|
||||
pub(crate) fn parse_git_conversation_context(
|
||||
channel: Option<&str>,
|
||||
source_message: Option<&str>,
|
||||
thread_root: Option<&str>,
|
||||
) -> Result<GitConversationContext, CliError> {
|
||||
if source_message.is_some() && channel.is_none() {
|
||||
return Err(CliError::Usage(
|
||||
"--source-message requires --channel".into(),
|
||||
));
|
||||
}
|
||||
if thread_root.is_some() && source_message.is_none() {
|
||||
return Err(CliError::Usage(
|
||||
"--thread-root requires --source-message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let channel_id = channel.map(parse_uuid).transpose()?;
|
||||
let source_event = source_message.map(parse_event_id).transpose()?;
|
||||
let thread_root = thread_root.map(parse_event_id).transpose()?;
|
||||
let source_message = match (channel_id, source_event) {
|
||||
(Some(channel_id), Some(source_message)) => Some(GitSourceMessage {
|
||||
channel_id: channel_id.to_string(),
|
||||
@@ -61,6 +69,7 @@ pub(crate) fn parse_git_conversation_context(
|
||||
(Some(channel_id), Some(source_message)) => Some(ActivityContext {
|
||||
channel_id,
|
||||
source_message,
|
||||
thread_root: thread_root.unwrap_or(source_message),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
@@ -116,7 +125,7 @@ fn build_activity_message(
|
||||
git_url: &str,
|
||||
) -> Result<EventBuilder, CliError> {
|
||||
let thread_ref = ThreadRef {
|
||||
root_event_id: context.source_message,
|
||||
root_event_id: context.thread_root,
|
||||
parent_event_id: context.source_message,
|
||||
};
|
||||
let content = format!("Created [{}]({git_url})", markdown_link_label(title.trim()));
|
||||
@@ -252,11 +261,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn source_message_requires_channel_and_valid_values() {
|
||||
assert!(parse_git_conversation_context(None, Some(&"a".repeat(64))).is_err());
|
||||
assert!(parse_git_conversation_context(Some("not-a-uuid"), None).is_err());
|
||||
assert!(parse_git_conversation_context(None, Some(&"a".repeat(64)), None).is_err());
|
||||
assert!(parse_git_conversation_context(Some("not-a-uuid"), None, None).is_err());
|
||||
assert!(parse_git_conversation_context(
|
||||
Some("11111111-1111-4111-8111-111111111111"),
|
||||
Some("not-an-event")
|
||||
None,
|
||||
Some(&"c".repeat(64)),
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_git_conversation_context(
|
||||
Some("11111111-1111-4111-8111-111111111111"),
|
||||
Some("not-an-event"),
|
||||
None,
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
@@ -267,6 +283,7 @@ mod tests {
|
||||
let context = ActivityContext {
|
||||
channel_id: Uuid::parse_str("11111111-1111-4111-8111-111111111111").expect("channel"),
|
||||
source_message: source,
|
||||
thread_root: source,
|
||||
};
|
||||
let event = build_activity_message(
|
||||
&context,
|
||||
@@ -291,4 +308,32 @@ mod tests {
|
||||
.iter()
|
||||
.any(|tag| { tag.as_slice() == ["e", &"b".repeat(64), "", "reply"] }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn activity_message_preserves_existing_thread_root() {
|
||||
let source = parse_event_id(&"b".repeat(64)).expect("source event");
|
||||
let root = parse_event_id(&"c".repeat(64)).expect("root event");
|
||||
let context = ActivityContext {
|
||||
channel_id: Uuid::parse_str("11111111-1111-4111-8111-111111111111").expect("channel"),
|
||||
source_message: source,
|
||||
thread_root: root,
|
||||
};
|
||||
let event = build_activity_message(
|
||||
&context,
|
||||
"Repository",
|
||||
"buzz://git?repo=owner%3Arepo&type=repository",
|
||||
)
|
||||
.expect("activity builder")
|
||||
.sign_with_keys(&Keys::generate())
|
||||
.expect("signed activity");
|
||||
|
||||
assert!(event
|
||||
.tags
|
||||
.iter()
|
||||
.any(|tag| tag.as_slice() == ["e", &"c".repeat(64), "", "root"]));
|
||||
assert!(event
|
||||
.tags
|
||||
.iter()
|
||||
.any(|tag| tag.as_slice() == ["e", &"b".repeat(64), "", "reply"]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@ pub async fn cmd_create_issue(
|
||||
to: &[String],
|
||||
channel: Option<&str>,
|
||||
source_message: Option<&str>,
|
||||
thread_root: Option<&str>,
|
||||
) -> Result<(), CliError> {
|
||||
validate_hex64(repo_owner)?;
|
||||
validate_repo_id(repo_id)?;
|
||||
let body = read_or_stdin(content)?;
|
||||
let context = parse_git_conversation_context(channel, source_message)?;
|
||||
let context = parse_git_conversation_context(channel, source_message, thread_root)?;
|
||||
|
||||
let meta = GitIssueMeta {
|
||||
labels: labels.to_vec(),
|
||||
@@ -170,6 +171,7 @@ pub async fn dispatch(cmd: crate::IssuesCmd, client: &BuzzClient) -> Result<(),
|
||||
to,
|
||||
channel,
|
||||
source_message,
|
||||
thread_root,
|
||||
} => {
|
||||
cmd_create_issue(
|
||||
client,
|
||||
@@ -181,6 +183,7 @@ pub async fn dispatch(cmd: crate::IssuesCmd, client: &BuzzClient) -> Result<(),
|
||||
&to,
|
||||
channel.as_deref(),
|
||||
source_message.as_deref(),
|
||||
thread_root.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -25,11 +25,12 @@ pub async fn cmd_send_patch(
|
||||
committer: Option<&str>,
|
||||
channel: Option<&str>,
|
||||
source_message: Option<&str>,
|
||||
thread_root: Option<&str>,
|
||||
) -> Result<(), CliError> {
|
||||
validate_hex64(repo_owner)?;
|
||||
validate_repo_id(repo_id)?;
|
||||
let content = read_file_or_stdin(patch)?;
|
||||
let context = parse_git_conversation_context(channel, source_message)?;
|
||||
let context = parse_git_conversation_context(channel, source_message, thread_root)?;
|
||||
|
||||
let committer = match committer {
|
||||
Some(spec) => Some(parse_committer(spec)?),
|
||||
@@ -244,6 +245,7 @@ pub async fn dispatch(cmd: crate::PatchesCmd, client: &BuzzClient) -> Result<(),
|
||||
committer,
|
||||
channel,
|
||||
source_message,
|
||||
thread_root,
|
||||
} => {
|
||||
cmd_send_patch(
|
||||
client,
|
||||
@@ -261,6 +263,7 @@ pub async fn dispatch(cmd: crate::PatchesCmd, client: &BuzzClient) -> Result<(),
|
||||
committer.as_deref(),
|
||||
channel.as_deref(),
|
||||
source_message.as_deref(),
|
||||
thread_root.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -36,12 +36,13 @@ pub async fn cmd_open_pr(
|
||||
to: &[String],
|
||||
channel: Option<&str>,
|
||||
source_message: Option<&str>,
|
||||
thread_root: Option<&str>,
|
||||
revision_of: Option<&str>,
|
||||
) -> Result<(), CliError> {
|
||||
validate_hex64(repo_owner)?;
|
||||
validate_repo_id(repo_id)?;
|
||||
let content = read_optional_body(body, body_file)?;
|
||||
let context = parse_git_conversation_context(channel, source_message)?;
|
||||
let context = parse_git_conversation_context(channel, source_message, thread_root)?;
|
||||
|
||||
let repo = GitRepoCoord {
|
||||
owner: repo_owner.to_string(),
|
||||
@@ -243,6 +244,7 @@ pub async fn dispatch(cmd: crate::PrCmd, client: &BuzzClient) -> Result<(), CliE
|
||||
to,
|
||||
channel,
|
||||
source_message,
|
||||
thread_root,
|
||||
revision_of,
|
||||
} => {
|
||||
cmd_open_pr(
|
||||
@@ -261,6 +263,7 @@ pub async fn dispatch(cmd: crate::PrCmd, client: &BuzzClient) -> Result<(), CliE
|
||||
&to,
|
||||
channel.as_deref(),
|
||||
source_message.as_deref(),
|
||||
thread_root.as_deref(),
|
||||
revision_of.as_deref(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -212,9 +212,10 @@ pub async fn cmd_create_repo(
|
||||
relays: &[String],
|
||||
channel: Option<&str>,
|
||||
source_message: Option<&str>,
|
||||
thread_root: Option<&str>,
|
||||
) -> Result<(), CliError> {
|
||||
validate_repo_id(repo_id)?;
|
||||
let context = parse_git_conversation_context(channel, source_message)?;
|
||||
let context = parse_git_conversation_context(channel, source_message, thread_root)?;
|
||||
|
||||
let clone_refs: Vec<&str> = clone_urls.iter().map(|s| s.as_str()).collect();
|
||||
let relay_refs: Vec<&str> = relays.iter().map(|s| s.as_str()).collect();
|
||||
@@ -378,6 +379,7 @@ pub async fn dispatch(cmd: crate::ReposCmd, client: &BuzzClient) -> Result<(), C
|
||||
relays,
|
||||
channel,
|
||||
source_message,
|
||||
thread_root,
|
||||
} => {
|
||||
cmd_create_repo(
|
||||
client,
|
||||
@@ -389,6 +391,7 @@ pub async fn dispatch(cmd: crate::ReposCmd, client: &BuzzClient) -> Result<(), C
|
||||
&relays,
|
||||
channel.as_deref(),
|
||||
source_message.as_deref(),
|
||||
thread_root.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1119,6 +1119,9 @@ pub enum ReposCmd {
|
||||
/// Triggering Buzz message event ID; requires --channel
|
||||
#[arg(long, requires = "channel")]
|
||||
source_message: Option<String>,
|
||||
/// Original thread root when the triggering message is already a reply
|
||||
#[arg(long, requires = "source_message")]
|
||||
thread_root: Option<String>,
|
||||
},
|
||||
/// Get a repository announcement
|
||||
Get {
|
||||
@@ -1244,6 +1247,9 @@ pub enum PatchesCmd {
|
||||
/// Triggering Buzz message event ID; requires --channel
|
||||
#[arg(long, requires = "channel")]
|
||||
source_message: Option<String>,
|
||||
/// Original thread root when the triggering message is already a reply
|
||||
#[arg(long, requires = "source_message")]
|
||||
thread_root: Option<String>,
|
||||
},
|
||||
/// Get a patch by event id
|
||||
Get {
|
||||
@@ -1356,6 +1362,9 @@ pub enum PrCmd {
|
||||
/// Triggering Buzz message event ID; requires --channel
|
||||
#[arg(long, requires = "channel")]
|
||||
source_message: Option<String>,
|
||||
/// Original thread root when the triggering message is already a reply
|
||||
#[arg(long, requires = "source_message")]
|
||||
thread_root: Option<String>,
|
||||
/// Root patch event id this PR revises
|
||||
#[arg(long)]
|
||||
revision_of: Option<String>,
|
||||
@@ -1482,6 +1491,9 @@ pub enum IssuesCmd {
|
||||
/// Triggering Buzz message event ID; requires --channel
|
||||
#[arg(long, requires = "channel")]
|
||||
source_message: Option<String>,
|
||||
/// Original thread root when the triggering message is already a reply
|
||||
#[arg(long, requires = "source_message")]
|
||||
thread_root: Option<String>,
|
||||
},
|
||||
/// Get an issue by event id
|
||||
Get {
|
||||
@@ -1894,6 +1906,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_thread_root_requires_source_message() {
|
||||
let error = Cli::try_parse_from([
|
||||
"buzz",
|
||||
"repos",
|
||||
"create",
|
||||
"--id",
|
||||
"repo",
|
||||
"--thread-root",
|
||||
&"a".repeat(64),
|
||||
])
|
||||
.err()
|
||||
.expect("--source-message must be required");
|
||||
assert!(error.to_string().contains("--source-message"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_inventory_is_stable() {
|
||||
let expected_groups: Vec<&str> = vec![
|
||||
|
||||
@@ -49,8 +49,10 @@ When a Buzz message triggers `repos create`, `issues create`, `pr open`, or
|
||||
`patches send`, always pass `--channel <current-channel-uuid>` from `[Context]`
|
||||
and `--source-message <triggering-event-id>` from `[Event]`. The CLI records
|
||||
the exact source backlink and replies with a clickable Git activity card. Do
|
||||
not ask for values already present in the event context. `pr open --channel`
|
||||
still works without `--source-message` when only an origin `h` tag is needed.
|
||||
not ask for values already present in the event context. When `[Context]` has
|
||||
`Scope: thread`, also pass its `Thread root` as `--thread-root <event-id>` so
|
||||
the card appears in the current thread. `pr open --channel` still works without
|
||||
`--source-message` when only an origin `h` tag is needed.
|
||||
|
||||
Manage your repository's enforced branch and tag rules with `repos protect list|set|remove`. Ref patterns must use full Git names such as `refs/heads/main` or `refs/tags/*`; supported rules are `--push owner|admin|member`, `--no-force-push`, `--no-delete`, and `--require-patch`. `protect set` replaces the complete rule for that exact pattern, so omitted constraints are removed. Protection updates preserve every unrelated metadata tag and return exit code 5 when a newer NIP-33 head wins a concurrent write.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user