1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
use anyhow::{Context, Result, bail};
use ngit::{
client::{Params, send_events, sign_event},
content_tags::{dedup_tags, tags_from_content},
};
use nostr::{EventBuilder, Tag, TagStandard, ToBech32, nips::nip19::Nip19Event};
use nostr_sdk::Kind;
use crate::{
client::{Client, Connect, fetching_with_report, get_repo_ref_from_cache},
git::{Repo, RepoActions},
login,
repo_ref::get_repo_coordinates_when_remote_unknown,
};
pub async fn launch(
title: Option<String>,
body: Option<String>,
labels: Vec<String>,
) -> Result<()> {
let git_repo = Repo::discover().context("failed to find a git repository")?;
let git_repo_path = git_repo.get_path()?;
let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
// Resolve title — required
let title = match title {
Some(t) if !t.trim().is_empty() => t,
_ => bail!("--title is required to create an issue"),
};
// Body defaults to empty string if not provided
let body = body.unwrap_or_default();
// Login
let (signer, user_ref, _) =
login::login_or_signup(&Some(&git_repo), &None, &None, Some(&client), true).await?;
// Build NIP-34 GitIssue event (kind 1621)
// Tags:
// - `a` coordinate tags for each maintainer's repo announcement
// - `subject` — issue title
// - `t` — hashtag labels
// - `alt` — human-readable summary
let mut tags: Vec<Tag> = vec![];
// Repo coordinate tags (one per maintainer)
for coord in repo_ref.coordinates() {
tags.push(Tag::from_standardized(TagStandard::Coordinate {
coordinate: coord.coordinate.clone(),
relay_url: coord.relays.first().cloned(),
uppercase: false,
}));
}
// Subject (title)
tags.push(Tag::parse(vec!["subject".to_string(), title.clone()])?);
// Hashtag labels
for label in &labels {
tags.push(Tag::hashtag(label));
}
// Alt text
tags.push(Tag::custom(
nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
vec![format!("git issue: {title}")],
));
// Maintainer p-tags (so they get notified)
for pk in &repo_ref.maintainers {
tags.push(Tag::public_key(*pk));
}
// NIP-21 mention tags: q tags for cited events/addresses, p tags for cited
// pubkeys
tags.extend(tags_from_content(&body, Some(git_repo_path)).await?);
let tags = dedup_tags(tags);
let issue_event = sign_event(
EventBuilder::new(Kind::GitIssue, body).tags(tags),
&signer,
"create issue".to_string(),
)
.await?;
let event_id = issue_event.id;
let mut client = client;
client.set_signer(signer).await;
send_events(
&client,
Some(git_repo_path),
vec![issue_event],
user_ref.relays.write(),
repo_ref.relays.clone(),
true,
false,
)
.await?;
let event_bech32 = if let Some(relay) = repo_ref.relays.first() {
Nip19Event {
event_id,
relays: vec![relay.clone()],
author: None,
kind: None,
}
.to_bech32()?
} else {
event_id.to_bech32()?
};
println!("issue created: {event_id}");
let dim = console::Style::new().color256(247);
println!(
"{}",
dim.apply_to(format!(
"view in gitworkshop.dev: https://gitworkshop.dev/{}",
&event_bech32,
))
);
Ok(())
}
|