1use crate::async_runtime;
2use crate::config::{Config, EnvConfig};
3use crate::error::{Error, ErrorKind, Result};
4use crate::git::Git;
5use crate::github_api::Client;
6use getopts::Options;
7use std::env;
8use std::ffi::OsStr;
9use std::fs;
10use std::path::PathBuf;
11
12fn handle_scp_like_syntax(mut url: String) -> String {
13 if url.contains("://") {
16 return url;
18 }
19
20 if let Some(i) = url.find(':') {
23 let after_colon = i + 1;
24 if let Some(i) = url[after_colon..].find(':') {
25 let i = after_colon + i;
28 url.replace_range(i..i + 1, "/");
29 } else {
30 url.insert_str(after_colon, "22/"); }
34 }
35
36 url.insert_str(0, "ssh://");
39 url
40}
41
42#[cfg_attr(feature = "cargo-clippy", allow(clippy::large_enum_variant))]
43#[derive(Debug)]
44pub enum Parsed {
45 Help(String),
46 Version(&'static str),
47 OpenPage(Config),
48}
49
50fn is_scp_like_syntax_with_user(s: &str) -> bool {
51 if s.contains(char::is_whitespace) {
57 return false; }
59 for (i, c) in s.char_indices() {
60 match c {
61 '@' if i == 0 => return false, '@' => return s[i + 1..].contains(':'), ':' => return false, _ => continue,
65 }
66 }
67 false
68}
69
70fn normalize_repo_format(mut slug: String, env: &EnvConfig) -> Result<String> {
71 if slug.is_empty() {
72 return Error::err(ErrorKind::BrokenRepoFormat { input: slug });
73 }
74
75 if slug.starts_with("https://")
80 || slug.starts_with("ssh://")
81 || slug.starts_with("http://")
82 || slug.starts_with("file://")
83 || is_scp_like_syntax_with_user(&slug)
84 {
85 if !slug.ends_with(".git") {
86 slug.push_str(".git");
87 }
88 return Ok(slug);
89 }
90
91 match slug.chars().filter(|c| *c == '/').count() {
92 1 => Ok(format!("https://github.com/{}.git", slug)),
93 2 => Ok(format!("https://{}.git", slug)),
94 0 => {
95 let client = Client::build("api.github.com", &env.github_token, &env.https_proxy)?;
96 async_runtime::blocking(client.most_popular_repo_by_name(&slug))
97 .map(|repo| repo.clone_url)
98 }
99 _ => Error::err(ErrorKind::BrokenRepoFormat { input: slug }),
100 }
101}
102
103fn get_cwd(specified: Option<String>) -> Result<PathBuf> {
104 if let Some(dir) = specified {
105 let p = fs::canonicalize(&dir)?;
106 if !p.exists() {
107 return Error::err(ErrorKind::SpecifiedDirNotExist { dir });
108 }
109 Ok(p)
110 } else {
111 Ok(env::current_dir()?.canonicalize()?)
112 }
113}
114
115const USAGE: &str = "\
116Usage: git brws [Options] {Args}
117
118 Open a repository, file, commit, diff or pull request, issue or project's
119 website in your web browser from command line.
120 GitHub, Bitbucket, GitLab, GitHub Enterprise, Azure DevOps are supported as
121 hosting service.
122 git-brws looks some environment variables for configuration. Please see
123 https://github.com/rhysd/git-brws#readme for more details.
124
125Examples:
126 - Current repository:
127
128 $ git brws
129
130 - GitHub repository:
131
132 $ git brws -r rhysd/git-brws
133
134 - Most popular GitHub repository by name:
135
136 $ git brws -r git-brws
137
138 - File:
139
140 $ git brws some/file.txt
141
142 - Commit:
143
144 $ git brws HEAD~3
145
146 - Tag:
147
148 $ git brws 0.10.0
149
150 - Diff between commits:
151
152 $ git brws HEAD~3..HEAD
153
154 - Diff between topic and topic's merge base commit:
155
156 $ git brws master...topic
157
158 - Line 123 of file:
159
160 $ git brws some/file.txt#L123
161
162 - Range from line 123 to line 126 of file:
163
164 $ git brws some/file.txt#L123-L126
165
166 - Pull request page (for GitHub and GitHub Enterprise):
167
168 $ git brws --pr
169
170 - Website of repository at current directory
171
172 $ git brws --website
173
174 - Website of other repository
175
176 $ git brws --website --repo react
177
178 - Issue page:
179
180 $ git brws '#8'";
181
182impl Parsed {
183 pub fn parse_iter<I>(argv: I) -> Result<Parsed>
184 where
185 I: IntoIterator,
186 I::Item: AsRef<OsStr>,
187 {
188 let mut opts = Options::new();
189
190 opts.optopt("r", "repo", "Shorthand format (repo, user/repo, host/user/repo) or Git URL you want to see. When only repo name is specified, most popular repository will be searched from GitHub", "REPO");
191 opts.optopt("b", "branch", "Branch name to browse", "BRANCH");
192 opts.optopt(
193 "d",
194 "dir",
195 "Directory path to the repository. Default value is current working directory",
196 "PATH",
197 );
198 opts.optopt("R", "remote", "Remote name (e.g. origin). Default value is a remote the current branch is tracking. If current branch tracks no branch, it falls back to 'origin'", "REMOTE");
199 opts.optflag(
200 "u",
201 "url",
202 "Output URL to stdout instead of opening in browser",
203 );
204 opts.optflag(
205 "p",
206 "pr",
207 "Open pull request page instead of repository page. If not existing, open 'Create Pull Request' page",
208 );
209 opts.optflag(
210 "w",
211 "website",
212 "Open website page instead of repository page (homepage URL for GitHub, GitLab pages, Bitbucket Cloud)",
213 );
214 opts.optflag(
215 "B",
216 "blame",
217 "Open blame page instead of repository page. File path to blame must be passed also.",
218 );
219 opts.optflag(
220 "c",
221 "current-branch",
222 "Open the current branch instead of default branch",
223 );
224 opts.optflag("h", "help", "Print this help");
225 opts.optflag("v", "version", "Show version");
226
227 let matches = opts.parse(argv.into_iter().skip(1))?;
228
229 if matches.opt_present("h") {
230 return Ok(Parsed::Help(opts.usage(USAGE)));
231 }
232
233 if matches.opt_present("v") {
234 return Ok(Parsed::Version(
235 option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
236 ));
237 }
238
239 let env = EnvConfig::from_iter(env::vars())?.with_global_env();
240 let cwd = get_cwd(matches.opt_str("d"))?;
241 let git = Git::new(&cwd, &env.git_command);
242 let branch = if let Some(b) = matches.opt_str("b") {
243 if b.is_empty() {
244 return Error::err(ErrorKind::BranchNameEmpty);
245 }
246 Some(b)
247 } else if matches.opt_present("c") {
248 Some(git.current_branch()?)
249 } else {
250 None
251 };
252 let (repo_url, remote) = match (matches.opt_str("r"), matches.opt_str("R")) {
253 (Some(repo), remote) => {
254 if !matches.free.is_empty() {
255 return Error::err(ErrorKind::ArgsNotAllowed {
256 flag: "--repo {repo}",
257 args: matches.free,
258 });
259 }
260 (normalize_repo_format(repo, &env)?, remote)
261 }
262 (None, remote) => {
263 let (url, remote) = if let Some(remote) = remote {
264 (git.remote_url(&remote)?, remote)
265 } else {
266 git.tracking_remote_url(&branch)?
267 };
268 (url, Some(remote))
269 }
270 };
271
272 let repo_url = handle_scp_like_syntax(repo_url);
273
274 Ok(Parsed::OpenPage(Config {
275 repo_url,
276 branch,
277 cwd,
278 stdout: matches.opt_present("u"),
279 pull_request: matches.opt_present("p"),
280 website: matches.opt_present("w"),
281 blame: matches.opt_present("B"),
282 args: matches.free,
283 remote,
284 env,
285 }))
286 }
287}