Cargo.lock 🔗
@@ -4317,6 +4317,7 @@ dependencies = [
"log",
"parking_lot",
"pretty_assertions",
+ "regex",
"rope",
"serde",
"serde_json",
Thorsten Ball created
Release Notes:
- Added links to GitHub pull requests to the git blame tooltips, if they
are available.
Screenshot:
(Yes, the icon will be resized! cc @iamnbutler)

Cargo.lock | 1
assets/icons/pull_request.svg | 1
crates/editor/src/blame_entry_tooltip.rs | 69 +++++++++++++++------
crates/editor/src/git/blame.rs | 7 ++
crates/git/Cargo.toml | 1
crates/git/src/git.rs | 1
crates/git/src/pull_request.rs | 83 ++++++++++++++++++++++++++
crates/ui/src/components/icon.rs | 2
8 files changed, 145 insertions(+), 20 deletions(-)
@@ -4317,6 +4317,7 @@ dependencies = [
"log",
"parking_lot",
"pretty_assertions",
+ "regex",
"rope",
"serde",
"serde_json",
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-pull-request-arrow"><circle cx="5" cy="6" r="3"/><path d="M5 9v12"/><circle cx="19" cy="18" r="3"/><path d="m15 9-3-3 3-3"/><path d="M12 6h5a2 2 0 0 1 2 2v7"/></svg>
@@ -149,6 +149,11 @@ impl Render for BlameEntryTooltip {
})
.unwrap_or("<no commit message>".into_any());
+ let pull_request = self
+ .details
+ .as_ref()
+ .and_then(|details| details.pull_request.clone());
+
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
let message_max_height = cx.line_height() * 12 + (ui_font_size / 0.4);
@@ -192,27 +197,51 @@ impl Render for BlameEntryTooltip {
.justify_between()
.child(absolute_timestamp)
.child(
- Button::new("commit-sha-button", short_commit_id.clone())
- .style(ButtonStyle::Transparent)
- .color(Color::Muted)
- .icon(IconName::FileGit)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .disabled(
- self.details.as_ref().map_or(true, |details| {
- details.permalink.is_none()
- }),
- )
- .when_some(
- self.details
- .as_ref()
- .and_then(|details| details.permalink.clone()),
- |this, url| {
- this.on_click(move |_, cx| {
+ h_flex()
+ .gap_2()
+ .when_some(pull_request, |this, pr| {
+ this.child(
+ Button::new(
+ "pull-request-button",
+ format!("#{}", pr.number),
+ )
+ .color(Color::Muted)
+ .icon(IconName::PullRequest)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .style(ButtonStyle::Transparent)
+ .on_click(move |_, cx| {
cx.stop_propagation();
- cx.open_url(url.as_str())
- })
- },
+ cx.open_url(pr.url.as_str())
+ }),
+ )
+ })
+ .child(
+ Button::new(
+ "commit-sha-button",
+ short_commit_id.clone(),
+ )
+ .style(ButtonStyle::Transparent)
+ .color(Color::Muted)
+ .icon(IconName::FileGit)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .disabled(
+ self.details.as_ref().map_or(true, |details| {
+ details.permalink.is_none()
+ }),
+ )
+ .when_some(
+ self.details
+ .as_ref()
+ .and_then(|details| details.permalink.clone()),
+ |this, url| {
+ this.on_click(move |_, cx| {
+ cx.stop_propagation();
+ cx.open_url(url.as_str())
+ })
+ },
+ ),
),
),
),
@@ -6,6 +6,7 @@ use git::{
blame::{Blame, BlameEntry},
hosting_provider::HostingProvider,
permalink::{build_commit_permalink, parse_git_remote_url},
+ pull_request::{extract_pull_request, PullRequest},
Oid,
};
use gpui::{Model, ModelContext, Subscription, Task};
@@ -75,6 +76,7 @@ pub struct CommitDetails {
pub message: String,
pub parsed_message: ParsedMarkdown,
pub permalink: Option<Url>,
+ pub pull_request: Option<PullRequest>,
pub remote: Option<GitRemote>,
}
@@ -438,6 +440,10 @@ async fn parse_commit_messages(
repo: remote.repo.to_string(),
});
+ let pull_request = parsed_remote_url
+ .as_ref()
+ .and_then(|remote| extract_pull_request(remote, &message));
+
commit_details.insert(
oid,
CommitDetails {
@@ -445,6 +451,7 @@ async fn parse_commit_messages(
parsed_message,
permalink,
remote,
+ pull_request,
},
);
}
@@ -25,6 +25,7 @@ time.workspace = true
url.workspace = true
util.workspace = true
serde.workspace = true
+regex.workspace = true
rope.workspace = true
parking_lot.workspace = true
windows.workspace = true
@@ -12,6 +12,7 @@ pub mod commit;
pub mod diff;
pub mod hosting_provider;
pub mod permalink;
+pub mod pull_request;
pub mod repository;
lazy_static! {
@@ -0,0 +1,83 @@
+use lazy_static::lazy_static;
+use url::Url;
+
+use crate::{hosting_provider::HostingProvider, permalink::ParsedGitRemote};
+
+lazy_static! {
+ static ref GITHUB_PULL_REQUEST_NUMBER: regex::Regex =
+ regex::Regex::new(r"\(#(\d+)\)$").unwrap();
+}
+
+#[derive(Clone, Debug)]
+pub struct PullRequest {
+ pub number: u32,
+ pub url: Url,
+}
+
+pub fn extract_pull_request(remote: &ParsedGitRemote, message: &str) -> Option<PullRequest> {
+ match remote.provider {
+ HostingProvider::Github => {
+ let line = message.lines().next()?;
+ let capture = GITHUB_PULL_REQUEST_NUMBER.captures(line)?;
+ let number = capture.get(1)?.as_str().parse::<u32>().ok()?;
+
+ let mut url = remote.provider.base_url();
+ let path = format!("/{}/{}/pull/{}", remote.owner, remote.repo, number);
+ url.set_path(&path);
+
+ Some(PullRequest { number, url })
+ }
+ _ => None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use unindent::Unindent;
+
+ use crate::{
+ hosting_provider::HostingProvider, permalink::ParsedGitRemote,
+ pull_request::extract_pull_request,
+ };
+
+ #[test]
+ fn test_github_pull_requests() {
+ let remote = ParsedGitRemote {
+ provider: HostingProvider::Github,
+ owner: "zed-industries",
+ repo: "zed",
+ };
+
+ let message = "This does not contain a pull request";
+ assert!(extract_pull_request(&remote, message).is_none());
+
+ // Pull request number at end of first line
+ let message = r#"
+ project panel: do not expand collapsed worktrees on "collapse all entries" (#10687)
+
+ Fixes #10597
+
+ Release Notes:
+
+ - Fixed "project panel: collapse all entries" expanding collapsed worktrees.
+ "#
+ .unindent();
+
+ assert_eq!(
+ extract_pull_request(&remote, &message)
+ .unwrap()
+ .url
+ .as_str(),
+ "https://github.com/zed-industries/zed/pull/10687"
+ );
+
+ // Pull request number in middle of line, which we want to ignore
+ let message = r#"
+ Follow-up to #10687 to fix problems
+
+ See the original PR, this is a fix.
+ "#
+ .unindent();
+ assert!(extract_pull_request(&remote, &message).is_none());
+ }
+}
@@ -121,6 +121,7 @@ pub enum IconName {
WholeWord,
XCircle,
ZedXCopilot,
+ PullRequest,
}
impl IconName {
@@ -222,6 +223,7 @@ impl IconName {
IconName::WholeWord => "icons/word_search.svg",
IconName::XCircle => "icons/error.svg",
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
+ IconName::PullRequest => "icons/pull_request.svg",
}
}
}