wtg_cli/
cli.rs

1use clap::Parser;
2
3use crate::constants;
4
5#[derive(Parser, Debug)]
6#[command(
7    name = "wtg",
8    version,
9    about = constants::DESCRIPTION,
10    disable_help_flag = true,
11)]
12pub struct Cli {
13    /// The thing to identify: commit hash (c62bbcc), issue/PR (#123), file path (Cargo.toml), tag (v1.2.3), or a GitHub URL
14    #[arg(value_name = "COMMIT|ISSUE|FILE|TAG|URL")]
15    pub input: Option<String>,
16
17    /// GitHub repository URL to operate on (e.g., <https://github.com/owner/repo>)
18    #[arg(short = 'r', long, value_name = "URL")]
19    pub repo: Option<String>,
20
21    /// Print help information
22    #[arg(short, long, action = clap::ArgAction::Help)]
23    help: Option<bool>,
24}
25
26/// Parsed input that can come from either the input argument or a GitHub URL
27#[derive(Debug, Clone)]
28pub struct ParsedInput {
29    pub owner: Option<String>,
30    pub repo: Option<String>,
31    pub query: String,
32}
33
34impl Cli {
35    /// Parse the input and -r flag to determine the repository and query
36    #[must_use]
37    pub fn parse_input(&self) -> Option<ParsedInput> {
38        let input = self.input.as_ref()?;
39
40        // If -r flag is provided, use it as the repo and input as the query
41        if let Some(repo_url) = &self.repo {
42            let (owner, repo) = parse_github_repo_url(repo_url)?;
43            return Some(ParsedInput {
44                owner: Some(owner),
45                repo: Some(repo),
46                query: input.clone(),
47            });
48        }
49
50        // Try to parse input as a GitHub URL
51        if let Some(parsed) = parse_github_url(input) {
52            return Some(parsed);
53        }
54
55        // Otherwise, it's just a query (local repo)
56        Some(ParsedInput {
57            owner: None,
58            repo: None,
59            query: input.clone(),
60        })
61    }
62}
63
64/// Parse a GitHub URL to extract owner, repo, and optional query
65/// Supports:
66/// - <https://github.com/owner/repo>
67/// - <https://github.com/owner/repo/commit/hash>
68/// - <https://github.com/owner/repo/issues/123>
69/// - <https://github.com/owner/repo/pull/123>
70/// - <https://github.com/owner/repo/blob/branch/path/to/file>
71fn parse_github_url(url: &str) -> Option<ParsedInput> {
72    if !url.contains("github.com") {
73        return None;
74    }
75
76    // Extract the path after github.com
77    let path = if url.starts_with("git@") {
78        // SSH format: git@github.com:owner/repo.git
79        url.split(':').nth(1)?
80    } else {
81        // HTTPS format: https://github.com/owner/repo/...
82        url.split("github.com/").nth(1)?
83    };
84
85    let path = path.trim_end_matches(".git");
86    let parts: Vec<&str> = path.split('/').collect();
87
88    if parts.len() < 2 {
89        return None;
90    }
91
92    let owner = parts[0].to_string();
93    let repo = parts[1].to_string();
94
95    // Determine what the query should be based on the URL structure
96    let query = if parts.len() == 2 {
97        // Just the repo URL, no specific query
98        return None;
99    } else if parts.len() >= 4 {
100        match parts[2] {
101            "commit" => parts[3].to_string(),
102            "issues" | "pull" => format!("#{}", parts[3]),
103            "blob" | "tree" => {
104                // Format: /blob/branch/path/to/file or /tree/branch/path/to/file
105                // Extract the file path after branch name
106                if parts.len() >= 5 {
107                    parts[4..].join("/")
108                } else {
109                    return None;
110                }
111            }
112            _ => return None,
113        }
114    } else {
115        return None;
116    };
117
118    Some(ParsedInput {
119        owner: Some(owner),
120        repo: Some(repo),
121        query,
122    })
123}
124
125/// Parse a simple GitHub repo URL (owner/repo or <https://github.com/owner/repo>)
126fn parse_github_repo_url(url: &str) -> Option<(String, String)> {
127    // Handle SSH format
128    if url.starts_with("git@") {
129        let path = url.split(':').nth(1)?;
130        let path = path.trim_end_matches(".git");
131        let parts: Vec<&str> = path.split('/').collect();
132        if parts.len() >= 2 {
133            return Some((parts[0].to_string(), parts[1].to_string()));
134        }
135        return None;
136    }
137
138    // Handle HTTPS format
139    if url.contains("github.com/") {
140        let path = url.split("github.com/").nth(1)?;
141        let path = path.trim_end_matches(".git");
142        let parts: Vec<&str> = path.split('/').collect();
143        if parts.len() >= 2 {
144            return Some((parts[0].to_string(), parts[1].to_string()));
145        }
146        return None;
147    }
148
149    // Handle simple owner/repo format
150    let parts: Vec<&str> = url.split('/').collect();
151    if parts.len() == 2 {
152        return Some((parts[0].to_string(), parts[1].to_string()));
153    }
154
155    None
156}