+54
-1
crates/tangled-api/src/client.rs
+54
-1
crates/tangled-api/src/client.rs
···
49
49
Ok(res.json::<TRes>().await?)
50
50
}
51
51
52
-
async fn get_json<TRes: DeserializeOwned>(
52
+
pub async fn get_json<TRes: DeserializeOwned>(
53
53
&self,
54
54
method: &str,
55
55
params: &[(&str, String)],
···
1121
1121
.post_json("com.atproto.repo.createRecord", &req, Some(access_jwt))
1122
1122
.await?;
1123
1123
Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in pull comment uri"))
1124
+
}
1125
+
1126
+
pub async fn merge_pull(
1127
+
&self,
1128
+
pull_did: &str,
1129
+
pull_rkey: &str,
1130
+
repo_did: &str,
1131
+
repo_name: &str,
1132
+
pds_base: &str,
1133
+
access_jwt: &str,
1134
+
) -> Result<()> {
1135
+
// Fetch the pull request to get patch and target branch
1136
+
let pds_client = TangledClient::new(pds_base);
1137
+
let pull = pds_client
1138
+
.get_pull_record(pull_did, pull_rkey, Some(access_jwt))
1139
+
.await?;
1140
+
1141
+
// Get service auth token for the knot
1142
+
let sa = self.service_auth_token(pds_base, access_jwt).await?;
1143
+
1144
+
#[derive(Serialize)]
1145
+
struct MergeReq<'a> {
1146
+
did: &'a str,
1147
+
name: &'a str,
1148
+
patch: &'a str,
1149
+
branch: &'a str,
1150
+
#[serde(skip_serializing_if = "Option::is_none")]
1151
+
#[serde(rename = "commitMessage")]
1152
+
commit_message: Option<&'a str>,
1153
+
#[serde(skip_serializing_if = "Option::is_none")]
1154
+
#[serde(rename = "commitBody")]
1155
+
commit_body: Option<&'a str>,
1156
+
}
1157
+
1158
+
let commit_body = if pull.body.is_empty() {
1159
+
None
1160
+
} else {
1161
+
Some(pull.body.as_str())
1162
+
};
1163
+
1164
+
let req = MergeReq {
1165
+
did: repo_did,
1166
+
name: repo_name,
1167
+
patch: &pull.patch,
1168
+
branch: &pull.target.branch,
1169
+
commit_message: Some(&pull.title),
1170
+
commit_body,
1171
+
};
1172
+
1173
+
let _: serde_json::Value = self
1174
+
.post_json("sh.tangled.repo.merge", &req, Some(&sa))
1175
+
.await?;
1176
+
Ok(())
1124
1177
}
1125
1178
}
1126
1179
-40
crates/tangled-cli/src/cli.rs
-40
crates/tangled-cli/src/cli.rs
···
284
284
#[derive(Args, Debug, Clone)]
285
285
pub struct PrMergeArgs {
286
286
pub id: String,
287
-
#[arg(long, default_value_t = false)]
288
-
pub squash: bool,
289
-
#[arg(long, default_value_t = false)]
290
-
pub rebase: bool,
291
-
#[arg(long, default_value_t = false)]
292
-
pub no_ff: bool,
293
287
}
294
288
295
289
#[derive(Subcommand, Debug, Clone)]
296
290
pub enum KnotCommand {
297
-
List(KnotListArgs),
298
-
Add(KnotAddArgs),
299
-
Verify(KnotVerifyArgs),
300
-
SetDefault(KnotRefArgs),
301
-
Remove(KnotRefArgs),
302
291
/// Migrate a repository to another knot
303
292
Migrate(KnotMigrateArgs),
304
-
}
305
-
306
-
#[derive(Args, Debug, Clone)]
307
-
pub struct KnotListArgs {
308
-
#[arg(long, default_value_t = false)]
309
-
pub public: bool,
310
-
#[arg(long, default_value_t = false)]
311
-
pub owned: bool,
312
-
}
313
-
314
-
#[derive(Args, Debug, Clone)]
315
-
pub struct KnotAddArgs {
316
-
pub url: String,
317
-
#[arg(long)]
318
-
pub did: Option<String>,
319
-
#[arg(long)]
320
-
pub name: Option<String>,
321
-
#[arg(long, default_value_t = false)]
322
-
pub verify: bool,
323
-
}
324
-
325
-
#[derive(Args, Debug, Clone)]
326
-
pub struct KnotVerifyArgs {
327
-
pub url: String,
328
-
}
329
-
330
-
#[derive(Args, Debug, Clone)]
331
-
pub struct KnotRefArgs {
332
-
pub url: String,
333
293
}
334
294
335
295
#[derive(Args, Debug, Clone)]
+1
-39
crates/tangled-cli/src/commands/knot.rs
+1
-39
crates/tangled-cli/src/commands/knot.rs
···
1
-
use crate::cli::{
2
-
Cli, KnotAddArgs, KnotCommand, KnotListArgs, KnotMigrateArgs, KnotRefArgs, KnotVerifyArgs,
3
-
};
1
+
use crate::cli::{Cli, KnotCommand, KnotMigrateArgs};
4
2
use anyhow::anyhow;
5
3
use anyhow::Result;
6
4
use git2::{Direction, Repository as GitRepository, StatusOptions};
···
9
7
10
8
pub async fn run(_cli: &Cli, cmd: KnotCommand) -> Result<()> {
11
9
match cmd {
12
-
KnotCommand::List(args) => list(args).await,
13
-
KnotCommand::Add(args) => add(args).await,
14
-
KnotCommand::Verify(args) => verify(args).await,
15
-
KnotCommand::SetDefault(args) => set_default(args).await,
16
-
KnotCommand::Remove(args) => remove(args).await,
17
10
KnotCommand::Migrate(args) => migrate(args).await,
18
11
}
19
-
}
20
-
21
-
async fn list(args: KnotListArgs) -> Result<()> {
22
-
println!(
23
-
"Knot list (stub) public={} owned={}",
24
-
args.public, args.owned
25
-
);
26
-
Ok(())
27
-
}
28
-
29
-
async fn add(args: KnotAddArgs) -> Result<()> {
30
-
println!(
31
-
"Knot add (stub) url={} did={:?} name={:?} verify={}",
32
-
args.url, args.did, args.name, args.verify
33
-
);
34
-
Ok(())
35
-
}
36
-
37
-
async fn verify(args: KnotVerifyArgs) -> Result<()> {
38
-
println!("Knot verify (stub) url={}", args.url);
39
-
Ok(())
40
-
}
41
-
42
-
async fn set_default(args: KnotRefArgs) -> Result<()> {
43
-
println!("Knot set-default (stub) url={}", args.url);
44
-
Ok(())
45
-
}
46
-
47
-
async fn remove(args: KnotRefArgs) -> Result<()> {
48
-
println!("Knot remove (stub) url={}", args.url);
49
-
Ok(())
50
12
}
51
13
52
14
async fn migrate(args: KnotMigrateArgs) -> Result<()> {
+65
-3
crates/tangled-cli/src/commands/pr.rs
+65
-3
crates/tangled-cli/src/commands/pr.rs
···
183
183
Ok(())
184
184
}
185
185
186
-
async fn merge(_args: PrMergeArgs) -> Result<()> {
187
-
// Placeholder: merging requires server-side merge call with the patch and target branch.
188
-
println!("Merge via CLI is not implemented yet. Use the web UI for now.");
186
+
async fn merge(args: PrMergeArgs) -> Result<()> {
187
+
let mgr = SessionManager::default();
188
+
let session = mgr
189
+
.load()?
190
+
.ok_or_else(|| anyhow!("Please login first: tangled auth login"))?;
191
+
let (did, rkey) = parse_record_id(&args.id, &session.did)?;
192
+
let pds = session
193
+
.pds
194
+
.clone()
195
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
196
+
.unwrap_or_else(|| "https://bsky.social".into());
197
+
198
+
// Get the PR to find the target repo
199
+
let pds_client = tangled_api::TangledClient::new(&pds);
200
+
let pull = pds_client
201
+
.get_pull_record(&did, &rkey, Some(session.access_jwt.as_str()))
202
+
.await?;
203
+
204
+
// Parse the target repo AT-URI to get did and name
205
+
let target_repo = &pull.target.repo;
206
+
// Format: at://did:plc:.../sh.tangled.repo/rkey
207
+
let parts: Vec<&str> = target_repo.strip_prefix("at://").unwrap_or(target_repo).split('/').collect();
208
+
if parts.len() < 2 {
209
+
return Err(anyhow!("Invalid target repo AT-URI: {}", target_repo));
210
+
}
211
+
let repo_did = parts[0];
212
+
213
+
// Get repo info to find the name
214
+
// Parse rkey from target repo AT-URI
215
+
let repo_rkey = if parts.len() >= 4 {
216
+
parts[3]
217
+
} else {
218
+
return Err(anyhow!("Invalid target repo AT-URI: {}", target_repo));
219
+
};
220
+
221
+
#[derive(serde::Deserialize)]
222
+
struct Rec {
223
+
name: String,
224
+
}
225
+
#[derive(serde::Deserialize)]
226
+
struct GetRes {
227
+
value: Rec,
228
+
}
229
+
let params = [
230
+
("repo", repo_did.to_string()),
231
+
("collection", "sh.tangled.repo".to_string()),
232
+
("rkey", repo_rkey.to_string()),
233
+
];
234
+
let repo_rec: GetRes = pds_client
235
+
.get_json("com.atproto.repo.getRecord", ¶ms, Some(session.access_jwt.as_str()))
236
+
.await?;
237
+
238
+
// Call merge on the default Tangled API base (tngl.sh)
239
+
let api = tangled_api::TangledClient::default();
240
+
api.merge_pull(
241
+
&did,
242
+
&rkey,
243
+
repo_did,
244
+
&repo_rec.value.name,
245
+
&pds,
246
+
&session.access_jwt,
247
+
)
248
+
.await?;
249
+
250
+
println!("Merged PR {}:{}", did, rkey);
189
251
Ok(())
190
252
}
191
253