worktree_io/issue/
parse.rs1use anyhow::{bail, Context, Result};
2use url::Url;
3
4use super::{DeepLinkOptions, IssueRef};
5
6impl IssueRef {
7 pub fn parse(s: &str) -> Result<Self> {
15 let s = s.trim();
16
17 if s.starts_with("worktree://") {
19 let (issue, _opts) = Self::parse_worktree_url(s)?;
20 return Ok(issue);
21 }
22
23 if s.starts_with("https://github.com") || s.starts_with("http://github.com") {
25 return Self::parse_github_url(s);
26 }
27
28 if let Some(result) = Self::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 Self::parse_worktree_url(s);
50 }
51 Ok((Self::parse(s)?, DeepLinkOptions::default()))
52 }
53
54 fn parse_worktree_url(s: &str) -> Result<(Self, DeepLinkOptions)> {
55 let url = Url::parse(s).with_context(|| format!("Invalid URL: {s}"))?;
56 let mut owner = None;
57 let mut repo = None;
58 let mut issue_num = None;
59 let mut linear_id = None;
60 let mut url_param = None;
61 let mut editor = None;
62
63 for (key, val) in url.query_pairs() {
64 match key.as_ref() {
65 "owner" => owner = Some(val.into_owned()),
66 "repo" => repo = Some(val.into_owned()),
67 "issue" => {
68 issue_num = Some(
69 val.parse::<u64>()
70 .with_context(|| format!("Invalid issue number: {val}"))?,
71 );
72 }
73 "linear_id" => {
74 let id = val.into_owned();
75 if uuid::Uuid::parse_str(&id).is_err() {
76 bail!("Invalid Linear issue UUID: {id}");
77 }
78 linear_id = Some(id);
79 }
80 "url" => {
81 url_param = Some(val.into_owned());
83 }
84 "editor" => editor = Some(val.into_owned()),
85 _ => {}
86 }
87 }
88
89 let opts = DeepLinkOptions { editor };
90
91 if let Some(url_str) = url_param {
92 return Ok((Self::parse_github_url(&url_str)?, opts));
93 }
94
95 if let Some(id) = linear_id {
96 return Ok((
97 Self::Linear {
98 owner: owner.context("Missing 'owner' query param")?,
99 repo: repo.context("Missing 'repo' query param")?,
100 id,
101 },
102 opts,
103 ));
104 }
105
106 Ok((
107 Self::GitHub {
108 owner: owner.context("Missing 'owner' query param")?,
109 repo: repo.context("Missing 'repo' query param")?,
110 number: issue_num.context("Missing 'issue' query param")?,
111 },
112 opts,
113 ))
114 }
115
116 fn parse_github_url(s: &str) -> Result<Self> {
117 let url = Url::parse(s).with_context(|| format!("Invalid URL: {s}"))?;
118
119 let segments: Vec<&str> = url
120 .path_segments()
121 .context("URL has no path")?
122 .filter(|s| !s.is_empty())
123 .collect();
124
125 if segments.len() < 4 || segments[2] != "issues" {
127 bail!(
128 "Expected GitHub issue URL like https://github.com/owner/repo/issues/42, got: {s}"
129 );
130 }
131
132 let owner = segments[0].to_string();
133 let repo = segments[1].to_string();
134 let number = segments[3]
135 .parse::<u64>()
136 .with_context(|| format!("Invalid issue number in URL: {}", segments[3]))?;
137
138 Ok(Self::GitHub { owner, repo, number })
139 }
140
141 fn try_parse_shorthand(s: &str) -> Option<Result<Self>> {
142 if let Some((repo_part, id)) = s.split_once('@') {
144 let (owner, repo) = repo_part.split_once('/')?;
145 if owner.is_empty() || repo.is_empty() {
146 return Some(Err(anyhow::anyhow!("Invalid shorthand format: {s}")));
147 }
148 if uuid::Uuid::parse_str(id).is_err() {
149 return Some(Err(anyhow::anyhow!(
150 "Invalid Linear issue UUID in shorthand: {id}"
151 )));
152 }
153 return Some(Ok(Self::Linear {
154 owner: owner.to_string(),
155 repo: repo.to_string(),
156 id: id.to_string(),
157 }));
158 }
159
160 let (repo_part, num_str) = s.split_once('#')?;
161 let (owner, repo) = repo_part.split_once('/')?;
162
163 if owner.is_empty() || repo.is_empty() {
164 return Some(Err(anyhow::anyhow!("Invalid shorthand format: {s}")));
165 }
166
167 let number = match num_str.parse::<u64>() {
168 Ok(n) => n,
169 Err(_) => return Some(Err(anyhow::anyhow!("Invalid issue number in shorthand: {num_str}"))),
170 };
171
172 Some(Ok(Self::GitHub {
173 owner: owner.to_string(),
174 repo: repo.to_string(),
175 number,
176 }))
177 }
178}