Skip to main content

worktree_io/issue/parse/
mod.rs

1mod shorthand;
2mod worktree_url;
3
4use anyhow::{bail, Context, Result};
5use url::Url;
6
7use super::{DeepLinkOptions, IssueRef};
8
9impl IssueRef {
10    /// Parse any of the supported input formats:
11    /// - `https://github.com/owner/repo/issues/42`
12    /// - `worktree://open?owner=X&repo=Y&issue=42`
13    /// - `worktree://open?url=<encoded-github-url>`
14    /// - `worktree://open?owner=X&repo=Y&linear_id=<uuid>`
15    /// - `owner/repo#42`
16    /// - `owner/repo@<linear-uuid>`
17    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    /// Like [`parse`] but also returns any [`DeepLinkOptions`] embedded in a
45    /// `worktree://` URL (e.g. the `editor` query param).
46    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}