From e07d9a438b37fa6e41cff42630facfb653aa4012 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:49:20 -0400 Subject: [PATCH] git: Further extract gitlib2 dependencies (#58280) Extraction done from #53453 I removed the default Oid implementation we had and added support back for SHA264 back as well. I also removed the hex dependency and just added some of those functions we needed in house so we can avoid building yet another dependency Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- Cargo.lock | 3 - .../collab/tests/integration/editor_tests.rs | 13 +- crates/editor/src/editor_tests.rs | 13 +- crates/editor/src/git/blame.rs | 13 +- crates/fs/src/fake_git_repo.rs | 2 - crates/git/src/blame.rs | 16 +- crates/git/src/git.rs | 197 ++++++++++--- crates/git/src/repository.rs | 11 +- crates/project/Cargo.toml | 1 - .../tests/integration/project_tests.rs | 270 +++++++++++------- crates/remote_server/Cargo.toml | 3 +- crates/util/Cargo.toml | 4 +- crates/util/src/test.rs | 15 +- 13 files changed, 394 insertions(+), 167 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 334815a59a..03720dca11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14313,7 +14313,6 @@ dependencies = [ "fuzzy", "fuzzy_nucleo", "git", - "git2", "git_hosting_providers", "globset", "gpui", @@ -15527,7 +15526,6 @@ dependencies = [ "fs", "futures 0.3.32", "git", - "git2", "git_hosting_providers", "gpui", "gpui_platform", @@ -20387,7 +20385,6 @@ dependencies = [ "dunce", "futures 0.3.32", "futures-lite 1.13.0", - "git2", "globset", "gpui_util", "itertools 0.14.0", diff --git a/crates/collab/tests/integration/editor_tests.rs b/crates/collab/tests/integration/editor_tests.rs index 71040766db..e4e8fbfae0 100644 --- a/crates/collab/tests/integration/editor_tests.rs +++ b/crates/collab/tests/integration/editor_tests.rs @@ -6235,6 +6235,17 @@ fn blame_entry(sha: &str, range: Range) -> git::blame::BlameEntry { git::blame::BlameEntry { sha: sha.parse().unwrap(), range, - ..Default::default() + original_line_number: 0, + author: None, + author_mail: None, + author_time: None, + author_tz: None, + committer_name: None, + committer_email: None, + committer_time: None, + committer_tz: None, + summary: None, + previous: None, + filename: String::new(), } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e7d23db81b..e7f06bcacf 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -30142,7 +30142,18 @@ async fn test_hide_pending_blame_popover_when_modal_opens(cx: &mut TestAppContex &::git::blame::BlameEntry { sha: "1b1b1b".parse().unwrap(), range: 0..1, - ..Default::default() + original_line_number: 0, + author: None, + author_mail: None, + author_time: None, + author_tz: None, + committer_name: None, + committer_email: None, + committer_time: None, + committer_tz: None, + summary: None, + previous: None, + filename: String::new(), }, gpui::point(gpui::px(0.), gpui::px(0.)), false, diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index a78c37e66d..0e6ad1cb0e 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -1281,7 +1281,18 @@ mod tests { BlameEntry { sha: sha.parse().unwrap(), range, - ..Default::default() + original_line_number: 0, + author: None, + author_mail: None, + author_time: None, + author_tz: None, + committer_name: None, + committer_email: None, + committer_time: None, + committer_tz: None, + summary: None, + previous: None, + filename: String::new(), } } } diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 7635317aed..3a2c098098 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -161,8 +161,6 @@ impl FakeGitRepository { } impl GitRepository for FakeGitRepository { - fn reload_index(&self) {} - fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option> { let fut = self.with_state_async(false, move |state| { state diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 2261f52f87..b10a50942b 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -12,8 +12,6 @@ use time::UtcOffset; use time::macros::format_description; use util::command::Stdio; -pub use git2 as libgit; - #[derive(Debug, Clone, Default)] pub struct Blame { pub entries: Vec, @@ -92,7 +90,7 @@ async fn run_git_blame( Ok(String::from_utf8(output.stdout)?) } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BlameEntry { pub sha: Oid, @@ -151,7 +149,17 @@ impl BlameEntry { sha, range, original_line_number, - ..Default::default() + author: None, + author_mail: None, + author_time: None, + author_tz: None, + committer_name: None, + committer_email: None, + committer_time: None, + committer_tz: None, + summary: None, + previous: None, + filename: String::new(), }) } diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 73ba627575..e826e5c0c3 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -8,13 +8,12 @@ pub mod status; pub use crate::hosting_provider::*; pub use crate::remote::*; -use anyhow::{Context as _, Result}; -pub use git2 as libgit; +use anyhow::Result; use gpui::{Action, actions}; pub use repository::RemoteCommandOutput; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt; +use std::fmt::{self, Write as _}; use std::str::FromStr; pub const DOT_GIT: &str = ".git"; @@ -137,40 +136,115 @@ pub struct RestoreFile { /// The length of a Git short SHA. pub const SHORT_SHA_LENGTH: usize = 7; +const SHA1_BYTE_LENGTH: usize = 20; +const SHA256_BYTE_LENGTH: usize = 32; +const SHA1_HEX_LENGTH: usize = SHA1_BYTE_LENGTH * 2; +const SHA256_HEX_LENGTH: usize = SHA256_BYTE_LENGTH * 2; +const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef"; + #[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub struct Oid(libgit::Oid); +pub struct Oid { + bytes: [u8; SHA256_BYTE_LENGTH], + format: OidFormat, +} + +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +enum OidFormat { + Sha1, + Sha256, +} + +impl OidFormat { + fn byte_len(self) -> usize { + match self { + Self::Sha1 => SHA1_BYTE_LENGTH, + Self::Sha256 => SHA256_BYTE_LENGTH, + } + } + + fn hex_len(self) -> usize { + match self { + Self::Sha1 => SHA1_HEX_LENGTH, + Self::Sha256 => SHA256_HEX_LENGTH, + } + } +} impl Oid { pub fn from_bytes(bytes: &[u8]) -> Result { - let oid = libgit::Oid::from_bytes(bytes).context("failed to parse bytes into git oid")?; - Ok(Self(oid)) + let format = match bytes.len() { + SHA1_BYTE_LENGTH => OidFormat::Sha1, + SHA256_BYTE_LENGTH => OidFormat::Sha256, + len => { + anyhow::bail!( + "invalid git oid byte length: expected {SHA1_BYTE_LENGTH} for SHA-1 or {SHA256_BYTE_LENGTH} for SHA-256, got {len}" + ); + } + }; + + let mut oid_bytes = [0u8; SHA256_BYTE_LENGTH]; + oid_bytes[..bytes.len()].copy_from_slice(bytes); + Ok(Self { + bytes: oid_bytes, + format, + }) } #[cfg(any(test, feature = "test-support"))] pub fn random(rng: &mut impl rand::Rng) -> Self { - let mut bytes = [0; 20]; - rng.fill(&mut bytes); - Self::from_bytes(&bytes).unwrap() + let mut bytes = [0u8; SHA256_BYTE_LENGTH]; + rng.fill(&mut bytes[..SHA1_BYTE_LENGTH]); + Self { + bytes, + format: OidFormat::Sha1, + } } pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() + &self.bytes[..self.format.byte_len()] } pub(crate) fn is_zero(&self) -> bool { - self.0.is_zero() + self.as_bytes().iter().all(|byte| *byte == 0) } /// Returns this [`Oid`] as a short SHA. pub fn display_short(&self) -> String { - self.to_string().chars().take(SHORT_SHA_LENGTH).collect() + self.hex_string(SHORT_SHA_LENGTH) + } + + fn hex_string(&self, len: usize) -> String { + let mut string = String::with_capacity(len); + for index in 0..len { + string.push(self.hex_digit(index)); + } + string + } + + fn write_hex(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for index in 0..self.format.hex_len() { + f.write_char(self.hex_digit(index))?; + } + Ok(()) + } + + #[inline(always)] + fn hex_digit(&self, index: usize) -> char { + debug_assert!(index < self.format.hex_len()); + let byte = self.as_bytes()[index / 2]; + let nibble = if index & 1 == 0 { + byte >> 4 + } else { + byte & 0x0f + }; + char::from(HEX_DIGITS[nibble as usize]) } } impl TryFrom<&str> for Oid { type Error = anyhow::Error; - fn try_from(value: &str) -> std::prelude::v1::Result { + fn try_from(value: &str) -> std::result::Result { Oid::from_str(value) } } @@ -178,13 +252,38 @@ impl TryFrom<&str> for Oid { impl FromStr for Oid { type Err = anyhow::Error; - fn from_str(s: &str) -> std::prelude::v1::Result { - let oid = if s.len() == 64 { - libgit::Oid::from_str_ext(s, libgit::ObjectFormat::Sha256) - } else { - libgit::Oid::from_str_ext(s, libgit::ObjectFormat::Sha1) + fn from_str(s: &str) -> std::result::Result { + let format = match s.len() { + 1..=SHA1_HEX_LENGTH => OidFormat::Sha1, + SHA256_HEX_LENGTH => OidFormat::Sha256, + len => { + anyhow::bail!( + "invalid git oid hex length: expected 1..={SHA1_HEX_LENGTH} for SHA-1 or {SHA256_HEX_LENGTH} for SHA-256, got {len}" + ); + } }; - oid.context("parsing git oid").map(Self) + + let mut bytes = [0u8; SHA256_BYTE_LENGTH]; + for (index, byte) in s.bytes().enumerate() { + let digit = decode_hex_digit(byte) + .ok_or_else(|| anyhow::anyhow!("invalid hex digit at byte {index} for git oid"))?; + if index % 2 == 0 { + bytes[index / 2] = digit << 4; + } else { + bytes[index / 2] |= digit; + } + } + + Ok(Self { bytes, format }) + } +} + +fn decode_hex_digit(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, } } @@ -196,21 +295,21 @@ impl fmt::Debug for Oid { impl fmt::Display for Oid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + self.write_hex(f) } } impl Serialize for Oid { - fn serialize(&self, serializer: S) -> std::prelude::v1::Result + fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { - serializer.serialize_str(&self.0.to_string()) + serializer.serialize_str(&self.hex_string(self.format.hex_len())) } } impl<'de> Deserialize<'de> for Oid { - fn deserialize(deserializer: D) -> Result + fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { @@ -219,36 +318,52 @@ impl<'de> Deserialize<'de> for Oid { } } -impl Default for Oid { - fn default() -> Self { - Self(libgit::Oid::ZERO_SHA1) - } -} - impl From 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]); - + let mut u32_bytes = [0u8; 4]; + u32_bytes.copy_from_slice(&oid.as_bytes()[..4]); u32::from_ne_bytes(u32_bytes) } } impl From 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]); - + let mut u64_bytes = [0u8; 8]; + u64_bytes.copy_from_slice(&oid.as_bytes()[..8]); u64::from_ne_bytes(u64_bytes) as usize } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_short_sha1_oid_by_padding_trailing_nibbles() { + let oid = "abc1234".parse::().expect("failed to parse oid"); + + assert_eq!(oid.as_bytes().len(), SHA1_BYTE_LENGTH); + assert_eq!(oid.display_short(), "abc1234"); + assert_eq!(oid.to_string(), "abc1234000000000000000000000000000000000"); + } + + #[test] + fn parses_full_sha256_oid() { + let sha = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + let oid = sha.parse::().expect("failed to parse oid"); + + assert_eq!(oid.as_bytes().len(), SHA256_BYTE_LENGTH); + assert_eq!(oid.to_string(), sha); + } + + #[test] + fn rejects_invalid_oid_lengths() { + assert!("".parse::().is_err()); + assert!("a".repeat(SHA1_HEX_LENGTH + 1).parse::().is_err()); + assert!("a".repeat(SHA256_HEX_LENGTH - 1).parse::().is_err()); + } +} + #[repr(i32)] #[derive(Copy, Clone, Debug)] pub enum RunHook { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 7da5ae4490..b58758cff1 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -774,8 +774,6 @@ pub fn delete_branch_flag(is_remote_tracking_ref: bool, force: bool) -> &'static } pub trait GitRepository: Send + Sync { - fn reload_index(&self); - /// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path. /// /// Also returns `None` for symlinks. @@ -1262,12 +1260,6 @@ pub async fn get_git_committer(cx: &AsyncApp) -> GitCommitter { } impl GitRepository for RealGitRepository { - fn reload_index(&self) { - if let Ok(mut index) = self.repository.lock().index() { - _ = index.read(false); - } - } - fn path(&self) -> PathBuf { let repo = self.repository.lock(); repo.path().into() @@ -1564,7 +1556,8 @@ impl GitRepository for RealGitRepository { self.executor .spawn(async move { let repo = repo.lock(); - let content = repo.find_blob(oid.0)?.content().to_owned(); + let oid = git2::Oid::from_bytes(oid.as_bytes())?; + let content = repo.find_blob(oid)?.content().to_owned(); Ok(String::from_utf8(content)?) }) .boxed() diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 3a27bf4de6..df57a8fdf5 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -114,7 +114,6 @@ context_server = { workspace = true, features = ["test-support"] } buffer_diff = { workspace = true, features = ["test-support"] } dap = { workspace = true, features = ["test-support"] } fs = { workspace = true, features = ["test-support"] } -git2.workspace = true gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index daaaa0bd2c..9ad334ece6 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -33,7 +33,6 @@ use git::{ repository::{RepoPath, repo_path}, status::{DiffStat, FileStatus, StatusCode, TrackedStatus}, }; -use git2::RepositoryInitOptions; use gpui::{ App, AppContext, BackgroundExecutor, BorrowAppContext, Entity, FutureExt, SharedString, Task, TestAppContext, UpdateGlobal, @@ -77,6 +76,7 @@ use std::{ num::NonZeroU32, ops::Range, path::{Path, PathBuf}, + process::Command, rc::Rc, str::FromStr, sync::{Arc, OnceLock, atomic}, @@ -11101,7 +11101,8 @@ async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) { })); let root_path = root.path(); - let repo = git_init(&root_path.join("project")); + let work_dir = &root_path.join("project"); + let repo = git_init(work_dir); git_add("a.txt", &repo); git_commit("init", &repo); @@ -11119,25 +11120,21 @@ async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) { }); git_branch("other-branch", &repo); - git_checkout("refs/heads/other-branch", &repo); + git_checkout("other-branch", &repo); std::fs::write(root_path.join("project/a.txt"), "A").unwrap(); git_add("a.txt", &repo); git_commit("capitalize", &repo); - let commit = repo - .head() - .expect("Failed to get HEAD") - .peel_to_commit() - .expect("HEAD is not a commit"); - git_checkout("refs/heads/main", &repo); + let commit = git_rev_parse("HEAD", &repo); + git_checkout("main", &repo); std::fs::write(root_path.join("project/a.txt"), "b").unwrap(); git_add("a.txt", &repo); git_commit("improve letter", &repo); - git_cherry_pick(&commit, &repo); + git_cherry_pick_expect_conflict(&commit, &repo); std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD")) .expect("No CHERRY_PICK_HEAD"); pretty_assertions::assert_eq!( git_status(&repo), - collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)]) + collections::HashMap::from_iter([("a.txt".to_owned(), GIT_STATUS_CONFLICTED.to_owned())]) ); tree.flush_fs_events(cx).await; project @@ -11364,8 +11361,7 @@ async fn test_file_status(cx: &mut gpui::TestAppContext) { // Set up git repository before creating the worktree. let work_dir = root.path().join("project"); - let mut repo = git_init(work_dir.as_path()); - repo.add_ignore_rule(IGNORE_RULE).unwrap(); + let repo = git_init(work_dir.as_path()); git_add(A_TXT, &repo); git_add(E_TXT, &repo); git_add(DOTGITIGNORE, &repo); @@ -11452,7 +11448,7 @@ async fn test_file_status(cx: &mut gpui::TestAppContext) { // Modify files in the working copy and perform git operations on other files. git_reset(0, &repo); git_remove_index(Path::new(B_TXT), &repo); - git_stash(&mut repo); + git_stash(&repo); std::fs::write(work_dir.join(E_TXT), "eeee").unwrap(); std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap(); tree.flush_fs_events(cx).await; @@ -11576,7 +11572,6 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) { // Set up git repository before creating the worktree. let work_dir = root.path().join("project"); let repo = git_init(work_dir.as_path()); - repo.add_ignore_rule(IGNORE_RULE).unwrap(); git_add("src/main.rs", &repo); git_add(".gitignore", &repo); git_commit("Initial commit", &repo); @@ -12740,111 +12735,192 @@ fn assert_entry_git_state( ); } -#[track_caller] -fn git_init(path: &Path) -> git2::Repository { - let mut init_opts = RepositoryInitOptions::new(); - init_opts.initial_head("main"); - git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository") +#[cfg(any())] +const GIT_STATUS_CONFLICTED: &str = "UU"; + +#[allow(clippy::disallowed_methods)] +fn git_cmd(work_dir: &Path) -> Command { + let mut cmd = Command::new("git"); + cmd.current_dir(work_dir) + .env("GIT_CONFIG_GLOBAL", "") + .env("GIT_CONFIG_SYSTEM", "") + .env("GIT_AUTHOR_NAME", "test") + .env("GIT_AUTHOR_EMAIL", "test@zed.dev") + .env("GIT_COMMITTER_NAME", "test") + .env("GIT_COMMITTER_EMAIL", "test@zed.dev"); + cmd } +#[allow(clippy::disallowed_methods)] #[track_caller] -fn git_add>(path: P, repo: &git2::Repository) { - let path = path.as_ref(); - let mut index = repo.index().expect("Failed to get index"); - index.add_path(path).expect("Failed to add file"); - index.write().expect("Failed to write index"); +fn git_init(path: &Path) -> PathBuf { + let output = git_cmd(path) + .args(["init", "-b", "main"]) + .output() + .expect("Failed to run git init"); + assert!( + output.status.success(), + "git init failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + path.to_path_buf() } +#[allow(clippy::disallowed_methods)] #[track_caller] -fn git_remove_index(path: &Path, repo: &git2::Repository) { - let mut index = repo.index().expect("Failed to get index"); - index.remove_path(path).expect("Failed to add file"); - index.write().expect("Failed to write index"); +fn git_add>(path: P, work_dir: &Path) { + let output = git_cmd(work_dir) + .args(["add"]) + .arg(path.as_ref()) + .output() + .expect("Failed to run git add"); + assert!( + output.status.success(), + "git add failed: {}", + String::from_utf8_lossy(&output.stderr) + ); } +#[allow(clippy::disallowed_methods)] #[track_caller] -fn git_commit(msg: &'static str, repo: &git2::Repository) { - use git2::Signature; +fn git_remove_index(path: &Path, work_dir: &Path) { + let output = git_cmd(work_dir) + .args(["rm", "--cached"]) + .arg(path) + .output() + .expect("Failed to run git rm"); + assert!( + output.status.success(), + "git rm --cached failed: {}", + String::from_utf8_lossy(&output.stderr) + ); +} - let signature = Signature::now("test", "test@zed.dev").unwrap(); - let oid = repo.index().unwrap().write_tree().unwrap(); - let tree = repo.find_tree(oid).unwrap(); - if let Ok(head) = repo.head() { - let parent_obj = head.peel(git2::ObjectType::Commit).unwrap(); +#[allow(clippy::disallowed_methods)] +#[track_caller] +fn git_commit(msg: &str, work_dir: &Path) { + let output = git_cmd(work_dir) + .args(["commit", "-m", msg]) + .output() + .expect("Failed to run git commit"); + assert!( + output.status.success(), + "git commit failed: {}", + String::from_utf8_lossy(&output.stderr) + ); +} - let parent_commit = parent_obj.as_commit().unwrap(); +#[allow(clippy::disallowed_methods)] +#[track_caller] +fn git_stash(work_dir: &Path) { + let output = git_cmd(work_dir) + .args(["stash"]) + .output() + .expect("Failed to run git stash"); + assert!( + output.status.success(), + "git stash failed: {}", + String::from_utf8_lossy(&output.stderr) + ); +} - repo.commit( - Some("HEAD"), - &signature, - &signature, - msg, - &tree, - &[parent_commit], - ) - .expect("Failed to commit with parent"); - } else { - repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[]) - .expect("Failed to commit"); - } +#[allow(clippy::disallowed_methods)] +#[track_caller] +fn git_reset(offset: usize, work_dir: &Path) { + let target = format!("HEAD~{}", offset + 1); + let output = git_cmd(work_dir) + .args(["reset", "--soft", &target]) + .output() + .expect("Failed to run git reset"); + assert!( + output.status.success(), + "git reset failed: {}", + String::from_utf8_lossy(&output.stderr) + ); } #[cfg(any())] +#[allow(clippy::disallowed_methods)] #[track_caller] -fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) { - repo.cherrypick(commit, None).expect("Failed to cherrypick"); +fn git_branch(name: &str, work_dir: &Path) { + let output = git_cmd(work_dir) + .args(["branch", name]) + .output() + .expect("Failed to run git branch"); + assert!( + output.status.success(), + "git branch failed: {}", + String::from_utf8_lossy(&output.stderr) + ); } +#[cfg(any())] +#[allow(clippy::disallowed_methods)] #[track_caller] -fn git_stash(repo: &mut git2::Repository) { - use git2::Signature; - - let signature = Signature::now("test", "test@zed.dev").unwrap(); - repo.stash_save(&signature, "N/A", None) - .expect("Failed to stash"); +fn git_checkout(name: &str, work_dir: &Path) { + let output = git_cmd(work_dir) + .args(["checkout", name]) + .output() + .expect("Failed to run git checkout"); + assert!( + output.status.success(), + "git checkout failed: {}", + String::from_utf8_lossy(&output.stderr) + ); } +#[cfg(any())] +#[allow(clippy::disallowed_methods)] #[track_caller] -fn git_reset(offset: usize, repo: &git2::Repository) { - let head = repo.head().expect("Couldn't get repo head"); - let object = head.peel(git2::ObjectType::Commit).unwrap(); - let commit = object.as_commit().unwrap(); - let new_head = commit - .parents() - .inspect(|parnet| { - let _ = parnet.message(); +fn git_rev_parse(rev: &str, work_dir: &Path) -> String { + let output = git_cmd(work_dir) + .args(["rev-parse", rev]) + .output() + .expect("Failed to run git rev-parse"); + assert!( + output.status.success(), + "git rev-parse failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8(output.stdout).unwrap().trim().to_string() +} + +#[cfg(any())] +#[allow(clippy::disallowed_methods)] +#[track_caller] +fn git_cherry_pick_expect_conflict(commit: &str, work_dir: &Path) { + let output = git_cmd(work_dir) + .args(["cherry-pick", "--no-commit", commit]) + .output() + .expect("Failed to run git cherry-pick"); + assert!( + !output.status.success(), + "git cherry-pick unexpectedly succeeded" + ); +} + +#[cfg(any())] +#[allow(clippy::disallowed_methods)] +#[track_caller] +fn git_status(work_dir: &Path) -> collections::HashMap { + let output = git_cmd(work_dir) + .args(["status", "--porcelain=v1"]) + .output() + .expect("Failed to run git status"); + assert!( + output.status.success(), + "git status failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + stdout + .lines() + .filter(|line| !line.is_empty()) + .map(|line| { + let status = line[..2].to_string(); + let path = line[3..].to_string(); + (path, status) }) - .nth(offset) - .expect("Not enough history"); - repo.reset(new_head.as_object(), git2::ResetType::Soft, None) - .expect("Could not reset"); -} - -#[cfg(any())] -#[track_caller] -fn git_branch(name: &str, repo: &git2::Repository) { - let head = repo - .head() - .expect("Couldn't get repo head") - .peel_to_commit() - .expect("HEAD is not a commit"); - repo.branch(name, &head, false).expect("Failed to commit"); -} - -#[cfg(any())] -#[track_caller] -fn git_checkout(name: &str, repo: &git2::Repository) { - repo.set_head(name).expect("Failed to set head"); - repo.checkout_head(None).expect("Failed to check out head"); -} - -#[cfg(any())] -#[track_caller] -fn git_status(repo: &git2::Repository) -> collections::HashMap { - repo.statuses(None) - .unwrap() - .iter() - .map(|status| (status.path().unwrap().to_string(), status.status())) .collect() } diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index db879f1ddf..05bebd25fd 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -39,7 +39,6 @@ fs.workspace = true futures.workspace = true git.workspace = true git_hosting_providers.workspace = true -git2 = { workspace = true, features = ["vendored-libgit2"] } gpui.workspace = true gpui_platform.workspace = true gpui_tokio.workspace = true @@ -115,4 +114,4 @@ cargo_toml.workspace = true toml.workspace = true [package.metadata.cargo-machete] -ignored = ["git2", "rust-embed", "paths"] +ignored = ["rust-embed", "paths"] diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 4f317e79e0..13f4081122 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -15,7 +15,7 @@ path = "src/util.rs" doctest = true [features] -test-support = ["git2", "rand", "util_macros"] +test-support = ["rand", "util_macros"] [dependencies] anyhow.workspace = true @@ -46,7 +46,6 @@ gpui_util.workspace = true [target.'cfg(not(target_family = "wasm"))'.dependencies] smol.workspace = true which.workspace = true -git2 = { workspace = true, optional = true } async-fs.workspace = true walkdir.workspace = true dirs.workspace = true @@ -63,7 +62,6 @@ mach2.workspace = true tendril = "0.4.3" [dev-dependencies] -git2.workspace = true rand.workspace = true util_macros.workspace = true pretty_assertions.workspace = true diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index 717754e333..68688ee2e2 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -4,7 +4,6 @@ mod marked_text; pub use assertions::*; pub use marked_text::*; -use git2; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use tempfile::TempDir; @@ -44,8 +43,20 @@ fn write_tree(path: &Path, tree: serde_json::Value) { fs::create_dir(&path).unwrap(); #[cfg(not(target_family = "wasm"))] + #[allow(clippy::disallowed_methods)] if path.file_name() == Some(OsStr::new(".git")) { - git2::Repository::init(path.parent().unwrap()).unwrap(); + let output = std::process::Command::new("git") + .args(["init", "-b", "main"]) + .current_dir(path.parent().unwrap()) + .env("GIT_CONFIG_GLOBAL", "") + .env("GIT_CONFIG_SYSTEM", "") + .output() + .expect("failed to init git repo"); + assert!( + output.status.success(), + "git init failed: {}", + String::from_utf8_lossy(&output.stderr) + ); } write_tree(&path, contents);