git_panel: Improve toast messages for push/pull/fetch (#35092)

On GitLab, when pushing a branch and a MR already existing the remote
log contains "View merge request" and the link to the MR.

Fixed `Already up to date` stdout check on pull (was `Everything up to
date` on stderr)
Fixed `Everything up-to-date` check on push (was `Everything up to
date`)
Improved messaging for up-to-date for fetch/push/pull
Fixed tests introduced in
https://github.com/zed-industries/zed/pull/33833.

<img width="470" height="111" alt="Screenshot 2025-07-31 at 18 37 05"
src="https://github.com/user-attachments/assets/2a5dcc4c-6f53-4a85-b983-8e25149efcc0"
/>

Release Notes:

- Git UI: Add "View Pull Request" when pushing to Gitlab remotes
- git: Improved toast messages on fetch/push/pull

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
This commit is contained in:
Guillaume Launay 2025-08-05 00:20:20 +02:00 committed by GitHub
parent 24e7f868ad
commit 182edbf526
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 95 additions and 65 deletions

1
Cargo.lock generated
View file

@ -6369,6 +6369,7 @@ dependencies = [
"fuzzy", "fuzzy",
"git", "git",
"gpui", "gpui",
"indoc",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
"language_model", "language_model",

View file

@ -70,6 +70,7 @@ windows.workspace = true
ctor.workspace = true ctor.workspace = true
editor = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
pretty_assertions.workspace = true pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] }

View file

@ -2899,7 +2899,9 @@ impl GitPanel {
let status_toast = StatusToast::new(message, cx, move |this, _cx| { let status_toast = StatusToast::new(message, cx, move |this, _cx| {
use remote_output::SuccessStyle::*; use remote_output::SuccessStyle::*;
match style { match style {
Toast { .. } => this, Toast { .. } => {
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
}
ToastWithLog { output } => this ToastWithLog { output } => this
.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
.action("View Log", move |window, cx| { .action("View Log", move |window, cx| {
@ -2912,9 +2914,9 @@ impl GitPanel {
}) })
.ok(); .ok();
}), }),
PushPrLink { link } => this PushPrLink { text, link } => this
.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
.action("Open Pull Request", move |_, cx| cx.open_url(&link)), .action(text, move |_, cx| cx.open_url(&link)),
} }
}); });
workspace.toggle_status_toast(status_toast, cx) workspace.toggle_status_toast(status_toast, cx)

View file

