Skip to content

Commit 1eb0cbd

Browse files
committed
feat: Support opening Git hash or GitHub PRs
1 parent 039003d commit 1eb0cbd

File tree

4 files changed

+101
-25
lines changed

4 files changed

+101
-25
lines changed

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "magic-opener"
3-
version = "0.2.2"
3+
version = "0.3.0"
44
authors = ["Dan Sully"]
55
edition = "2024"
66
description = "An 'open' replacement that tries to do the right thing."

src/main.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)]
1414

1515
use std::env;
16-
use std::io::{Write, stdout};
16+
use std::io::{stdout, Write};
1717
use std::net::TcpStream;
1818
use std::os::unix::process::CommandExt;
1919
use std::process::{self, Command, Stdio};
@@ -23,7 +23,7 @@ use clap::{Arg, ArgAction, Command as ClapCommand};
2323
mod parser;
2424
mod repo;
2525

26-
use repo::{GitRepository, RepositoryError, is_git_repo};
26+
use repo::{GitRepository, RepositoryError};
2727

2828
const LOCALHOST: &str = "localhost";
2929
const OPEN: &str = "/usr/bin/open";
@@ -74,22 +74,15 @@ fn main() {
7474
.to_string_lossy()
7575
.to_string();
7676

77-
let remote_path = if paths.is_empty() && is_git_repo(&current_dir) {
78-
match GitRepository::from_path(&current_dir) {
79-
Ok(r) => r.http_url(),
80-
Err(RepositoryError::NoSuchRemote(_)) => {
81-
println!("Found a Git repository, but no remote URL is set.");
82-
current_dir.clone()
83-
}
84-
Err(e) => {
85-
println!("Unknown error while trying to get remote URL: {e}");
86-
return;
87-
}
77+
let remote_path = match GitRepository::url(&current_dir, &paths) {
78+
Ok(url) => url,
79+
Err(RepositoryError::NoSuchRemote(_)) => {
80+
println!("Found a Git repository, but no remote URL is set.");
81+
current_dir.clone()
8882
}
89-
} else {
90-
match paths.join(" ") {
91-
path if path == "." => current_dir.clone(),
92-
path => path,
83+
Err(e) => {
84+
println!("Unknown error while trying to get remote URL: {e}");
85+
return;
9386
}
9487
};
9588

src/repo.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,44 @@ impl GitRepository {
9797
format!("git@{}:{}/{}.git", self.host, self.org, self.name)
9898
}
9999

100+
/// Returns the URL for viewing a specific commit
101+
pub fn commit_url(&self, hash: &str) -> String {
102+
format!(
103+
"https://{}/{}/{}/commit/{}",
104+
self.host, self.org, self.name, hash
105+
)
106+
}
107+
108+
/// Returns the URL for viewing a pull request
109+
pub fn pr_url(&self, pr_number: &str) -> String {
110+
format!(
111+
"https://{}/{}/{}/pull/{}",
112+
self.host, self.org, self.name, pr_number
113+
)
114+
}
115+
116+
/// Try to find a PR number from a commit message
117+
pub fn pr_for_commit(&self, hash: &str) -> Option<String> {
118+
if let Some(path) = &self.path {
119+
//
120+
if let Ok(message) = git(path, &["log", "-1", "--pretty=%B", hash]) {
121+
//
122+
if let Some(captures) = message.find('#') {
123+
let after_hash = &message[captures + 1..];
124+
let pr_number: String = after_hash
125+
.chars()
126+
.take_while(char::is_ascii_digit)
127+
.collect();
128+
129+
if !pr_number.is_empty() {
130+
return Some(pr_number);
131+
}
132+
}
133+
}
134+
}
135+
None
136+
}
137+
100138
pub fn current_branch(&self) -> String {
101139
self.path
102140
.as_ref()
@@ -114,6 +152,43 @@ impl GitRepository {
114152
Err(e) => Err(e),
115153
}
116154
}
155+
156+
pub fn url(current_dir: &str, paths: &[String]) -> Result<String, RepositoryError> {
157+
let is_git = is_git_repo(current_dir);
158+
159+
let join_paths = || match paths.join(" ") {
160+
path if path == "." => current_dir.to_string(),
161+
path => path,
162+
};
163+
164+
if !is_git {
165+
return Ok(join_paths());
166+
}
167+
168+
let r = Self::from_path(current_dir)?;
169+
170+
if paths.is_empty() {
171+
return Ok(r.http_url());
172+
}
173+
174+
if paths.len() == 1 {
175+
let arg = &paths[0];
176+
let is_commit = is_valid_commit_hash(arg);
177+
178+
if is_commit || is_pr_number(arg) {
179+
return if is_commit {
180+
match r.pr_for_commit(arg) {
181+
Some(pr_number) => Ok(r.pr_url(&pr_number)),
182+
None => Ok(r.commit_url(arg)),
183+
}
184+
} else {
185+
Ok(r.pr_url(arg))
186+
};
187+
}
188+
}
189+
190+
Ok(join_paths())
191+
}
117192
}
118193

119194
impl fmt::Debug for GitRepository {
@@ -146,6 +221,14 @@ pub fn is_git_repo(path: impl AsRef<Path>) -> bool {
146221
git(&path, &["rev-parse", "--git-dir"]).is_ok()
147222
}
148223

224+
pub fn is_valid_commit_hash(hash: &str) -> bool {
225+
hash.len() >= 7 && hash.len() <= 40 && hash.chars().all(|c| c.is_ascii_hexdigit())
226+
}
227+
228+
pub fn is_pr_number(s: &str) -> bool {
229+
!s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
230+
}
231+
149232
#[cfg(test)]
150233
mod tests {
151234
use super::*;

0 commit comments

Comments
 (0)