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