@ -24,7 +24,7 @@ impl RemoteAction {
pub enum SuccessStyle { pub enum SuccessStyle {
Toast, Toast,
ToastWithLog { output: RemoteCommandOutput }, ToastWithLog { output: RemoteCommandOutput },
PushPrLink { link: String }, PushPrLink { text: String, link: String },
} }
pub struct SuccessMessage { pub struct SuccessMessage {
@ -37,7 +37,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
RemoteAction::Fetch(remote) => { RemoteAction::Fetch(remote) => {
if output.stderr.is_empty() { if output.stderr.is_empty() {
SuccessMessage { SuccessMessage {
message: "Already up to date".into(), message: "Fetch: Already up to date".into(),
style: SuccessStyle::Toast, style: SuccessStyle::Toast,
} }
} else { } else {
@ -68,10 +68,9 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
Ok(files_changed) Ok(files_changed)
}; };
if output.stdout.ends_with("Already up to date.\n") {
if output.stderr.starts_with("Everything up to date") {
SuccessMessage { SuccessMessage {
message: output.stderr.trim().to_owned(), message: "Pull: Already up to date".into(),
style: SuccessStyle::Toast, style: SuccessStyle::Toast,
} }
} else if output.stdout.starts_with("Updating") { } else if output.stdout.starts_with("Updating") {
@ -119,48 +118,42 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
} }
} }
RemoteAction::Push(branch_name, remote_ref) => { RemoteAction::Push(branch_name, remote_ref) => {
if output.stderr.contains("* [new branch]") { let message = if output.stderr.ends_with("Everything up-to-date\n") {
let pr_hints = [ "Push: Everything is up-to-date".to_string()
// GitHub
"Create a pull request",
// Bitbucket
"Create pull request",
// GitLab
"create a merge request",
];
let style = if pr_hints
.iter()
.any(|indicator| output.stderr.contains(indicator))
{
let finder = LinkFinder::new();
let first_link = finder
.links(&output.stderr)
.filter(|link| *link.kind() == LinkKind::Url)
.map(|link| link.start()..link.end())
.next();
if let Some(link) = first_link {
let link = output.stderr[link].to_string();
SuccessStyle::PushPrLink { link }
} else {
SuccessStyle::ToastWithLog { output }
}
} else {
SuccessStyle::ToastWithLog { output }
};
SuccessMessage {
message: format!("Published {} to {}", branch_name, remote_ref.name),
style,
}
} else if output.stderr.starts_with("Everything up to date") {
SuccessMessage {
message: output.stderr.trim().to_owned(),
style: SuccessStyle::Toast,
}
} else { } else {
SuccessMessage { format!("Pushed {} to {}", branch_name, remote_ref.name)
message: format!("Pushed {} to {}", branch_name, remote_ref.name), };
style: SuccessStyle::ToastWithLog { output },
} let style = if output.stderr.ends_with("Everything up-to-date\n") {
Some(SuccessStyle::Toast)
} else if output.stderr.contains("\nremote: ") {
let pr_hints = [
("Create a pull request", "Create Pull Request"), // GitHub
("Create pull request", "Create Pull Request"), // Bitbucket
("create a merge request", "Create Merge Request"), // GitLab
("View merge request", "View Merge Request"), // GitLab
];
pr_hints
.iter()
.find(|(indicator, _)| output.stderr.contains(indicator))
.and_then(|(_, mapped)| {
let finder = LinkFinder::new();
finder
.links(&output.stderr)
.filter(|link| *link.kind() == LinkKind::Url)
.map(|link| link.start()..link.end())
.next()
.map(|link| SuccessStyle::PushPrLink {
text: mapped.to_string(),
link: output.stderr[link].to_string(),
})
})
} else {
None
};
SuccessMessage {
message,
style: style.unwrap_or(SuccessStyle::ToastWithLog { output }),
} }
} }
} }
@ -169,6 +162,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use indoc::indoc;
#[test] #[test]
fn test_push_new_branch_pull_request() { fn test_push_new_branch_pull_request() {
@ -181,8 +175,7 @@ mod tests {
let output = RemoteCommandOutput { let output = RemoteCommandOutput {
stdout: String::new(), stdout: String::new(),
stderr: String::from( stderr: indoc! { "
"
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: remote:
remote: Create a pull request for 'test' on GitHub by visiting: remote: Create a pull request for 'test' on GitHub by visiting:
@ -190,13 +183,14 @@ mod tests {
remote: remote:
To example.com:test/test.git To example.com:test/test.git
* [new branch] test -> test * [new branch] test -> test
", "}
), .to_string(),
}; };
let msg = format_output(&action, output); let msg = format_output(&action, output);
if let SuccessStyle::PushPrLink { link } = &msg.style { if let SuccessStyle::PushPrLink { text: hint, link } = &msg.style {
assert_eq!(hint, "Create Pull Request");
assert_eq!(link, "https://example.com/test/test/pull/new/test"); assert_eq!(link, "https://example.com/test/test/pull/new/test");
} else { } else {
panic!("Expected PushPrLink variant"); panic!("Expected PushPrLink variant");
@ -214,7 +208,7 @@ mod tests {
let output = RemoteCommandOutput { let output = RemoteCommandOutput {
stdout: String::new(), stdout: String::new(),
stderr: String::from(" stderr: indoc! {"
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: remote:
remote: To create a merge request for test, visit: remote: To create a merge request for test, visit:
@ -222,12 +216,14 @@ mod tests {
remote: remote:
To example.com:test/test.git To example.com:test/test.git
* [new branch] test -> test * [new branch] test -> test
"), "}
}; .to_string()
};
let msg = format_output(&action, output); let msg = format_output(&action, output);
if let SuccessStyle::PushPrLink { link } = &msg.style { if let SuccessStyle::PushPrLink { text, link } = &msg.style {
assert_eq!(text, "Create Merge Request");
assert_eq!( assert_eq!(
link, link,
"https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test" "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test"
@ -237,6 +233,39 @@ mod tests {
} }
} }
#[test]
fn test_push_branch_existing_merge_request() {
let action = RemoteAction::Push(
SharedString::new("test_branch"),
Remote {
name: SharedString::new("test_remote"),
},
);
let output = RemoteCommandOutput {
stdout: String::new(),
stderr: indoc! {"
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote:
remote: View merge request for test:
remote: https://example.com/test/test/-/merge_requests/99999
remote:
To example.com:test/test.git
+ 80bd3c83be...e03d499d2e test -> test
"}
.to_string(),
};
let msg = format_output(&action, output);
if let SuccessStyle::PushPrLink { text, link } = &msg.style {
assert_eq!(text, "View Merge Request");
assert_eq!(link, "https://example.com/test/test/-/merge_requests/99999");
} else {
panic!("Expected PushPrLink variant");
}
}
#[test] #[test]
fn test_push_new_branch_no_link() { fn test_push_new_branch_no_link() {
let action = RemoteAction::Push( let action = RemoteAction::Push(
@ -248,12 +277,12 @@ mod tests {
let output = RemoteCommandOutput { let output = RemoteCommandOutput {
stdout: String::new(), stdout: String::new(),
stderr: String::from( stderr: indoc! { "
"
To http://example.com/test/test.git To http://example.com/test/test.git
* [new branch] test -> test * [new branch] test -> test
", ",
), }
.to_string(),
}; };
let msg = format_output(&action, output); let msg = format_output(&action, output);
@ -261,10 +290,7 @@ mod tests {
if let SuccessStyle::ToastWithLog { output } = &msg.style { if let SuccessStyle::ToastWithLog { output } = &msg.style {
assert_eq!( assert_eq!(
output.stderr, output.stderr,
" "To http://example.com/test/test.git\n * [new branch] test -> test\n"
To http://example.com/test/test.git
* [new branch] test -> test
"
); );
} else { } else {
panic!("Expected ToastWithLog variant"); panic!("Expected ToastWithLog variant");