ZIm/crates/git/src/git.rs
Eric Cornelissen 1d72fa8e9e
git: Add ability to pass --signoff (#29874)
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.
2025-07-17 03:39:54 +00:00

199 lines
5.8 KiB
Rust

pub mod blame;
pub mod commit;
mod hosting_provider;
mod remote;
pub mod repository;
pub mod status;
pub use crate::hosting_provider::*;
pub use crate::remote::*;
use anyhow::{Context as _, Result};
pub use git2 as libgit;
use gpui::{Action, actions};
pub use repository::WORK_DIRECTORY_REPO_PATH;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore"));
pub static FSMONITOR_DAEMON: LazyLock<&'static OsStr> =
LazyLock::new(|| OsStr::new("fsmonitor--daemon"));
pub static LFS_DIR: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("lfs"));
pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
actions!(
git,
[
// per-hunk
/// Toggles the staged state of the hunk or status entry at cursor.
ToggleStaged,
/// Stage status entries between an anchor entry and the cursor.
StageRange,
/// Stages the current hunk and moves to the next one.
StageAndNext,
/// Unstages the current hunk and moves to the next one.
UnstageAndNext,
/// Restores the selected hunks to their original state.
#[action(deprecated_aliases = ["editor::RevertSelectedHunks"])]
Restore,
// per-file
/// Shows git blame information for the current file.
#[action(deprecated_aliases = ["editor::ToggleGitBlame"])]
Blame,
/// Stages the current file.
StageFile,
/// Unstages the current file.
UnstageFile,
// repo-wide
/// Stages all changes in the repository.
StageAll,
/// Unstages all changes in the repository.
UnstageAll,
/// Restores all tracked files to their last committed state.
RestoreTrackedFiles,
/// Moves all untracked files to trash.
TrashUntrackedFiles,
/// Undoes the last commit, keeping changes in the working directory.
Uncommit,
/// Pushes commits to the remote repository.
Push,
/// Pushes commits to a specific remote branch.
PushTo,
/// Force pushes commits to the remote repository.
ForcePush,
/// Pulls changes from the remote repository.
Pull,
/// Fetches changes from the remote repository.
Fetch,
/// Fetches changes from a specific remote.
FetchFrom,
/// Creates a new commit with staged changes.
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.
ExpandCommitEditor,
/// Generates a commit message using AI.
GenerateCommitMessage,
/// Initializes a new git repository.
Init,
/// Opens all modified files in the editor.
OpenModifiedFiles,
]
);
/// Restores a file to its last committed state, discarding local changes.
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]
#[serde(deny_unknown_fields)]
pub struct RestoreFile {
#[serde(default)]
pub skip_prompt: bool,
}
/// The length of a Git short SHA.
pub const SHORT_SHA_LENGTH: usize = 7;
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Oid(libgit::Oid);
impl Oid {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let oid = libgit::Oid::from_bytes(bytes).context("failed to parse bytes into git oid")?;
Ok(Self(oid))
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub(crate) fn is_zero(&self) -> bool {
self.0.is_zero()
}
/// Returns this [`Oid`] as a short SHA.
pub fn display_short(&self) -> String {
self.to_string().chars().take(SHORT_SHA_LENGTH).collect()
}
}
impl FromStr for Oid {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
libgit::Oid::from_str(s)
.context("parsing git oid")
.map(Self)
}
}
impl fmt::Debug for Oid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for Oid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl Serialize for Oid {
fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for Oid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse::<Oid>().map_err(serde::de::Error::custom)
}
}
impl Default for Oid {
fn default() -> Self {
Self(libgit::Oid::zero())
}
}
impl From<Oid> for u32 {
fn from(oid: Oid) -> Self {
let bytes = oid.0.as_bytes();
debug_assert!(bytes.len() > 4);
let mut u32_bytes: [u8; 4] = [0; 4];
u32_bytes.copy_from_slice(&bytes[..4]);
u32::from_ne_bytes(u32_bytes)
}
}
impl From<Oid> for usize {
fn from(oid: Oid) -> Self {
let bytes = oid.0.as_bytes();
debug_assert!(bytes.len() > 8);
let mut u64_bytes: [u8; 8] = [0; 8];
u64_bytes.copy_from_slice(&bytes[..8]);
u64::from_ne_bytes(u64_bytes) as usize
}
}