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

Guillaume Launay and Peter Tripp created

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>

Change summary

Cargo.lock                         |   1 
crates/git_ui/Cargo.toml           |   1 
crates/git_ui/src/git_panel.rs     |   8 +
crates/git_ui/src/remote_output.rs | 144 ++++++++++++++++++-------------
4 files changed, 92 insertions(+), 62 deletions(-)

Detailed changes

Cargo.lock 🔗

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

crates/git_ui/Cargo.toml 🔗

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

crates/git_ui/src/git_panel.rs 🔗

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

crates/git_ui/src/remote_output.rs 🔗

@@ -24,7 +24,7 @@ impl RemoteAction {
 pub enum SuccessStyle {
     Toast,
     ToastWithLog { output: RemoteCommandOutput },
-    PushPrLink { link: String },
+    PushPrLink { text: String, link: String },
 }
 
 pub struct SuccessMessage {
@@ -37,7 +37,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
         RemoteAction::Fetch(remote) => {
             if output.stderr.is_empty() {
                 SuccessMessage {
-                    message: "Already up to date".into(),
+                    message: "Fetch: Already up to date".into(),
                     style: SuccessStyle::Toast,
                 }
             } else {
@@ -68,10 +68,9 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
 
                 Ok(files_changed)
             };
-
-            if output.stderr.starts_with("Everything up to date") {
+            if output.stdout.ends_with("Already up to date.\n") {
                 SuccessMessage {
-                    message: output.stderr.trim().to_owned(),
+                    message: "Pull: Already up to date".into(),
                     style: SuccessStyle::Toast,
                 }
             } 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) => {
-            if output.stderr.contains("* [new branch]") {
+            let message = if output.stderr.ends_with("Everything up-to-date\n") {
+                "Push: Everything is up-to-date".to_string()
+            } else {
+                format!("Pushed {} to {}", branch_name, remote_ref.name)
+            };
+
+            let style = if output.stderr.ends_with("Everything up-to-date\n") {
+                Some(SuccessStyle::Toast)
+            } else if output.stderr.contains("\nremote: ") {
                 let pr_hints = [
-                    // GitHub
-                    "Create a pull request",
-                    // Bitbucket
-                    "Create pull request",
-                    // GitLab
-                    "create a merge request",
+                    ("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
                 ];
-                let style = if pr_hints
+                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,
-                }
+                    .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 {
-                SuccessMessage {
-                    message: format!("Pushed {} to {}", branch_name, remote_ref.name),
-                    style: SuccessStyle::ToastWithLog { output },
-                }
+                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)]
 mod tests {
     use super::*;
+    use indoc::indoc;
 
     #[test]
     fn test_push_new_branch_pull_request() {
@@ -181,8 +175,7 @@ mod tests {
 
         let output = RemoteCommandOutput {
             stdout: String::new(),
-            stderr: String::from(
-                "
+            stderr: indoc! { "
                 Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
                 remote:
                 remote: Create a pull request for 'test' on GitHub by visiting:
@@ -190,13 +183,14 @@ mod tests {
                 remote:
                 To example.com:test/test.git
                  * [new branch]      test -> test
-                ",
-            ),
+                "}
+            .to_string(),
         };
 
         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");
         } else {
             panic!("Expected PushPrLink variant");
@@ -214,7 +208,7 @@ mod tests {
 
         let output = RemoteCommandOutput {
             stdout: String::new(),
-            stderr: String::from("
+            stderr: indoc! {"
                 Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
                 remote:
                 remote: To create a merge request for test, visit:
@@ -222,12 +216,14 @@ mod tests {
                 remote:
                 To example.com:test/test.git
                  * [new branch]      test -> test
-                "),
-        };
+                "}
+            .to_string()
+            };
 
         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!(
                 link,
                 "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]
     fn test_push_new_branch_no_link() {
         let action = RemoteAction::Push(
@@ -248,12 +277,12 @@ mod tests {
 
         let output = RemoteCommandOutput {
             stdout: String::new(),
-            stderr: String::from(
-                "
+            stderr: indoc! { "
                 To http://example.com/test/test.git
                  * [new branch]      test -> test
                 ",
-            ),
+            }
+            .to_string(),
         };
 
         let msg = format_output(&action, output);
@@ -261,10 +290,7 @@ mod tests {
         if let SuccessStyle::ToastWithLog { output } = &msg.style {
             assert_eq!(
                 output.stderr,
-                "
-                To http://example.com/test/test.git
-                 * [new branch]      test -> test
-                "
+                "To http://example.com/test/test.git\n * [new branch]      test -> test\n"
             );
         } else {
             panic!("Expected ToastWithLog variant");