+36
TODO.md
+36
TODO.md
···
1
+
# TODO - Tech Debt
2
+
3
+
## Pull Request Support
4
+
5
+
### Branch-Based PR Merge
6
+
- [ ] Implement branch-based PR merge support in CLI
7
+
- **Issue**: Currently only patch-based PRs can be merged via `tangled pr merge`
8
+
- **Location**: `crates/tangled-api/src/client.rs:1250-1253`
9
+
- **Current behavior**: Returns error: "Cannot merge branch-based PR via CLI. Please use the web interface."
10
+
- **Required**: Add support for merging PRs that have a `source` field with SHA/branch info instead of a `patch` field
11
+
- **Related**: Server-side merge API may need updates to support branch merges
12
+
13
+
### PR Comments Display
14
+
- [ ] Implement `--comments` flag functionality in `pr show` command
15
+
- **Issue**: Flag is defined but not implemented
16
+
- **Location**: `crates/tangled-cli/src/commands/pr.rs:145-180`
17
+
- **Current behavior**: `tangled pr show <id> --comments` doesn't display any comments
18
+
- **Required**:
19
+
- Fetch comments from the API
20
+
- Display comment author, timestamp, and content
21
+
- Handle threaded/nested comments if supported
22
+
- **API**: Need to determine correct endpoint for fetching PR comments
23
+
24
+
### PR Format Compatibility
25
+
- [x] Support both patch-based and branch-based PR formats
26
+
- **Completed**: Added `PullSource` struct and made `patch` field optional
27
+
- **Location**: `crates/tangled-api/src/client.rs:1392-1413`
28
+
- **Details**: PRs can now have either:
29
+
- `patch: String` (legacy format)
30
+
- `source: { sha, repo?, branch? }` (new format)
31
+
32
+
## Related Issues
33
+
34
+
- Consider adding `--format json` output for programmatic access to PR data
35
+
- Add better error messages when operations aren't supported for certain PR types
36
+
- Document the differences between patch-based and branch-based PRs in user docs
+59
-2
crates/tangled-api/src/client.rs
+59
-2
crates/tangled-api/src/client.rs
···
987
987
Ok(out)
988
988
}
989
989
990
+
pub async fn list_repo_pulls(
991
+
&self,
992
+
repo_at: &str,
993
+
state: Option<&str>,
994
+
pds_base: &str,
995
+
access_jwt: &str,
996
+
) -> Result<Vec<RepoPull>> {
997
+
let sa = self.service_auth_token(pds_base, access_jwt).await?;
998
+
999
+
#[derive(Deserialize)]
1000
+
struct Res {
1001
+
pulls: Vec<RepoPull>,
1002
+
}
1003
+
1004
+
let mut params = vec![("repo", repo_at.to_string())];
1005
+
if let Some(s) = state {
1006
+
params.push(("state", s.to_string()));
1007
+
}
1008
+
1009
+
let res: Res = self
1010
+
.get_json("sh.tangled.repo.listPulls", ¶ms, Some(&sa))
1011
+
.await?;
1012
+
Ok(res.pulls)
1013
+
}
1014
+
990
1015
#[allow(clippy::too_many_arguments)]
991
1016
pub async fn create_pull(
992
1017
&self,
···
1222
1247
Some(pull.body.as_str())
1223
1248
};
1224
1249
1250
+
// For now, only patch-based PRs can be merged via CLI
1251
+
// Branch-based PRs need to be merged via the web interface
1252
+
let patch_str = pull.patch.as_deref()
1253
+
.ok_or_else(|| anyhow!("Cannot merge branch-based PR via CLI. Please use the web interface."))?;
1254
+
1225
1255
let req = MergeReq {
1226
1256
did: repo_did,
1227
1257
name: repo_name,
1228
-
patch: &pull.patch,
1258
+
patch: patch_str,
1229
1259
branch: &pull.target.branch,
1230
1260
commit_message: Some(&pull.title),
1231
1261
commit_body,
···
1365
1395
}
1366
1396
1367
1397
#[derive(Debug, Clone, Serialize, Deserialize)]
1398
+
pub struct PullSource {
1399
+
pub sha: String,
1400
+
#[serde(default)]
1401
+
pub repo: Option<String>,
1402
+
#[serde(default)]
1403
+
pub branch: Option<String>,
1404
+
}
1405
+
1406
+
#[derive(Debug, Clone, Serialize, Deserialize)]
1368
1407
pub struct Pull {
1369
1408
pub target: PullTarget,
1370
1409
pub title: String,
1371
1410
#[serde(default)]
1372
1411
pub body: String,
1373
-
pub patch: String,
1412
+
#[serde(default)]
1413
+
pub patch: Option<String>,
1414
+
#[serde(default)]
1415
+
pub source: Option<PullSource>,
1374
1416
#[serde(rename = "createdAt")]
1375
1417
pub created_at: String,
1376
1418
}
···
1380
1422
pub author_did: String,
1381
1423
pub rkey: String,
1382
1424
pub pull: Pull,
1425
+
}
1426
+
1427
+
#[derive(Debug, Clone, Deserialize)]
1428
+
pub struct RepoPull {
1429
+
pub rkey: String,
1430
+
#[serde(rename = "ownerDid")]
1431
+
pub owner_did: String,
1432
+
#[serde(rename = "pullId")]
1433
+
pub pull_id: i32,
1434
+
pub title: String,
1435
+
pub state: i32,
1436
+
#[serde(rename = "targetBranch")]
1437
+
pub target_branch: String,
1438
+
#[serde(rename = "createdAt")]
1439
+
pub created_at: String,
1383
1440
}
1384
1441
1385
1442
#[derive(Debug, Clone)]
+1
-1
crates/tangled-cli/src/cli.rs
+1
-1
crates/tangled-cli/src/cli.rs
+56
-18
crates/tangled-cli/src/commands/pr.rs
+56
-18
crates/tangled-cli/src/commands/pr.rs
···
21
21
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
22
22
.unwrap_or_else(|| "https://bsky.social".into());
23
23
let client = tangled_api::TangledClient::new(&pds);
24
-
let target_repo_at = if let Some(repo) = &args.repo {
24
+
25
+
// NEW: If --repo is specified, use the new API to list all PRs for that repo
26
+
if let Some(repo) = &args.repo {
25
27
let (owner, name) = parse_repo_ref(repo, &session.handle);
26
28
let info = client
27
29
.get_repo_info(owner, name, Some(session.access_jwt.as_str()))
28
30
.await?;
29
-
Some(format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey))
30
-
} else {
31
-
None
32
-
};
33
-
let pulls = client
34
-
.list_pulls(
35
-
&session.did,
36
-
target_repo_at.as_deref(),
37
-
Some(session.access_jwt.as_str()),
38
-
)
39
-
.await?;
40
-
if pulls.is_empty() {
41
-
println!("No pull requests found (showing only those you created)");
31
+
let repo_at = format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey);
32
+
33
+
// Use Tangled API (tngl.sh) instead of PDS for aggregated query
34
+
let api_client = tangled_api::TangledClient::default();
35
+
let state = args.state.as_deref();
36
+
let pulls = api_client
37
+
.list_repo_pulls(&repo_at, state, &pds, &session.access_jwt)
38
+
.await?;
39
+
40
+
if pulls.is_empty() {
41
+
println!("No pull requests found for this repository");
42
+
} else {
43
+
println!("OWNER\tID\tTITLE\tSTATE");
44
+
for pr in pulls {
45
+
let state_str = match pr.state {
46
+
1 => "open",
47
+
0 => "closed",
48
+
2 => "merged",
49
+
_ => "unknown",
50
+
};
51
+
println!("{}\t{}\t{}\t{}", pr.owner_did, pr.pull_id, pr.title, state_str);
52
+
}
53
+
}
42
54
} else {
43
-
println!("RKEY\tTITLE\tTARGET");
44
-
for pr in pulls {
45
-
println!("{}\t{}\t{}", pr.rkey, pr.pull.title, pr.pull.target.repo);
55
+
// OLD: Without --repo, show only user's PRs (existing behavior)
56
+
let pulls = client
57
+
.list_pulls(
58
+
&session.did,
59
+
None,
60
+
Some(session.access_jwt.as_str()),
61
+
)
62
+
.await?;
63
+
if pulls.is_empty() {
64
+
println!("No pull requests found (showing only those you created)");
65
+
} else {
66
+
println!("RKEY\tTITLE\tTARGET");
67
+
for pr in pulls {
68
+
println!("{}\t{}\t{}", pr.rkey, pr.pull.title, pr.pull.target.repo);
69
+
}
46
70
}
47
71
}
48
72
Ok(())
···
135
159
println!("BODY:\n{}", pr.body);
136
160
}
137
161
println!("TARGET: {} @ {}", pr.target.repo, pr.target.branch);
162
+
163
+
// Display source info if it's a branch-based PR
164
+
if let Some(source) = &pr.source {
165
+
println!("SOURCE: {} ({})", source.sha,
166
+
source.branch.as_deref().unwrap_or("detached"));
167
+
if let Some(repo) = &source.repo {
168
+
println!("SOURCE REPO: {}", repo);
169
+
}
170
+
}
171
+
138
172
if args.diff {
139
-
println!("PATCH:\n{}", pr.patch);
173
+
if let Some(patch) = &pr.patch {
174
+
println!("PATCH:\n{}", patch);
175
+
} else {
176
+
println!("(No patch available - this is a branch-based PR)");
177
+
}
140
178
}
141
179
Ok(())
142
180
}
+26
-1
crates/tangled-cli/src/commands/spindle.rs
+26
-1
crates/tangled-cli/src/commands/spindle.rs
···
250
250
.unwrap_or_else(|| "https://spindle.tangled.sh".to_string());
251
251
let api = tangled_api::TangledClient::new(&spindle_base);
252
252
253
-
api.add_repo_secret(&pds, &session.access_jwt, &repo_at, &args.key, &args.value)
253
+
// Handle special value patterns: @file or - (stdin)
254
+
let value = if args.value == "-" {
255
+
// Read from stdin
256
+
use std::io::Read;
257
+
let mut buffer = String::new();
258
+
std::io::stdin().read_to_string(&mut buffer)?;
259
+
buffer
260
+
} else if let Some(path) = args.value.strip_prefix('@') {
261
+
// Read from file, expand ~ if needed
262
+
let expanded_path = if path.starts_with("~/") {
263
+
if let Some(home) = std::env::var("HOME").ok() {
264
+
path.replacen("~/", &format!("{}/", home), 1)
265
+
} else {
266
+
path.to_string()
267
+
}
268
+
} else {
269
+
path.to_string()
270
+
};
271
+
std::fs::read_to_string(&expanded_path)
272
+
.map_err(|e| anyhow!("Failed to read file '{}': {}", expanded_path, e))?
273
+
} else {
274
+
// Use value as-is
275
+
args.value
276
+
};
277
+
278
+
api.add_repo_secret(&pds, &session.access_jwt, &repo_at, &args.key, &value)
254
279
.await?;
255
280
println!("Added secret '{}' to {}", args.key, args.repo);
256
281
Ok(())