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    ///
18    /// # Errors
19    ///
20    /// Returns an error if `s` does not match any supported format or if the
21    /// extracted values (e.g. issue number) are invalid.
22    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    /// Like [`parse`] but also returns any [`DeepLinkOptions`] embedded in a
57    /// `worktree://` URL (e.g. the `editor` query param).
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if `s` cannot be parsed as a valid issue reference.
62    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
71/// Parse an Azure DevOps work item URL.
72///
73/// Expected format: `https://dev.azure.com/{org}/{project}/_workitems/edit/{id}`
74///
75/// Since the URL does not include the git repository name, the project name is
76/// used as the repository name by default.
77pub(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    // Expected: [org, project, "_workitems", "edit", id]
87    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}