worktree_io/issue/parse/
mod.rs1mod shorthand;
2mod worktree_url;
3
4use anyhow::{bail, Context, Result};
5use url::Url;
6
7use super::{DeepLinkOptions, IssueRef};
8
9impl IssueRef {
10 pub fn parse(s: &str) -> Result<Self> {
18 let s = s.trim();
19
20 if s.starts_with("worktree://") {
21 let (issue, _opts) = worktree_url::parse_worktree_url(s)?;
22 return Ok(issue);
23 }
24
25 if s.starts_with("https://github.com") || s.starts_with("http://github.com") {
26 return parse_github_url(s);
27 }
28
29 if let Some(result) = shorthand::try_parse_shorthand(s) {
30 return result;
31 }
32
33 bail!(
34 "Could not parse issue reference: {s:?}\n\
35 Supported formats:\n\
36 - https://github.com/owner/repo/issues/42\n\
37 - worktree://open?owner=owner&repo=repo&issue=42\n\
38 - worktree://open?owner=owner&repo=repo&linear_id=<uuid>\n\
39 - owner/repo#42\n\
40 - owner/repo@<linear-uuid>"
41 )
42 }
43
44 pub fn parse_with_options(s: &str) -> Result<(Self, DeepLinkOptions)> {
47 let s = s.trim();
48 if s.starts_with("worktree://") {
49 return worktree_url::parse_worktree_url(s);
50 }
51 Ok((Self::parse(s)?, DeepLinkOptions::default()))
52 }
53}
54
55pub(super) fn parse_github_url(s: &str) -> Result<IssueRef> {
56 let url = Url::parse(s).with_context(|| format!("Invalid URL: {s}"))?;
57
58 let segments: Vec<&str> = url
59 .path_segments()
60 .context("URL has no path")?
61 .filter(|s| !s.is_empty())
62 .collect();
63
64 if segments.len() < 4 || segments[2] != "issues" {
65 bail!("Expected GitHub issue URL like https://github.com/owner/repo/issues/42, got: {s}");
66 }
67
68 let owner = segments[0].to_string();
69 let repo = segments[1].to_string();
70 let number = segments[3]
71 .parse::<u64>()
72 .with_context(|| format!("Invalid issue number in URL: {}", segments[3]))?;
73
74 Ok(IssueRef::GitHub {
75 owner,
76 repo,
77 number,
78 })
79}