Detailed changes
@@ -782,6 +782,7 @@
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
"alt-enter": "menu::SecondaryConfirm",
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
@@ -790,12 +791,20 @@
"ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
}
},
+ {
+ "context": "GitPanel && CommitEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "git::Cancel"
+ }
+ },
{
"context": "GitCommit > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
"alt-l": "git::GenerateCommitMessage"
}
},
@@ -817,6 +826,7 @@
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
}
@@ -835,6 +845,7 @@
"shift-tab": "git_panel::FocusChanges",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
"alt-up": "git_panel::FocusChanges",
"alt-l": "git::GenerateCommitMessage"
}
@@ -855,17 +855,26 @@
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit",
+ "cmd-shift-enter": "git::Amend",
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
"cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
}
},
+ {
+ "context": "GitPanel && CommitEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "git::Cancel"
+ }
+ },
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit",
+ "cmd-shift-enter": "git::Amend",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
}
@@ -876,6 +885,7 @@
"bindings": {
"enter": "editor::Newline",
"cmd-enter": "git::Commit",
+ "cmd-shift-enter": "git::Amend",
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"alt-up": "git_panel::FocusChanges",
@@ -905,6 +915,7 @@
"enter": "editor::Newline",
"escape": "menu::Cancel",
"cmd-enter": "git::Commit",
+ "cmd-shift-enter": "git::Amend",
"alt-tab": "git::GenerateCommitMessage"
}
},
@@ -5,8 +5,8 @@ use futures::future::{self, BoxFuture};
use git::{
blame::Blame,
repository::{
- AskPassDelegate, Branch, CommitDetails, GitRepository, GitRepositoryCheckpoint,
- PushOptions, Remote, RepoPath, ResetMode,
+ AskPassDelegate, Branch, CommitDetails, CommitOptions, GitRepository,
+ GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode,
},
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
};
@@ -365,6 +365,7 @@ impl GitRepository for FakeGitRepository {
&self,
_message: gpui::SharedString,
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
+ _options: CommitOptions,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
unimplemented!()
@@ -50,6 +50,8 @@ actions!(
Pull,
Fetch,
Commit,
+ Amend,
+ Cancel,
ExpandCommitEditor,
GenerateCommitMessage,
Init,
@@ -74,6 +74,11 @@ impl Upstream {
}
}
+#[derive(Clone, Copy, Default)]
+pub struct CommitOptions {
+ pub amend: bool,
+}
+
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UpstreamTracking {
/// Remote ref not present in local repository.
@@ -252,6 +257,7 @@ pub trait GitRepository: Send + Sync {
&self,
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
+ options: CommitOptions,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>>;
@@ -368,8 +374,8 @@ impl RealGitRepository {
#[derive(Clone, Debug)]
pub struct GitRepositoryCheckpoint {
- ref_name: String,
- commit_sha: Oid,
+ pub ref_name: String,
+ pub commit_sha: Oid,
}
impl GitRepository for RealGitRepository {
@@ -957,6 +963,7 @@ impl GitRepository for RealGitRepository {
&self,
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
+ options: CommitOptions,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
@@ -969,6 +976,10 @@ impl GitRepository for RealGitRepository {
.arg(&message.to_string())
.arg("--cleanup=strip");
+ if options.amend {
+ cmd.arg("--amend");
+ }
+
if let Some((name, email)) = name_and_email {
cmd.arg("--author").arg(&format!("{name} <{email}>"));
}
@@ -1765,6 +1776,7 @@ mod tests {
repo.commit(
"Initial commit".into(),
None,
+ CommitOptions::default(),
Arc::new(checkpoint_author_envs()),
)
.await
@@ -1793,6 +1805,7 @@ mod tests {
repo.commit(
"Commit after checkpoint".into(),
None,
+ CommitOptions::default(),
Arc::new(checkpoint_author_envs()),
)
.await
@@ -1,8 +1,12 @@
use crate::branch_picker::{self, BranchList};
use crate::git_panel::{GitPanel, commit_message_editor};
-use git::{Commit, GenerateCommitMessage};
+use git::repository::CommitOptions;
+use git::{Amend, Commit, GenerateCommitMessage};
+use language::Buffer;
use panel::{panel_button, panel_editor_style, panel_filled_button};
-use ui::{KeybindingHint, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
+use ui::{
+ ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
+};
use editor::{Editor, EditorElement};
use gpui::*;
@@ -100,6 +104,9 @@ impl CommitModal {
workspace.register_action(|workspace, _: &Commit, window, cx| {
CommitModal::toggle(workspace, window, cx);
});
+ workspace.register_action(|workspace, _: &Amend, window, cx| {
+ CommitModal::toggle(workspace, window, cx);
+ });
}
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
@@ -214,23 +221,67 @@ impl CommitModal {
)
}
+ fn render_git_commit_menu(
+ &self,
+ id: impl Into<ElementId>,
+ keybinding_target: Option<FocusHandle>,
+ ) -> impl IntoElement {
+ PopoverMenu::new(id.into())
+ .trigger(
+ ui::ButtonLike::new_rounded_right("commit-split-button-right")
+ .layer(ui::ElevationIndex::ModalSurface)
+ .size(ui::ButtonSize::None)
+ .child(
+ div()
+ .px_1()
+ .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
+ ),
+ )
+ .menu(move |window, cx| {
+ Some(ContextMenu::build(window, cx, |context_menu, _, _| {
+ context_menu
+ .when_some(keybinding_target.clone(), |el, keybinding_target| {
+ el.context(keybinding_target.clone())
+ })
+ .action("Amend...", Amend.boxed_clone())
+ }))
+ })
+ .anchor(Corner::TopRight)
+ }
+
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let (can_commit, tooltip, commit_label, co_authors, generate_commit_message, active_repo) =
- self.git_panel.update(cx, |git_panel, cx| {
- let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
- let title = git_panel.commit_button_title();
- let co_authors = git_panel.render_co_authors(cx);
- let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
- let active_repo = git_panel.active_repository.clone();
- (
- can_commit,
- tooltip,
- title,
- co_authors,
- generate_commit_message,
- active_repo,
- )
- });
+ let (
+ can_commit,
+ tooltip,
+ commit_label,
+ co_authors,
+ generate_commit_message,
+ active_repo,
+ is_amend_pending,
+ has_previous_commit,
+ ) = self.git_panel.update(cx, |git_panel, cx| {
+ let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
+ let title = git_panel.commit_button_title();
+ let co_authors = git_panel.render_co_authors(cx);
+ let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
+ let active_repo = git_panel.active_repository.clone();
+ let is_amend_pending = git_panel.amend_pending();
+ let has_previous_commit = active_repo
+ .as_ref()
+ .and_then(|repo| repo.read(cx).branch.as_ref())
+ .and_then(|branch| branch.most_recent_commit.as_ref())
+ .is_some();
+ (
+ can_commit,
+ tooltip,
+ title,
+ co_authors,
+ generate_commit_message,
+ active_repo,
+ is_amend_pending,
+ has_previous_commit,
+ )
+ });
let branch = active_repo
.as_ref()
@@ -277,21 +328,6 @@ impl CommitModal {
None
};
- let commit_button = panel_filled_button(commit_label)
- .tooltip({
- let panel_editor_focus_handle = focus_handle.clone();
- move |window, cx| {
- Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
- }
- })
- .disabled(!can_commit)
- .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
- telemetry::event!("Git Committed", source = "Git Modal");
- this.git_panel
- .update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
- cx.emit(DismissEvent);
- }));
-
h_flex()
.group("commit_editor_footer")
.flex_none()
@@ -324,21 +360,188 @@ impl CommitModal {
.px_1()
.gap_4()
.children(close_kb_hint)
- .child(commit_button),
+ .when(is_amend_pending, |this| {
+ let focus_handle = focus_handle.clone();
+ this.child(
+ panel_filled_button(commit_label)
+ .tooltip(move |window, cx| {
+ if can_commit {
+ Tooltip::for_action_in(
+ tooltip,
+ &Amend,
+ &focus_handle,
+ window,
+ cx,
+ )
+ } else {
+ Tooltip::simple(tooltip, cx)
+ }
+ })
+ .disabled(!can_commit)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(Box::new(git::Commit), cx);
+ }),
+ )
+ })
+ .when(!is_amend_pending, |this| {
+ this.when(has_previous_commit, |this| {
+ this.child(SplitButton::new(
+ ui::ButtonLike::new_rounded_left(ElementId::Name(
+ format!("split-button-left-{}", commit_label).into(),
+ ))
+ .layer(ui::ElevationIndex::ModalSurface)
+ .size(ui::ButtonSize::Compact)
+ .child(
+ div()
+ .child(Label::new(commit_label).size(LabelSize::Small))
+ .mr_0p5(),
+ )
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(Box::new(git::Commit), cx);
+ })
+ .disabled(!can_commit)
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ if can_commit {
+ Tooltip::with_meta_in(
+ tooltip,
+ Some(&git::Commit),
+ "git commit",
+ &focus_handle.clone(),
+ window,
+ cx,
+ )
+ } else {
+ Tooltip::simple(tooltip, cx)
+ }
+ }
+ }),
+ self.render_git_commit_menu(
+ ElementId::Name(
+ format!("split-button-right-{}", commit_label).into(),
+ ),
+ Some(focus_handle.clone()),
+ )
+ .into_any_element(),
+ ))
+ })
+ .when(!has_previous_commit, |this| {
+ this.child(
+ panel_filled_button(commit_label)
+ .tooltip(move |window, cx| {
+ if can_commit {
+ Tooltip::with_meta_in(
+ tooltip,
+ Some(&git::Commit),
+ "git commit",
+ &focus_handle,
+ window,
+ cx,
+ )
+ } else {
+ Tooltip::simple(tooltip, cx)
+ }
+ })
+ .disabled(!can_commit)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(Box::new(git::Commit), cx);
+ }),
+ )
+ })
+ }),
)
}
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
- cx.emit(DismissEvent);
+ if self.git_panel.read(cx).amend_pending() {
+ self.git_panel
+ .update(cx, |git_panel, _| git_panel.set_amend_pending(false));
+ cx.notify();
+ } else {
+ cx.emit(DismissEvent);
+ }
+ }
+
+ pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
+ self.commit_editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .unwrap()
+ .clone()
}
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
+ if self.git_panel.read(cx).amend_pending() {
+ return;
+ }
telemetry::event!("Git Committed", source = "Git Modal");
- self.git_panel
- .update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
+ self.git_panel.update(cx, |git_panel, cx| {
+ git_panel.commit_changes(CommitOptions { amend: false }, window, cx)
+ });
cx.emit(DismissEvent);
}
+ fn amend(&mut self, _: &git::Amend, window: &mut Window, cx: &mut Context<Self>) {
+ let Some(active_repository) = self.git_panel.read(cx).active_repository.as_ref() else {
+ return;
+ };
+ let Some(branch) = active_repository.read(cx).branch.as_ref() else {
+ return;
+ };
+ let Some(recent_sha) = branch
+ .most_recent_commit
+ .as_ref()
+ .map(|commit| commit.sha.to_string())
+ else {
+ return;
+ };
+ if self
+ .commit_editor
+ .focus_handle(cx)
+ .contains_focused(window, cx)
+ {
+ if !self.git_panel.read(cx).amend_pending() {
+ self.git_panel.update(cx, |git_panel, _| {
+ git_panel.set_amend_pending(true);
+ });
+ cx.notify();
+ if self.commit_editor.read(cx).is_empty(cx) {
+ let detail_task = self.git_panel.update(cx, |git_panel, cx| {
+ git_panel.load_commit_details(recent_sha, cx)
+ });
+ cx.spawn(async move |this, cx| {
+ if let Ok(message) = detail_task.await.map(|detail| detail.message) {
+ this.update(cx, |this, cx| {
+ this.commit_message_buffer(cx).update(cx, |buffer, cx| {
+ let insert_position = buffer.anchor_before(buffer.len());
+ buffer.edit(
+ [(insert_position..insert_position, message)],
+ None,
+ cx,
+ );
+ });
+ })
+ .log_err();
+ }
+ })
+ .detach();
+ }
+ } else {
+ telemetry::event!("Git Amended", source = "Git Panel");
+ self.git_panel.update(cx, |git_panel, cx| {
+ git_panel.set_amend_pending(false);
+ git_panel.commit_changes(CommitOptions { amend: true }, window, cx);
+ });
+ cx.emit(DismissEvent);
+ }
+ } else {
+ cx.propagate();
+ }
+ }
+
fn toggle_branch_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.branch_list_handle.is_focused(window, cx) {
self.focus_handle(cx).focus(window)
@@ -361,6 +564,7 @@ impl Render for CommitModal {
.key_context("GitCommit")
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::commit))
+ .on_action(cx.listener(Self::amend))
.on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| {
this.git_panel.update(cx, |panel, cx| {
panel.generate_commit_message(cx);
@@ -21,11 +21,11 @@ use editor::{
use futures::StreamExt as _;
use git::blame::ParsedCommitMessage;
use git::repository::{
- Branch, CommitDetails, CommitSummary, DiffType, PushOptions, Remote, RemoteCommandOutput,
- ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
+ Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, PushOptions, Remote,
+ RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
};
use git::status::StageStatus;
-use git::{Commit, ToggleStaged, repository::RepoPath, status::FileStatus};
+use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::{
Action, Animation, AnimationExt as _, Axis, ClickEvent, Corner, DismissEvent, Entity,
@@ -59,8 +59,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
use ui::{
- Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, Tooltip,
- prelude::*,
+ Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, SplitButton,
+ Tooltip, prelude::*,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::AppState;
@@ -340,6 +340,7 @@ pub struct GitPanel {
new_staged_count: usize,
pending: Vec<PendingOperation>,
pending_commit: Option<Task<()>>,
+ amend_pending: bool,
pending_serialization: Task<Option<()>>,
pub(crate) project: Entity<Project>,
scroll_handle: UniformListScrollHandle,
@@ -492,6 +493,7 @@ impl GitPanel {
new_staged_count: 0,
pending: Vec::new(),
pending_commit: None,
+ amend_pending: false,
pending_serialization: Task::ready(None),
single_staged_entry: None,
single_tracked_entry: None,
@@ -1417,18 +1419,76 @@ impl GitPanel {
}
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
+ if self.amend_pending {
+ return;
+ }
if self
.commit_editor
.focus_handle(cx)
.contains_focused(window, cx)
{
telemetry::event!("Git Committed", source = "Git Panel");
- self.commit_changes(window, cx)
+ self.commit_changes(CommitOptions { amend: false }, window, cx)
+ } else {
+ cx.propagate();
+ }
+ }
+
+ fn amend(&mut self, _: &git::Amend, window: &mut Window, cx: &mut Context<Self>) {
+ let Some(active_repository) = self.active_repository.as_ref() else {
+ return;
+ };
+ let Some(branch) = active_repository.read(cx).branch.as_ref() else {
+ return;
+ };
+ let Some(recent_sha) = branch
+ .most_recent_commit
+ .as_ref()
+ .map(|commit| commit.sha.to_string())
+ else {
+ return;
+ };
+ if self
+ .commit_editor
+ .focus_handle(cx)
+ .contains_focused(window, cx)
+ {
+ if !self.amend_pending {
+ self.amend_pending = true;
+ cx.notify();
+ if self.commit_editor.read(cx).is_empty(cx) {
+ let detail_task = self.load_commit_details(recent_sha, cx);
+ cx.spawn(async move |this, cx| {
+ if let Ok(message) = detail_task.await.map(|detail| detail.message) {
+ this.update(cx, |this, cx| {
+ this.commit_message_buffer(cx).update(cx, |buffer, cx| {
+ let start = buffer.anchor_before(0);
+ let end = buffer.anchor_after(buffer.len());
+ buffer.edit([(start..end, message)], None, cx);
+ });
+ })
+ .log_err();
+ }
+ })
+ .detach();
+ }
+ } else {
+ telemetry::event!("Git Amended", source = "Git Panel");
+ self.amend_pending = false;
+ self.commit_changes(CommitOptions { amend: true }, window, cx);
+ }
} else {
cx.propagate();
}
}
+ fn cancel(&mut self, _: &git::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+ if self.amend_pending {
+ self.amend_pending = false;
+ cx.notify();
+ }
+ }
+
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
let message = self.commit_editor.read(cx).text(cx);
@@ -1440,7 +1500,12 @@ impl GitPanel {
.filter(|message| !message.trim().is_empty())
}
- pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ pub(crate) fn commit_changes(
+ &mut self,
+ options: CommitOptions,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
let Some(active_repository) = self.active_repository.clone() else {
return;
};
@@ -1474,8 +1539,9 @@ impl GitPanel {
let task = if self.has_staged_changes() {
// Repository serializes all git operations, so we can just send a commit immediately
- let commit_task =
- active_repository.update(cx, |repo, cx| repo.commit(message.into(), None, cx));
+ let commit_task = active_repository.update(cx, |repo, cx| {
+ repo.commit(message.into(), None, options, cx)
+ });
cx.background_spawn(async move { commit_task.await? })
} else {
let changed_files = self
@@ -1495,8 +1561,9 @@ impl GitPanel {
active_repository.update(cx, |repo, cx| repo.stage_entries(changed_files, cx));
cx.spawn(async move |_, cx| {
stage_task.await?;
- let commit_task = active_repository
- .update(cx, |repo, cx| repo.commit(message.into(), None, cx))?;
+ let commit_task = active_repository.update(cx, |repo, cx| {
+ repo.commit(message.into(), None, options, cx)
+ })?;
commit_task.await?
})
};
@@ -2722,6 +2789,34 @@ impl GitPanel {
}
}
+ fn render_git_commit_menu(
+ &self,
+ id: impl Into<ElementId>,
+ keybinding_target: Option<FocusHandle>,
+ ) -> impl IntoElement {
+ PopoverMenu::new(id.into())
+ .trigger(
+ ui::ButtonLike::new_rounded_right("commit-split-button-right")
+ .layer(ui::ElevationIndex::ModalSurface)
+ .size(ui::ButtonSize::None)
+ .child(
+ div()
+ .px_1()
+ .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
+ ),
+ )
+ .menu(move |window, cx| {
+ Some(ContextMenu::build(window, cx, |context_menu, _, _| {
+ context_menu
+ .when_some(keybinding_target.clone(), |el, keybinding_target| {
+ el.context(keybinding_target.clone())
+ })
+ .action("Amend...", Amend.boxed_clone())
+ }))
+ })
+ .anchor(Corner::TopRight)
+ }
+
pub fn configure_commit_button(&self, cx: &mut Context<Self>) -> (bool, &'static str) {
if self.has_unstaged_conflicts() {
(false, "You must resolve conflicts before committing")
@@ -2739,10 +2834,18 @@ impl GitPanel {
}
pub fn commit_button_title(&self) -> &'static str {
- if self.has_staged_changes() {
- "Commit"
+ if self.amend_pending {
+ if self.has_staged_changes() {
+ "Amend"
+ } else {
+ "Amend Tracked"
+ }
} else {
- "Commit Tracked"
+ if self.has_staged_changes() {
+ "Commit"
+ } else {
+ "Commit Tracked"
+ }
}
}
@@ -2885,6 +2988,10 @@ impl GitPanel {
let editor_is_long = self.commit_editor.update(cx, |editor, cx| {
editor.max_point(cx).row().0 >= MAX_PANEL_EDITOR_LINES as u32
});
+ let has_previous_commit = branch
+ .as_ref()
+ .and_then(|branch| branch.most_recent_commit.as_ref())
+ .is_some();
let footer = v_flex()
.child(PanelRepoFooter::new(display_name, branch, Some(git_panel)))
@@ -2920,32 +3027,140 @@ impl GitPanel {
.unwrap_or_else(|| div().into_any_element()),
)
.child(
- h_flex().gap_0p5().children(enable_coauthors).child(
- panel_filled_button(title)
- .tooltip(move |window, cx| {
- if can_commit {
- Tooltip::for_action_in(
- tooltip,
- &Commit,
- &commit_tooltip_focus_handle,
- window,
- cx,
+ h_flex()
+ .gap_0p5()
+ .children(enable_coauthors)
+ .when(self.amend_pending, {
+ |this| {
+ this.h_flex()
+ .gap_1()
+ .child(
+ panel_filled_button("Cancel")
+ .tooltip({
+ let handle =
+ commit_tooltip_focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Cancel amend",
+ &git::Cancel,
+ &handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(
+ Box::new(git::Cancel),
+ cx,
+ );
+ }),
)
- } else {
- Tooltip::simple(tooltip, cx)
- }
+ .child(
+ panel_filled_button(title)
+ .tooltip({
+ let handle =
+ commit_tooltip_focus_handle.clone();
+ move |window, cx| {
+ if can_commit {
+ Tooltip::for_action_in(
+ tooltip, &Amend, &handle,
+ window, cx,
+ )
+ } else {
+ Tooltip::simple(tooltip, cx)
+ }
+ }
+ })
+ .disabled(!can_commit || self.modal_open)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(
+ Box::new(git::Amend),
+ cx,
+ );
+ }),
+ )
+ }
+ })
+ .when(!self.amend_pending, |this| {
+ this.when(has_previous_commit, |this| {
+ this.child(SplitButton::new(
+ ui::ButtonLike::new_rounded_left(ElementId::Name(
+ format!("split-button-left-{}", title).into(),
+ ))
+ .layer(ui::ElevationIndex::ModalSurface)
+ .size(ui::ButtonSize::Compact)
+ .child(
+ div()
+ .child(
+ Label::new(title)
+ .size(LabelSize::Small),
+ )
+ .mr_0p5(),
+ )
+ .on_click(move |_, window, cx| {
+ window
+ .dispatch_action(Box::new(git::Commit), cx);
+ })
+ .disabled(!can_commit || self.modal_open)
+ .tooltip({
+ let handle =
+ commit_tooltip_focus_handle.clone();
+ move |window, cx| {
+ if can_commit {
+ Tooltip::with_meta_in(
+ tooltip,
+ Some(&git::Commit),
+ "git commit",
+ &handle.clone(),
+ window,
+ cx,
+ )
+ } else {
+ Tooltip::simple(tooltip, cx)
+ }
+ }
+ }),
+ self.render_git_commit_menu(
+ ElementId::Name(
+ format!("split-button-right-{}", title)
+ .into(),
+ ),
+ Some(commit_tooltip_focus_handle.clone()),
+ )
+ .into_any_element(),
+ ))
})
- .disabled(!can_commit || self.modal_open)
- .on_click({
- cx.listener(move |this, _: &ClickEvent, window, cx| {
- telemetry::event!(
- "Git Committed",
- source = "Git Panel"
- );
- this.commit_changes(window, cx)
- })
- }),
- ),
+ .when(
+ !has_previous_commit,
+ |this| {
+ this.child(
+ panel_filled_button(title)
+ .tooltip(move |window, cx| {
+ if can_commit {
+ Tooltip::with_meta_in(
+ tooltip,
+ Some(&git::Commit),
+ "git commit",
+ &commit_tooltip_focus_handle,
+ window,
+ cx,
+ )
+ } else {
+ Tooltip::simple(tooltip, cx)
+ }
+ })
+ .disabled(!can_commit || self.modal_open)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(
+ Box::new(git::Commit),
+ cx,
+ );
+ }),
+ )
+ },
+ )
+ }),
),
)
.child(
@@ -2994,6 +3209,17 @@ impl GitPanel {
Some(footer)
}
+ fn render_pending_amend(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .py_2()
+ .px(px(8.))
+ .border_color(cx.theme().colors().border)
+ .child(
+ Label::new("Your changes will modify your most recent commit. If you want to make these changes as a new commit, you can cancel the amend operation.")
+ .size(LabelSize::Small),
+ )
+ }
+
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let active_repository = self.active_repository.as_ref()?;
let branch = active_repository.read(cx).branch.as_ref()?;
@@ -3448,7 +3674,7 @@ impl GitPanel {
.into_any_element()
}
- fn load_commit_details(
+ pub fn load_commit_details(
&self,
sha: String,
cx: &mut Context<Self>,
@@ -3766,6 +3992,14 @@ impl GitPanel {
fn has_write_access(&self, cx: &App) -> bool {
!self.project.read(cx).is_read_only(cx)
}
+
+ pub fn amend_pending(&self) -> bool {
+ self.amend_pending
+ }
+
+ pub fn set_amend_pending(&mut self, value: bool) {
+ self.amend_pending = value;
+ }
}
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
@@ -3806,6 +4040,8 @@ impl Render for GitPanel {
.when(has_write_access && !project.is_read_only(cx), |this| {
this.on_action(cx.listener(Self::toggle_staged_for_selected))
.on_action(cx.listener(GitPanel::commit))
+ .on_action(cx.listener(GitPanel::amend))
+ .on_action(cx.listener(GitPanel::cancel))
.on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::stage_selected))
@@ -3852,7 +4088,12 @@ impl Render for GitPanel {
}
})
.children(self.render_footer(window, cx))
- .children(self.render_previous_commit(cx))
+ .when(self.amend_pending, |this| {
+ this.child(self.render_pending_amend(cx))
+ })
+ .when(!self.amend_pending, |this| {
+ this.children(self.render_previous_commit(cx))
+ })
.into_any_element(),
)
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
@@ -368,6 +368,7 @@ mod remote_button {
})
.anchor(Corner::TopRight)
}
+
#[allow(clippy::too_many_arguments)]
fn split_button(
id: SharedString,
@@ -21,7 +21,7 @@ use git::{
blame::Blame,
parse_git_remote_url,
repository::{
- Branch, CommitDetails, CommitDiff, CommitFile, DiffType, GitRepository,
+ Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, GitRepository,
GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
UpstreamTrackingStatus,
},
@@ -1656,10 +1656,18 @@ impl GitStore {
let message = SharedString::from(envelope.payload.message);
let name = envelope.payload.name.map(SharedString::from);
let email = envelope.payload.email.map(SharedString::from);
+ let options = envelope.payload.options.unwrap_or_default();
repository_handle
.update(&mut cx, |repository_handle, cx| {
- repository_handle.commit(message, name.zip(email), cx)
+ repository_handle.commit(
+ message,
+ name.zip(email),
+ CommitOptions {
+ amend: options.amend,
+ },
+ cx,
+ )
})?
.await??;
Ok(proto::Ack {})
@@ -3248,6 +3256,7 @@ impl Repository {
&mut self,
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
+ options: CommitOptions,
_cx: &mut App,
) -> oneshot::Receiver<Result<()>> {
let id = self.id;
@@ -3258,7 +3267,11 @@ impl Repository {
backend,
environment,
..
- } => backend.commit(message, name_and_email, environment).await,
+ } => {
+ backend
+ .commit(message, name_and_email, options, environment)
+ .await
+ }
RepositoryState::Remote { project_id, client } => {
let (name, email) = name_and_email.unzip();
client
@@ -3268,6 +3281,9 @@ impl Repository {
message: String::from(message),
name: name.map(String::from),
email: email.map(String::from),
+ options: Some(proto::commit::CommitOptions {
+ amend: options.amend,
+ }),
})
.await
.context("sending commit request")?;
@@ -292,6 +292,11 @@ message Commit {
optional string name = 4;
optional string email = 5;
string message = 6;
+ optional CommitOptions options = 7;
+
+ message CommitOptions {
+ bool amend = 1;
+ }
}
message OpenCommitMessageBuffer {
@@ -20,6 +20,12 @@ pub struct SplitButton {
pub right: AnyElement,
}
+impl SplitButton {
+ pub fn new(left: ButtonLike, right: AnyElement) -> Self {
+ Self { left, right }
+ }
+}
+
impl RenderOnce for SplitButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()