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:
parent
24e7f868ad
commit
182edbf526
4 changed files with 95 additions and 65 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue