git: Add ability to pass `--signoff` (#29874)

Eric Cornelissen created

This adds an option for `--signoff` to the git panel and commit modal.
It allows users to enable the [`--signoff`
flag](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt-code--signoffcode)
when committing through Zed. The option is added to the context menu of
the commit button (following the style of the "Editor Controls").

To support this, the commit+amend experience was revamped (following the
ideas of [this
comment](https://github.com/zed-industries/zed/pull/29874#issuecomment-2950848000)).
Amending is now also a toggle in the commit button's dropdown menu. I've
kept some of the original experience such as the changed button text and
ability to cancel outside the context menu.

The tooltip of the commit buttons now also includes the flags that will
be used based on the amending and signoff status (which I couldn't
capture in screenshots unfortunately). So, by default the tooltip will
say `git commit` and if you toggle, e.g., amending on it will say `git
commit --amend`.

| What | Panel | Modal |
| --- | --- | --- |
| Not amending, dropdown | ![git modal preview, not amending,
dropdown](https://github.com/user-attachments/assets/82c2b338-b3b5-418c-97bf-98c33202d7dd)
| ![commit modal preview, not amending,
dropdown](https://github.com/user-attachments/assets/f7a6f2fb-902d-447d-a473-2efb4ba0f444)
|
| Amending, dropdown | ![git modal preview, amending,
dropdown](https://github.com/user-attachments/assets/9e755975-4a27-43f0-aa62-be002ecd3a92)
| ![commit modal preview, amending,
dropdown](https://github.com/user-attachments/assets/cad03817-14e1-46f6-ba39-8ccc7dd12161)
|
| Amending | ![git modal preview,
amending](https://github.com/user-attachments/assets/e1ec4eba-174e-4e5f-9659-5867d6b0fdc2)
| - |

The initial implementation was based on the changeset of
https://github.com/zed-industries/zed/pull/28187.

Closes https://github.com/zed-industries/zed/discussions/26114

Release Notes:

- Added git `--signoff` support.
- Update the git `--amend` experience.
- Improved git panel to persist width as well as amend and signoff on a
per-workspace basis.

Change summary

crates/git/src/git.rs             |   2 
crates/git/src/repository.rs      |   5 
crates/git_ui/src/commit_modal.rs | 253 +++++++++----------
crates/git_ui/src/git_panel.rs    | 416 +++++++++++++++++---------------
crates/project/src/git_store.rs   |   2 
crates/proto/proto/git.proto      |   1 
6 files changed, 342 insertions(+), 337 deletions(-)

Detailed changes

crates/git/src/git.rs 🔗

@@ -77,6 +77,8 @@ actions!(
         Commit,
         /// Amends the last commit with staged changes.
         Amend,
+        /// Enable the --signoff option.
+        Signoff,
         /// Cancels the current git operation.
         Cancel,
         /// Expands the commit message editor.

crates/git/src/repository.rs 🔗

@@ -96,6 +96,7 @@ impl Upstream {
 #[derive(Clone, Copy, Default)]
 pub struct CommitOptions {
     pub amend: bool,
+    pub signoff: bool,
 }
 
 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
@@ -1209,6 +1210,10 @@ impl GitRepository for RealGitRepository {
                     cmd.arg("--amend");
                 }
 
+                if options.signoff {
+                    cmd.arg("--signoff");
+                }
+
                 if let Some((name, email)) = name_and_email {
                     cmd.arg("--author").arg(&format!("{name} <{email}>"));
                 }

crates/git_ui/src/commit_modal.rs 🔗

@@ -1,8 +1,8 @@
 use crate::branch_picker::{self, BranchList};
 use crate::git_panel::{GitPanel, commit_message_editor};
 use git::repository::CommitOptions;
-use git::{Amend, Commit, GenerateCommitMessage};
-use panel::{panel_button, panel_editor_style, panel_filled_button};
+use git::{Amend, Commit, GenerateCommitMessage, Signoff};
+use panel::{panel_button, panel_editor_style};
 use ui::{
     ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
 };
@@ -273,14 +273,51 @@ impl CommitModal {
                             .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())
-                }))
+            .menu({
+                let git_panel_entity = self.git_panel.clone();
+                move |window, cx| {
+                    let git_panel = git_panel_entity.read(cx);
+                    let amend_enabled = git_panel.amend_pending();
+                    let signoff_enabled = git_panel.signoff_enabled();
+                    let has_previous_commit = git_panel.head_commit(cx).is_some();
+
+                    Some(ContextMenu::build(window, cx, |context_menu, _, _| {
+                        context_menu
+                            .when_some(keybinding_target.clone(), |el, keybinding_target| {
+                                el.context(keybinding_target.clone())
+                            })
+                            .when(has_previous_commit, |this| {
+                                this.toggleable_entry(
+                                    "Amend",
+                                    amend_enabled,
+                                    IconPosition::Start,
+                                    Some(Box::new(Amend)),
+                                    {
+                                        let git_panel = git_panel_entity.clone();
+                                        move |window, cx| {
+                                            git_panel.update(cx, |git_panel, cx| {
+                                                git_panel.toggle_amend_pending(&Amend, window, cx);
+                                            })
+                                        }
+                                    },
+                                )
+                            })
+                            .toggleable_entry(
+                                "Signoff",
+                                signoff_enabled,
+                                IconPosition::Start,
+                                Some(Box::new(Signoff)),
+                                {
+                                    let git_panel = git_panel_entity.clone();
+                                    move |window, cx| {
+                                        git_panel.update(cx, |git_panel, cx| {
+                                            git_panel.toggle_signoff_enabled(&Signoff, window, cx);
+                                        })
+                                    }
+                                },
+                            )
+                    }))
+                }
             })
             .with_handle(self.commit_menu_handle.clone())
             .anchor(Corner::TopRight)
@@ -295,7 +332,7 @@ impl CommitModal {
             generate_commit_message,
             active_repo,
             is_amend_pending,
-            has_previous_commit,
+            is_signoff_enabled,
         ) = 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();
@@ -303,10 +340,7 @@ impl CommitModal {
             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).head_commit.as_ref())
-                .is_some();
+            let is_signoff_enabled = git_panel.signoff_enabled();
             (
                 can_commit,
                 tooltip,
@@ -315,7 +349,7 @@ impl CommitModal {
                 generate_commit_message,
                 active_repo,
                 is_amend_pending,
-                has_previous_commit,
+                is_signoff_enabled,
             )
         });
 
@@ -396,126 +430,59 @@ impl CommitModal {
                     .px_1()
                     .gap_4()
                     .children(close_kb_hint)
-                    .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(cx.listener(move |this, _: &ClickEvent, window, cx| {
-                                    telemetry::event!("Git Amended", source = "Git Modal");
-                                    this.git_panel.update(cx, |git_panel, cx| {
-                                        git_panel.set_amend_pending(false, cx);
-                                        git_panel.commit_changes(
-                                            CommitOptions { amend: true },
-                                            window,
-                                            cx,
-                                        );
-                                    });
-                                    cx.emit(DismissEvent);
-                                })),
+                    .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(),
                         )
-                    })
-                    .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(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(
+                                    CommitOptions {
+                                        amend: is_amend_pending,
+                                        signoff: is_signoff_enabled,
+                                    },
+                                    window,
+                                    cx,
                                 )
-                                .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(
-                                            CommitOptions { amend: false },
-                                            window,
-                                            cx,
-                                        )
-                                    });
-                                    cx.emit(DismissEvent);
-                                }))
-                                .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(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(
-                                                    CommitOptions { amend: false },
-                                                    window,
-                                                    cx,
-                                                )
-                                            });
-                                            cx.emit(DismissEvent);
-                                        },
-                                    )),
-                            )
-                        })
-                    }),
+                            });
+                            cx.emit(DismissEvent);
+                        }))
+                        .disabled(!can_commit)
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |window, cx| {
+                                if can_commit {
+                                    Tooltip::with_meta_in(
+                                        tooltip,
+                                        Some(&git::Commit),
+                                        format!(
+                                            "git commit{}{}",
+                                            if is_amend_pending { " --amend" } else { "" },
+                                            if is_signoff_enabled { " --signoff" } else { "" }
+                                        ),
+                                        &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(),
+                    )),
             )
     }
 
@@ -534,7 +501,14 @@ impl CommitModal {
         }
         telemetry::event!("Git Committed", source = "Git Modal");
         self.git_panel.update(cx, |git_panel, cx| {
-            git_panel.commit_changes(CommitOptions { amend: false }, window, cx)
+            git_panel.commit_changes(
+                CommitOptions {
+                    amend: false,
+                    signoff: git_panel.signoff_enabled(),
+                },
+                window,
+                cx,
+            )
         });
         cx.emit(DismissEvent);
     }
@@ -559,7 +533,14 @@ impl CommitModal {
             telemetry::event!("Git Amended", source = "Git Modal");
             self.git_panel.update(cx, |git_panel, cx| {
                 git_panel.set_amend_pending(false, cx);
-                git_panel.commit_changes(CommitOptions { amend: true }, window, cx);
+                git_panel.commit_changes(
+                    CommitOptions {
+                        amend: true,
+                        signoff: git_panel.signoff_enabled(),
+                    },
+                    window,
+                    cx,
+                );
             });
             cx.emit(DismissEvent);
         }

crates/git_ui/src/git_panel.rs 🔗

@@ -25,7 +25,7 @@ use git::repository::{
     UpstreamTrackingStatus, get_git_committer,
 };
 use git::status::StageStatus;
-use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
+use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
 use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
 use gpui::{
     Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
@@ -61,8 +61,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, SplitButton,
-    Tooltip, prelude::*,
+    Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar,
+    ScrollbarState, SplitButton, Tooltip, prelude::*,
 };
 use util::{ResultExt, TryFutureExt, maybe};
 
@@ -174,6 +174,10 @@ pub enum Event {
 #[derive(Serialize, Deserialize)]
 struct SerializedGitPanel {
     width: Option<Pixels>,
+    #[serde(default)]
+    amend_pending: bool,
+    #[serde(default)]
+    signoff_enabled: bool,
 }
 
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -337,6 +341,7 @@ pub struct GitPanel {
     pending: Vec<PendingOperation>,
     pending_commit: Option<Task<()>>,
     amend_pending: bool,
+    signoff_enabled: bool,
     pending_serialization: Task<Option<()>>,
     pub(crate) project: Entity<Project>,
     scroll_handle: UniformListScrollHandle,
@@ -512,6 +517,7 @@ impl GitPanel {
                 pending: Vec::new(),
                 pending_commit: None,
                 amend_pending: false,
+                signoff_enabled: false,
                 pending_serialization: Task::ready(None),
                 single_staged_entry: None,
                 single_tracked_entry: None,
@@ -690,14 +696,38 @@ impl GitPanel {
         cx.notify();
     }
 
+    fn serialization_key(workspace: &Workspace) -> Option<String> {
+        workspace
+            .database_id()
+            .map(|id| i64::from(id).to_string())
+            .or(workspace.session_id())
+            .map(|id| format!("{}-{:?}", GIT_PANEL_KEY, id))
+    }
+
     fn serialize(&mut self, cx: &mut Context<Self>) {
         let width = self.width;
+        let amend_pending = self.amend_pending;
+        let signoff_enabled = self.signoff_enabled;
+
+        let Some(serialization_key) = self
+            .workspace
+            .read_with(cx, |workspace, _| Self::serialization_key(workspace))
+            .ok()
+            .flatten()
+        else {
+            return;
+        };
+
         self.pending_serialization = cx.background_spawn(
             async move {
                 KEY_VALUE_STORE
                     .write_kvp(
-                        GIT_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedGitPanel { width })?,
+                        serialization_key,
+                        serde_json::to_string(&SerializedGitPanel {
+                            width,
+                            amend_pending,
+                            signoff_enabled,
+                        })?,
                     )
                     .await?;
                 anyhow::Ok(())
@@ -1432,7 +1462,14 @@ impl GitPanel {
             .contains_focused(window, cx)
         {
             telemetry::event!("Git Committed", source = "Git Panel");
-            self.commit_changes(CommitOptions { amend: false }, window, cx)
+            self.commit_changes(
+                CommitOptions {
+                    amend: false,
+                    signoff: self.signoff_enabled,
+                },
+                window,
+                cx,
+            )
         } else {
             cx.propagate();
         }
@@ -1444,19 +1481,21 @@ impl GitPanel {
             .focus_handle(cx)
             .contains_focused(window, cx)
         {
-            if self
-                .active_repository
-                .as_ref()
-                .and_then(|repo| repo.read(cx).head_commit.as_ref())
-                .is_some()
-            {
+            if self.head_commit(cx).is_some() {
                 if !self.amend_pending {
                     self.set_amend_pending(true, cx);
                     self.load_last_commit_message_if_empty(cx);
                 } else {
                     telemetry::event!("Git Amended", source = "Git Panel");
                     self.set_amend_pending(false, cx);
-                    self.commit_changes(CommitOptions { amend: true }, window, cx);
+                    self.commit_changes(
+                        CommitOptions {
+                            amend: true,
+                            signoff: self.signoff_enabled,
+                        },
+                        window,
+                        cx,
+                    );
                 }
             }
         } else {
@@ -1464,21 +1503,21 @@ impl GitPanel {
         }
     }
 
+    pub fn head_commit(&self, cx: &App) -> Option<CommitDetails> {
+        self.active_repository
+            .as_ref()
+            .and_then(|repo| repo.read(cx).head_commit.as_ref())
+            .cloned()
+    }
+
     pub fn load_last_commit_message_if_empty(&mut self, cx: &mut Context<Self>) {
         if !self.commit_editor.read(cx).is_empty(cx) {
             return;
         }
-        let Some(active_repository) = self.active_repository.as_ref() else {
-            return;
-        };
-        let Some(recent_sha) = active_repository
-            .read(cx)
-            .head_commit
-            .as_ref()
-            .map(|commit| commit.sha.to_string())
-        else {
+        let Some(head_commit) = self.head_commit(cx) else {
             return;
         };
+        let recent_sha = head_commit.sha.to_string();
         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) {
@@ -1495,12 +1534,6 @@ impl GitPanel {
         .detach();
     }
 
-    fn cancel(&mut self, _: &git::Cancel, _: &mut Window, cx: &mut Context<Self>) {
-        if self.amend_pending {
-            self.set_amend_pending(false, cx);
-        }
-    }
-
     fn custom_or_suggested_commit_message(
         &self,
         window: &mut Window,
@@ -3003,14 +3036,35 @@ impl GitPanel {
                             .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())
-                }))
+            .menu({
+                let has_previous_commit = self.head_commit(cx).is_some();
+                let amend = self.amend_pending();
+                let signoff = self.signoff_enabled;
+
+                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())
+                            })
+                            .when(has_previous_commit, |this| {
+                                this.toggleable_entry(
+                                    "Amend",
+                                    amend,
+                                    IconPosition::Start,
+                                    Some(Box::new(Amend)),
+                                    move |window, cx| window.dispatch_action(Box::new(Amend), cx),
+                                )
+                            })
+                            .toggleable_entry(
+                                "Signoff",
+                                signoff,
+                                IconPosition::Start,
+                                Some(Box::new(Signoff)),
+                                move |window, cx| window.dispatch_action(Box::new(Signoff), cx),
+                            )
+                    }))
+                }
             })
             .anchor(Corner::TopRight)
     }
@@ -3187,7 +3241,6 @@ 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 = head_commit.is_some();
 
         let footer = v_flex()
             .child(PanelRepoFooter::new(
@@ -3231,7 +3284,7 @@ impl GitPanel {
                                 h_flex()
                                     .gap_0p5()
                                     .children(enable_coauthors)
-                                    .child(self.render_commit_button(has_previous_commit, cx)),
+                                    .child(self.render_commit_button(cx)),
                             ),
                     )
                     .child(
@@ -3280,14 +3333,12 @@ impl GitPanel {
         Some(footer)
     }
 
-    fn render_commit_button(
-        &self,
-        has_previous_commit: bool,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
+    fn render_commit_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
         let (can_commit, tooltip) = self.configure_commit_button(cx);
         let title = self.commit_button_title();
         let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx);
+        let amend = self.amend_pending();
+        let signoff = self.signoff_enabled;
 
         div()
             .id("commit-wrapper")
@@ -3296,165 +3347,86 @@ impl GitPanel {
                     *hovered && !this.has_staged_changes() && !this.has_unstaged_conflicts();
                 cx.notify()
             }))
-            .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);
-                                }),
-                        )
-                        .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({
-                                    let git_panel = cx.weak_entity();
-                                    move |_, window, cx| {
-                                        telemetry::event!("Git Amended", source = "Git Panel");
-                                        git_panel
-                                            .update(cx, |git_panel, cx| {
-                                                git_panel.set_amend_pending(false, cx);
-                                                git_panel.commit_changes(
-                                                    CommitOptions { amend: true },
-                                                    window,
-                                                    cx,
-                                                );
-                                            })
-                                            .ok();
-                                    }
-                                }),
-                        )
-                }
-            })
-            .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({
-                            let git_panel = cx.weak_entity();
-                            move |_, window, cx| {
-                                telemetry::event!("Git Committed", source = "Git Panel");
-                                git_panel
-                                    .update(cx, |git_panel, cx| {
-                                        git_panel.commit_changes(
-                                            CommitOptions { amend: false },
-                                            window,
-                                            cx,
-                                        );
-                                    })
-                                    .ok();
-                            }
-                        })
-                        .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()),
-                            cx,
-                        )
-                        .into_any_element(),
-                    ))
-                })
-                .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)
-                                }
+            .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({
+                    let git_panel = cx.weak_entity();
+                    move |_, window, cx| {
+                        telemetry::event!("Git Committed", source = "Git Panel");
+                        git_panel
+                            .update(cx, |git_panel, cx| {
+                                git_panel.set_amend_pending(false, cx);
+                                git_panel.commit_changes(
+                                    CommitOptions { amend, signoff },
+                                    window,
+                                    cx,
+                                );
                             })
-                            .disabled(!can_commit || self.modal_open)
-                            .on_click({
-                                let git_panel = cx.weak_entity();
-                                move |_, window, cx| {
-                                    telemetry::event!("Git Committed", source = "Git Panel");
-                                    git_panel
-                                        .update(cx, |git_panel, cx| {
-                                            git_panel.commit_changes(
-                                                CommitOptions { amend: false },
-                                                window,
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
-                                }
-                            }),
-                    )
+                            .ok();
+                    }
                 })
-            })
+                .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),
+                                format!(
+                                    "git commit{}{}",
+                                    if amend { " --amend" } else { "" },
+                                    if signoff { " --signoff" } else { "" }
+                                ),
+                                &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()),
+                    cx,
+                )
+                .into_any_element(),
+            ))
     }
 
     fn render_pending_amend(&self, cx: &mut Context<Self>) -> impl IntoElement {
-        div()
-            .p_2()
+        h_flex()
+            .py_1p5()
+            .px_2()
+            .gap_1p5()
+            .justify_between()
             .border_t_1()
-            .border_color(cx.theme().colors().border)
+            .border_color(cx.theme().colors().border.opacity(0.8))
             .child(
-                Label::new(
-                    "This will update your most recent commit. Cancel to make a new one instead.",
-                )
-                .size(LabelSize::Small),
+                div()
+                    .flex_grow()
+                    .overflow_hidden()
+                    .max_w(relative(0.85))
+                    .child(
+                        Label::new("This will update your most recent commit.")
+                            .size(LabelSize::Small)
+                            .truncate(),
+                    ),
             )
+            .child(panel_button("Cancel").size(ButtonSize::Default).on_click(
+                cx.listener(|this, _, window, cx| this.toggle_amend_pending(&Amend, window, cx)),
+            ))
     }
 
     fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
@@ -4218,17 +4190,56 @@ impl GitPanel {
         cx.notify();
     }
 
+    pub fn toggle_amend_pending(
+        &mut self,
+        _: &Amend,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.set_amend_pending(!self.amend_pending, cx);
+        self.serialize(cx);
+    }
+
+    pub fn signoff_enabled(&self) -> bool {
+        self.signoff_enabled
+    }
+
+    pub fn set_signoff_enabled(&mut self, value: bool, cx: &mut Context<Self>) {
+        self.signoff_enabled = value;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    pub fn toggle_signoff_enabled(
+        &mut self,
+        _: &Signoff,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.set_signoff_enabled(!self.signoff_enabled, cx);
+    }
+
     pub async fn load(
         workspace: WeakEntity<Workspace>,
         mut cx: AsyncWindowContext,
     ) -> anyhow::Result<Entity<Self>> {
-        let serialized_panel = cx
-            .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&GIT_PANEL_KEY) })
-            .await
-            .context("loading git panel")
-            .log_err()
+        let serialized_panel = match workspace
+            .read_with(&cx, |workspace, _| Self::serialization_key(workspace))
+            .ok()
             .flatten()
-            .and_then(|panel| serde_json::from_str::<SerializedGitPanel>(&panel).log_err());
+        {
+            Some(serialization_key) => cx
+                .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
+                .await
+                .context("loading git panel")
+                .log_err()
+                .flatten()
+                .map(|panel| serde_json::from_str::<SerializedGitPanel>(&panel))
+                .transpose()
+                .log_err()
+                .flatten(),
+            None => None,
+        };
 
         workspace.update_in(&mut cx, |workspace, window, cx| {
             let panel = GitPanel::new(workspace, window, cx);
@@ -4236,6 +4247,8 @@ impl GitPanel {
             if let Some(serialized_panel) = serialized_panel {
                 panel.update(cx, |panel, cx| {
                     panel.width = serialized_panel.width;
+                    panel.amend_pending = serialized_panel.amend_pending;
+                    panel.signoff_enabled = serialized_panel.signoff_enabled;
                     cx.notify();
                 })
             }
@@ -4320,7 +4333,8 @@ impl Render for GitPanel {
                     .on_action(cx.listener(Self::stage_range))
                     .on_action(cx.listener(GitPanel::commit))
                     .on_action(cx.listener(GitPanel::amend))
-                    .on_action(cx.listener(GitPanel::cancel))
+                    .on_action(cx.listener(GitPanel::toggle_amend_pending))
+                    .on_action(cx.listener(GitPanel::toggle_signoff_enabled))
                     .on_action(cx.listener(Self::stage_all))
                     .on_action(cx.listener(Self::unstage_all))
                     .on_action(cx.listener(Self::stage_selected))

crates/project/src/git_store.rs 🔗

@@ -1738,6 +1738,7 @@ impl GitStore {
                     name.zip(email),
                     CommitOptions {
                         amend: options.amend,
+                        signoff: options.signoff,
                     },
                     cx,
                 )
@@ -3488,6 +3489,7 @@ impl Repository {
                             email: email.map(String::from),
                             options: Some(proto::commit::CommitOptions {
                                 amend: options.amend,
+                                signoff: options.signoff,
                             }),
                         })
                         .await