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 s.starts_with("https://dev.azure.com") || s.starts_with("http://dev.azure.com") {
35 return parse_azure_devops_url(s);
36 }
37
38 if let Some(result) = shorthand::try_parse_shorthand(s) {
39 return result;
40 }
41
42 bail!(
43 "Could not parse issue reference: {s:?}\n\
44 Supported formats:\n\
45 - https://github.com/owner/repo/issues/42\n\
46 - https://dev.azure.com/org/project/_workitems/edit/42\n\
47 - worktree://open?owner=owner&repo=repo&issue=42\n\
48 - worktree://open?owner=owner&repo=repo&linear_id=<uuid>\n\
49 - worktree://open?org=org&project=project&repo=repo&work_item_id=42\n\
50 - owner/repo#42\n\
51 - owner/repo@<linear-uuid>\n\
52 - org/project/repo!42"
53 )
54 }
55
56 pub fn parse_with_options(s: &str) -> Result<(Self, DeepLinkOptions)> {
63 let s = s.trim();
64 if s.starts_with("worktree://") {
65 return worktree_url::parse_worktree_url(s);
66 }
67 Ok((Self::parse(s)?, DeepLinkOptions::default()))
68 }
69}
70
71pub(super) fn parse_azure_devops_url(s: &str) -> Result<IssueRef> {
78 let url = Url::parse(s).with_context(|| format!("Invalid URL: {s}"))?;
79
80 let segments: Vec<&str> = url
81 .path_segments()
82 .context("URL has no path")?
83 .filter(|s| !s.is_empty())
84 .collect();
85
86 if segments.len() < 5 || segments[2] != "_workitems" || segments[3] != "edit" {
88 bail!(
89 "Expected Azure DevOps work item URL like \
90 https://dev.azure.com/org/project/_workitems/edit/42, got: {s}"
91 );
92 }
93
94 let org = segments[0].to_string();
95 let project = segments[1].to_string();
96 let id = segments[4]
97 .parse::<u64>()
98 .with_context(|| format!("Invalid work item ID in URL: {}", segments[4]))?;
99
100 Ok(IssueRef::AzureDevOps {
101 repo: project.clone(),
102 org,
103 project,
104 id,
105 })
106}
107
108pub(super) fn parse_github_url(s: &str) -> Result<IssueRef> {
109 let url = Url::parse(s).with_context(|| format!("Invalid URL: {s}"))?;
110
111 let segments: Vec<&str> = url
112 .path_segments()
113 .context("URL has no path")?
114 .filter(|s| !s.is_empty())
115 .collect();
116
117 if segments.len() < 4 || segments[2] != "issues" {
118 bail!("Expected GitHub issue URL like https://github.com/owner/repo/issues/42, got: {s}");
119 }
120
121 let owner = segments[0].to_string();
122 let repo = segments[1].to_string();
123 let number = segments[3]
124 .parse::<u64>()
125 .with_context(|| format!("Invalid issue number in URL: {}", segments[3]))?;
126
127 Ok(IssueRef::GitHub {
128 owner,
129 repo,
130 number,
131 })
132}