From 51cf919393aa1de95cb1489d56868832904f5302 Mon Sep 17 00:00:00 2001 From: Mattias Granlund Date: Sun, 5 Jan 2025 16:48:59 +0100 Subject: [PATCH 1/4] Fix running of pre-commit hooks by staging selected files --- .../desktop/src/lib/shared/InfoMessage.svelte | 1 + .../gitbutler-branch-actions/src/actions.rs | 8 +- .../gitbutler-branch-actions/src/virtual.rs | 234 ++++++++++++------ crates/gitbutler-diff/src/diff.rs | 7 +- crates/gitbutler-diff/src/write.rs | 5 +- 5 files changed, 178 insertions(+), 77 deletions(-) diff --git a/apps/desktop/src/lib/shared/InfoMessage.svelte b/apps/desktop/src/lib/shared/InfoMessage.svelte index 12385ce38a..ee98c1ca88 100644 --- a/apps/desktop/src/lib/shared/InfoMessage.svelte +++ b/apps/desktop/src/lib/shared/InfoMessage.svelte @@ -218,6 +218,7 @@ color: var(--clr-scale-err-10); border-radius: var(--radius-s); font-size: 12px; + white-space: pre; /* scrollbar */ &::-webkit-scrollbar { diff --git a/crates/gitbutler-branch-actions/src/actions.rs b/crates/gitbutler-branch-actions/src/actions.rs index 68e8436734..6d89217cd6 100644 --- a/crates/gitbutler-branch-actions/src/actions.rs +++ b/crates/gitbutler-branch-actions/src/actions.rs @@ -2,7 +2,7 @@ use super::r#virtual as vbranch; use crate::branch_upstream_integration; use crate::branch_upstream_integration::IntegrationStrategy; use crate::move_commits; -use crate::r#virtual::StackListResult; +use crate::r#virtual::{unstage_all, StackListResult}; use crate::reorder::{self, StackOrder}; use crate::upstream_integration::{ self, BaseBranchResolution, BaseBranchResolutionApproach, Resolution, StackStatuses, @@ -47,6 +47,12 @@ pub fn create_commit( let mut guard = project.exclusive_worktree_access(); let snapshot_tree = ctx.project().prepare_snapshot(guard.read_permission()); let result = vbranch::commit(&ctx, stack_id, message, ownership, run_hooks).map_err(Into::into); + + if run_hooks && result.is_err() { + // If commit hooks fail then files will still be staged. + unstage_all(&ctx)? + } + let _ = snapshot_tree.and_then(|snapshot_tree| { ctx.project().snapshot_commit_creation( snapshot_tree, diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index 16b1d22b5e..b2298d7c63 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -12,13 +12,14 @@ use crate::{ }; use anyhow::{anyhow, bail, Context, Result}; use bstr::{BString, ByteSlice}; +use git2::{ApplyLocation, ApplyOptions, Repository, ResetType}; use git2_hooks::HookResult; use gitbutler_branch::BranchUpdateRequest; use gitbutler_branch::{dedup, dedup_fmt}; use gitbutler_cherry_pick::RepositoryExt as _; use gitbutler_command_context::CommandContext; use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders}; -use gitbutler_diff::{trees, GitHunk, Hunk}; +use gitbutler_diff::{trees, ChangeType, GitHunk, Hunk}; use gitbutler_error::error::Code; use gitbutler_hunk_dependency::RangeCalculationError; use gitbutler_operating_modes::assure_open_workspace_mode; @@ -732,44 +733,6 @@ pub fn commit( ownership: Option<&BranchOwnershipClaims>, run_hooks: bool, ) -> Result { - let mut message_buffer = message.to_owned(); - - fn join_output<'a>(stdout: &'a str, stderr: &'a str) -> Cow<'a, str> { - let stdout = stdout.trim(); - if stdout.is_empty() { - stderr.trim().into() - } else { - stdout.into() - } - } - - if run_hooks { - let hook_result = - git2_hooks::hooks_commit_msg(ctx.repo(), Some(&["../.husky"]), &mut message_buffer) - .context("failed to run hook") - .context(Code::CommitHookFailed)?; - - if let HookResult::RunNotSuccessful { stdout, stderr, .. } = &hook_result { - return Err( - anyhow!("commit-msg hook rejected: {}", join_output(stdout, stderr)) - .context(Code::CommitHookFailed), - ); - } - - let hook_result = git2_hooks::hooks_pre_commit(ctx.repo(), Some(&["../.husky"])) - .context("failed to run hook") - .context(Code::CommitHookFailed)?; - - if let HookResult::RunNotSuccessful { stdout, stderr, .. } = &hook_result { - return Err( - anyhow!("commit hook rejected: {}", join_output(stdout, stderr)) - .context(Code::CommitHookFailed), - ); - } - } - - let message = &message_buffer; - // get the files to commit let diffs = gitbutler_diff::workdir(ctx.repo(), get_workspace_head(ctx)?)?; let statuses = get_applied_status_cached(ctx, None, &diffs) @@ -781,49 +744,69 @@ pub fn commit( .find(|(stack, _)| stack.id == stack_id) .with_context(|| format!("stack {stack_id} not found"))?; - update_conflict_markers(ctx, &diffs).context(Code::CommitMergeConflictFailure)?; - - ctx.assure_unconflicted() - .context(Code::CommitMergeConflictFailure)?; - - let tree_oid = if let Some(ownership) = ownership { - let files = files.into_iter().filter_map(|file| { - let hunks = file - .hunks - .into_iter() - .filter(|hunk| { - let hunk: GitHunk = hunk.clone().into(); - ownership - .claims - .iter() - .find(|f| f.file_path.eq(&file.path)) - .map_or(false, |f| { - f.hunks.iter().any(|h| { - h.start == hunk.new_start - && h.end == hunk.new_start + hunk.new_lines + let selected_files = if let Some(ownership) = ownership { + files + .clone() + .into_iter() + .filter_map(|file| { + let hunks = file + .hunks + .into_iter() + .filter(|hunk| { + let hunk: GitHunk = hunk.clone().into(); + ownership + .claims + .iter() + .find(|f| f.file_path.eq(&file.path)) + .map_or(false, |f| { + f.hunks.iter().any(|h| { + h.start == hunk.new_start + && h.end == hunk.new_start + hunk.new_lines + }) }) - }) - }) - .collect::>(); - if hunks.is_empty() { - None - } else { - Some((file.path, hunks)) - } - }); - gitbutler_diff::write::hunks_onto_commit(ctx, branch.head(), files)? + }) + .collect::>(); + if !hunks.is_empty() { + Some((file.path, hunks)) + } else { + None + } + }) + .collect::)>>() } else { - let files = files + files + .clone() .into_iter() .map(|file| (file.path, file.hunks)) - .collect::)>>(); - gitbutler_diff::write::hunks_onto_commit(ctx, branch.head(), files)? + .collect::)>>() }; + let final_message = if run_hooks { + run_message_hook(ctx, message.to_owned())? + } else { + message.to_owned() + }; + + if run_hooks { + stage_files(ctx, &selected_files)?; + let result = run_pre_commit_hook(ctx); + if result.is_err() { + unstage_all(ctx)?; + result?; + } + } + + update_conflict_markers(ctx, &diffs).context(Code::CommitMergeConflictFailure)?; + + ctx.assure_unconflicted() + .context(Code::CommitMergeConflictFailure)?; + let git_repository = ctx.repo(); let parent_commit = git_repository .find_commit(branch.head()) .context(format!("failed to find commit {:?}", branch.head()))?; + + let tree_oid = gitbutler_diff::write::hunks_onto_commit(ctx, branch.head(), selected_files)?; let tree = git_repository .find_tree(tree_oid) .context(format!("failed to find tree {:?}", tree_oid))?; @@ -838,13 +821,18 @@ pub fn commit( let merge_parent = git_repository .find_commit(merge_parent) .context(format!("failed to find merge parent {:?}", merge_parent))?; - let commit_oid = ctx.commit(message, &tree, &[&parent_commit, &merge_parent], None)?; + let commit_oid = ctx.commit( + &final_message, + &tree, + &[&parent_commit, &merge_parent], + None, + )?; conflicts::clear(ctx) .context("failed to clear conflicts") .context(Code::CommitMergeConflictFailure)?; commit_oid } - None => ctx.commit(message, &tree, &[&parent_commit], None)?, + None => ctx.commit(&final_message, &tree, &[&parent_commit], None)?, }; if run_hooks { @@ -862,6 +850,104 @@ pub fn commit( Ok(commit_oid) } +fn join_output<'a>(stdout: &'a str, stderr: &'a str) -> Cow<'a, str> { + let stdout = stdout.trim(); + if stdout.is_empty() { + stderr.trim().into() + } else { + stdout.into() + } +} + +fn run_message_hook(ctx: &CommandContext, mut message: String) -> Result { + let hook_result = git2_hooks::hooks_commit_msg(ctx.repo(), Some(&["../.husky"]), &mut message) + .context("failed to run hook") + .context(Code::CommitHookFailed)?; + + if let HookResult::RunNotSuccessful { stdout, stderr, .. } = &hook_result { + return Err( + anyhow!("commit-msg hook rejected: {}", join_output(stdout, stderr)) + .context(Code::CommitHookFailed), + ); + } + Ok(message) +} + +fn run_pre_commit_hook(ctx: &CommandContext) -> Result<()> { + let hook_result = git2_hooks::hooks_pre_commit(ctx.repo(), Some(&["../.husky"])) + .context("failed to run hook") + .context(Code::CommitHookFailed)?; + + if let HookResult::RunNotSuccessful { stdout, stderr, .. } = &hook_result { + return Err( + anyhow!("commit hook rejected: {}", join_output(stdout, stderr)) + .context(Code::CommitHookFailed), + ); + } + Ok(()) +} + +pub(crate) fn unstage_all(ctx: &CommandContext) -> Result<()> { + let repo = ctx.repo(); + // Get the HEAD commit (current commit) + let head_commit = repo.head()?.peel_to_commit()?; + // Reset the index to match the HEAD commit + repo.reset(head_commit.as_object(), ResetType::Mixed, None)?; + Ok(()) +} + +fn diff_workdir_to_index<'a>(repo: &'a Repository, path: &PathBuf) -> Result> { + let index = repo.index()?; + let mut diff_opts = git2::DiffOptions::new(); + diff_opts + .recurse_untracked_dirs(true) + .include_untracked(true) + .show_binary(true) + .show_untracked_content(true) + .ignore_submodules(true) + .context_lines(3) + .pathspec(path); + Ok(repo.diff_index_to_workdir(Some(&index), Some(&mut diff_opts))?) +} + +fn stage_file(ctx: &CommandContext, path: &PathBuf, hunks: &Vec) -> Result<()> { + let repo = ctx.repo(); + let mut index = repo.index()?; + if hunks.iter().any(|h| h.change_type == ChangeType::Untracked) { + index.add_path(path)?; + index.write()?; + return Ok(()); + } + + let mut apply_opts = ApplyOptions::new(); + apply_opts.hunk_callback(|cb_hunk| { + cb_hunk.map_or(false, |cb_hunk| { + for hunk in hunks { + if hunk.start == cb_hunk.new_start() + && hunk.end == cb_hunk.new_start() + cb_hunk.new_lines() + { + return true; + } + } + false + }) + }); + + let diff = diff_workdir_to_index(repo, path)?; + repo.apply(&diff, ApplyLocation::Index, Some(&mut apply_opts))?; + Ok(()) +} + +fn stage_files( + ctx: &CommandContext, + files_to_stage: &Vec<(PathBuf, Vec)>, +) -> Result<()> { + for (path_to_stage, hunks_to_stage) in files_to_stage { + stage_file(ctx, path_to_stage, hunks_to_stage)?; + } + Ok(()) +} + pub(crate) fn push( ctx: &CommandContext, stack_id: StackId, diff --git a/crates/gitbutler-diff/src/diff.rs b/crates/gitbutler-diff/src/diff.rs index be88408b3e..5fa0f6791f 100644 --- a/crates/gitbutler-diff/src/diff.rs +++ b/crates/gitbutler-diff/src/diff.rs @@ -16,6 +16,8 @@ pub type DiffByPathMap = HashMap; pub enum ChangeType { /// Entry does not exist in old version Added, + /// Entry is untracked item in workdir + Untracked, /// Entry does not exist in new version Deleted, /// Entry content changed between old and new @@ -26,7 +28,8 @@ impl From for ChangeType { use git2::Delta as D; use ChangeType as C; match v { - D::Untracked | D::Added => C::Added, + D::Added => C::Added, + D::Untracked => C::Untracked, D::Modified | D::Unmodified | D::Renamed @@ -609,6 +612,7 @@ fn reverse_lines( pub fn reverse_hunk(hunk: &GitHunk) -> Option { let new_change_type = match hunk.change_type { ChangeType::Added => ChangeType::Deleted, + ChangeType::Untracked => ChangeType::Deleted, ChangeType::Deleted => ChangeType::Added, ChangeType::Modified => ChangeType::Modified, }; @@ -635,6 +639,7 @@ pub fn reverse_hunk_lines( lines: Vec<(Option, Option)>, ) -> Option { let new_change_type = match hunk.change_type { + ChangeType::Untracked => ChangeType::Deleted, ChangeType::Added => ChangeType::Deleted, ChangeType::Deleted => ChangeType::Added, ChangeType::Modified => ChangeType::Modified, diff --git a/crates/gitbutler-diff/src/write.rs b/crates/gitbutler-diff/src/write.rs index 7d3b804e19..468627f72b 100644 --- a/crates/gitbutler-diff/src/write.rs +++ b/crates/gitbutler-diff/src/write.rs @@ -195,7 +195,10 @@ where // upsert into the builder builder.upsert(rel_path, new_blob_oid, filemode); } else if !full_path_exists - && discard_hunk.map_or(false, |hunk| hunk.change_type == crate::ChangeType::Added) + && discard_hunk.map_or(false, |hunk| { + hunk.change_type == crate::ChangeType::Added + || hunk.change_type == crate::ChangeType::Untracked + }) { // File was deleted but now that hunk is being discarded with an inversed hunk let mut all_diffs = BString::default(); From 72c9b83e39696757e6893af0a51cf7e153fff5df Mon Sep 17 00:00:00 2001 From: Mattias Granlund Date: Mon, 6 Jan 2025 22:14:01 +0100 Subject: [PATCH 2/4] Move hooks and staging code into gitbutler-repo --- Cargo.lock | 2 + .../gitbutler-branch-actions/src/actions.rs | 5 +- .../gitbutler-branch-actions/src/virtual.rs | 198 ++++-------------- crates/gitbutler-repo/Cargo.toml | 2 + crates/gitbutler-repo/src/hooks.rs | 50 +++++ crates/gitbutler-repo/src/lib.rs | 2 + crates/gitbutler-repo/src/staging.rs | 67 ++++++ 7 files changed, 169 insertions(+), 157 deletions(-) create mode 100644 crates/gitbutler-repo/src/hooks.rs create mode 100644 crates/gitbutler-repo/src/staging.rs diff --git a/Cargo.lock b/Cargo.lock index 5697687a02..41cd8179f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2649,10 +2649,12 @@ dependencies = [ "base64 0.22.1", "bstr", "git2", + "git2-hooks", "gitbutler-cherry-pick", "gitbutler-command-context", "gitbutler-commit", "gitbutler-config", + "gitbutler-diff", "gitbutler-error", "gitbutler-oxidize", "gitbutler-project", diff --git a/crates/gitbutler-branch-actions/src/actions.rs b/crates/gitbutler-branch-actions/src/actions.rs index 6d89217cd6..f8f6adce11 100644 --- a/crates/gitbutler-branch-actions/src/actions.rs +++ b/crates/gitbutler-branch-actions/src/actions.rs @@ -2,7 +2,7 @@ use super::r#virtual as vbranch; use crate::branch_upstream_integration; use crate::branch_upstream_integration::IntegrationStrategy; use crate::move_commits; -use crate::r#virtual::{unstage_all, StackListResult}; +use crate::r#virtual::StackListResult; use crate::reorder::{self, StackOrder}; use crate::upstream_integration::{ self, BaseBranchResolution, BaseBranchResolutionApproach, Resolution, StackStatuses, @@ -29,6 +29,7 @@ use gitbutler_oplog::{ }; use gitbutler_project::{FetchResult, Project}; use gitbutler_reference::{ReferenceName, Refname, RemoteRefname}; +use gitbutler_repo::staging; use gitbutler_repo::RepositoryExt; use gitbutler_repo_actions::RepoActionsExt; use gitbutler_stack::{BranchOwnershipClaims, StackId}; @@ -50,7 +51,7 @@ pub fn create_commit( if run_hooks && result.is_err() { // If commit hooks fail then files will still be staged. - unstage_all(&ctx)? + staging::unstage_all(&ctx)? } let _ = snapshot_tree.and_then(|snapshot_tree| { diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index b2298d7c63..0f2cc1dee6 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -12,14 +12,12 @@ use crate::{ }; use anyhow::{anyhow, bail, Context, Result}; use bstr::{BString, ByteSlice}; -use git2::{ApplyLocation, ApplyOptions, Repository, ResetType}; -use git2_hooks::HookResult; use gitbutler_branch::BranchUpdateRequest; use gitbutler_branch::{dedup, dedup_fmt}; use gitbutler_cherry_pick::RepositoryExt as _; use gitbutler_command_context::CommandContext; use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders}; -use gitbutler_diff::{trees, ChangeType, GitHunk, Hunk}; +use gitbutler_diff::{trees, GitHunk, Hunk}; use gitbutler_error::error::Code; use gitbutler_hunk_dependency::RangeCalculationError; use gitbutler_operating_modes::assure_open_workspace_mode; @@ -29,9 +27,10 @@ use gitbutler_oxidize::{ use gitbutler_project::access::WorktreeWritePermission; use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname}; use gitbutler_repo::{ + hooks, logging::{LogUntil, RepositoryExt as _}, rebase::{cherry_rebase, cherry_rebase_group}, - RepositoryExt, + staging, RepositoryExt, }; use gitbutler_repo_actions::RepoActionsExt; use gitbutler_stack::{ @@ -41,7 +40,6 @@ use gitbutler_stack::{ use gitbutler_time::time::now_since_unix_epoch_ms; use itertools::Itertools; use serde::Serialize; -use std::borrow::Cow; use std::{collections::HashMap, path::PathBuf, vec}; use tracing::instrument; @@ -733,65 +731,55 @@ pub fn commit( ownership: Option<&BranchOwnershipClaims>, run_hooks: bool, ) -> Result { - // get the files to commit let diffs = gitbutler_diff::workdir(ctx.repo(), get_workspace_head(ctx)?)?; - let statuses = get_applied_status_cached(ctx, None, &diffs) - .context("failed to get status by branch")? - .branches; - let (ref mut branch, files) = statuses - .into_iter() - .find(|(stack, _)| stack.id == stack_id) - .with_context(|| format!("stack {stack_id} not found"))?; - - let selected_files = if let Some(ownership) = ownership { - files - .clone() - .into_iter() - .filter_map(|file| { - let hunks = file + if ownership.is_none() { + // If we are committing everything owned by a branch then we + // need to make sure we have first updated ownerships. + get_applied_status_cached(ctx, None, &diffs)?; + } + + let mut stack = ctx.project().virtual_branches().get_stack(stack_id)?; + let ownership = ownership + .map::, _>(Ok) + .unwrap_or_else(|| Ok(&stack.ownership))?; + + let selected_files = ownership + .claims + .iter() + .map(|claim| { + if let Some(diff) = diffs.get(&claim.file_path) { + let hunks = claim .hunks - .into_iter() - .filter(|hunk| { - let hunk: GitHunk = hunk.clone().into(); - ownership - .claims + .iter() + .filter_map(|claimed_hunk| { + diff.hunks .iter() - .find(|f| f.file_path.eq(&file.path)) - .map_or(false, |f| { - f.hunks.iter().any(|h| { - h.start == hunk.new_start - && h.end == hunk.new_start + hunk.new_lines - }) + .find(|diff_hunk| { + claimed_hunk.start == diff_hunk.new_start + && claimed_hunk.end == diff_hunk.new_start + diff_hunk.new_lines }) + .cloned() }) - .collect::>(); - if !hunks.is_empty() { - Some((file.path, hunks)) - } else { - None - } - }) - .collect::)>>() - } else { - files - .clone() - .into_iter() - .map(|file| (file.path, file.hunks)) - .collect::)>>() - }; + .collect_vec(); + Ok((claim.file_path.clone(), hunks)) + } else { + Err(anyhow!("Claim not found in workspace diff")) + } + }) + .collect::)>>>()?; let final_message = if run_hooks { - run_message_hook(ctx, message.to_owned())? + hooks::message(ctx, message.to_owned())? } else { message.to_owned() }; if run_hooks { - stage_files(ctx, &selected_files)?; - let result = run_pre_commit_hook(ctx); + staging::stage_files(ctx, &selected_files)?; + let result = hooks::pre_commit(ctx); if result.is_err() { - unstage_all(ctx)?; + staging::unstage_all(ctx)?; result?; } } @@ -803,10 +791,10 @@ pub fn commit( let git_repository = ctx.repo(); let parent_commit = git_repository - .find_commit(branch.head()) - .context(format!("failed to find commit {:?}", branch.head()))?; + .find_commit(stack.head()) + .context(format!("failed to find commit {:?}", stack.head()))?; - let tree_oid = gitbutler_diff::write::hunks_onto_commit(ctx, branch.head(), selected_files)?; + let tree_oid = gitbutler_diff::write::hunks_onto_commit(ctx, stack.head(), selected_files)?; let tree = git_repository .find_tree(tree_oid) .context(format!("failed to find tree {:?}", tree_oid))?; @@ -836,13 +824,11 @@ pub fn commit( }; if run_hooks { - git2_hooks::hooks_post_commit(ctx.repo(), Some(&["../.husky"])) - .context("failed to run hook") - .context(Code::CommitHookFailed)?; + hooks::post_commit(ctx)?; } let vb_state = ctx.project().virtual_branches(); - branch.set_stack_head(ctx, commit_oid, Some(tree_oid))?; + stack.set_stack_head(ctx, commit_oid, Some(tree_oid))?; crate::integration::update_workspace_commit(&vb_state, ctx) .context("failed to update gitbutler workspace")?; @@ -850,104 +836,6 @@ pub fn commit( Ok(commit_oid) } -fn join_output<'a>(stdout: &'a str, stderr: &'a str) -> Cow<'a, str> { - let stdout = stdout.trim(); - if stdout.is_empty() { - stderr.trim().into() - } else { - stdout.into() - } -} - -fn run_message_hook(ctx: &CommandContext, mut message: String) -> Result { - let hook_result = git2_hooks::hooks_commit_msg(ctx.repo(), Some(&["../.husky"]), &mut message) - .context("failed to run hook") - .context(Code::CommitHookFailed)?; - - if let HookResult::RunNotSuccessful { stdout, stderr, .. } = &hook_result { - return Err( - anyhow!("commit-msg hook rejected: {}", join_output(stdout, stderr)) - .context(Code::CommitHookFailed), - ); - } - Ok(message) -} - -fn run_pre_commit_hook(ctx: &CommandContext) -> Result<()> { - let hook_result = git2_hooks::hooks_pre_commit(ctx.repo(), Some(&["../.husky"])) - .context("failed to run hook") - .context(Code::CommitHookFailed)?; - - if let HookResult::RunNotSuccessful { stdout, stderr, .. } = &hook_result { - return Err( - anyhow!("commit hook rejected: {}", join_output(stdout, stderr)) - .context(Code::CommitHookFailed), - ); - } - Ok(()) -} - -pub(crate) fn unstage_all(ctx: &CommandContext) -> Result<()> { - let repo = ctx.repo(); - // Get the HEAD commit (current commit) - let head_commit = repo.head()?.peel_to_commit()?; - // Reset the index to match the HEAD commit - repo.reset(head_commit.as_object(), ResetType::Mixed, None)?; - Ok(()) -} - -fn diff_workdir_to_index<'a>(repo: &'a Repository, path: &PathBuf) -> Result> { - let index = repo.index()?; - let mut diff_opts = git2::DiffOptions::new(); - diff_opts - .recurse_untracked_dirs(true) - .include_untracked(true) - .show_binary(true) - .show_untracked_content(true) - .ignore_submodules(true) - .context_lines(3) - .pathspec(path); - Ok(repo.diff_index_to_workdir(Some(&index), Some(&mut diff_opts))?) -} - -fn stage_file(ctx: &CommandContext, path: &PathBuf, hunks: &Vec) -> Result<()> { - let repo = ctx.repo(); - let mut index = repo.index()?; - if hunks.iter().any(|h| h.change_type == ChangeType::Untracked) { - index.add_path(path)?; - index.write()?; - return Ok(()); - } - - let mut apply_opts = ApplyOptions::new(); - apply_opts.hunk_callback(|cb_hunk| { - cb_hunk.map_or(false, |cb_hunk| { - for hunk in hunks { - if hunk.start == cb_hunk.new_start() - && hunk.end == cb_hunk.new_start() + cb_hunk.new_lines() - { - return true; - } - } - false - }) - }); - - let diff = diff_workdir_to_index(repo, path)?; - repo.apply(&diff, ApplyLocation::Index, Some(&mut apply_opts))?; - Ok(()) -} - -fn stage_files( - ctx: &CommandContext, - files_to_stage: &Vec<(PathBuf, Vec)>, -) -> Result<()> { - for (path_to_stage, hunks_to_stage) in files_to_stage { - stage_file(ctx, path_to_stage, hunks_to_stage)?; - } - Ok(()) -} - pub(crate) fn push( ctx: &CommandContext, stack_id: StackId, diff --git a/crates/gitbutler-repo/Cargo.toml b/crates/gitbutler-repo/Cargo.toml index a49e109a3d..e0fe4e08e8 100644 --- a/crates/gitbutler-repo/Cargo.toml +++ b/crates/gitbutler-repo/Cargo.toml @@ -8,6 +8,7 @@ autotests = false [dependencies] git2.workspace = true +git2-hooks = "0.4" gix = { workspace = true, features = ["merge", "status", "tree-editor"] } anyhow = "1.0.95" bstr.workspace = true @@ -25,6 +26,7 @@ gitbutler-commit.workspace = true gitbutler-url.workspace = true gitbutler-cherry-pick.workspace = true gitbutler-oxidize.workspace = true +gitbutler-diff.workspace = true uuid.workspace = true itertools = "0.14" toml.workspace = true diff --git a/crates/gitbutler-repo/src/hooks.rs b/crates/gitbutler-repo/src/hooks.rs new file mode 100644 index 0000000000..174172263f --- /dev/null +++ b/crates/gitbutler-repo/src/hooks.rs @@ -0,0 +1,50 @@ +use std::borrow::Cow; + +use anyhow::{anyhow, Context, Result}; +use git2_hooks::HookResult; +use gitbutler_command_context::CommandContext; +use gitbutler_error::error::Code; + +pub fn message(ctx: &CommandContext, mut message: String) -> Result { + let hook_result = git2_hooks::hooks_commit_msg(ctx.repo(), Some(&["../.husky"]), &mut message) + .context("failed to run hook") + .context(Code::CommitHookFailed)?; + + if let HookResult::RunNotSuccessful { stdout, stderr, .. } = &hook_result { + return Err( + anyhow!("commit-msg hook rejected: {}", join_output(stdout, stderr)) + .context(Code::CommitHookFailed), + ); + } + Ok(message) +} + +pub fn pre_commit(ctx: &CommandContext) -> Result<()> { + let hook_result = git2_hooks::hooks_pre_commit(ctx.repo(), Some(&["../.husky"])) + .context("failed to run hook") + .context(Code::CommitHookFailed)?; + + if let HookResult::RunNotSuccessful { stdout, stderr, .. } = &hook_result { + return Err( + anyhow!("commit hook rejected: {}", join_output(stdout, stderr)) + .context(Code::CommitHookFailed), + ); + } + Ok(()) +} + +pub fn post_commit(ctx: &CommandContext) -> Result<()> { + git2_hooks::hooks_post_commit(ctx.repo(), Some(&["../.husky"])) + .context("failed to run hook") + .context(Code::CommitHookFailed)?; + Ok(()) +} + +fn join_output<'a>(stdout: &'a str, stderr: &'a str) -> Cow<'a, str> { + let stdout = stdout.trim(); + if stdout.is_empty() { + stderr.trim().into() + } else { + stdout.into() + } +} diff --git a/crates/gitbutler-repo/src/lib.rs b/crates/gitbutler-repo/src/lib.rs index 3825920e5a..c48464d1e0 100644 --- a/crates/gitbutler-repo/src/lib.rs +++ b/crates/gitbutler-repo/src/lib.rs @@ -10,7 +10,9 @@ pub use repository_ext::RepositoryExt; pub mod credentials; mod config; +pub mod hooks; mod remote; +pub mod staging; pub use config::Config; diff --git a/crates/gitbutler-repo/src/staging.rs b/crates/gitbutler-repo/src/staging.rs new file mode 100644 index 0000000000..9fc2a1ac19 --- /dev/null +++ b/crates/gitbutler-repo/src/staging.rs @@ -0,0 +1,67 @@ +use std::path::PathBuf; + +use anyhow::Result; +use git2::{ApplyLocation, ApplyOptions, Repository, ResetType}; +use gitbutler_command_context::CommandContext; +use gitbutler_diff::{ChangeType, GitHunk}; + +pub fn stage_file(ctx: &CommandContext, path: &PathBuf, hunks: &Vec) -> Result<()> { + let repo = ctx.repo(); + let mut index = repo.index()?; + if hunks.iter().any(|h| h.change_type == ChangeType::Untracked) { + index.add_path(path)?; + index.write()?; + return Ok(()); + } + + let mut apply_opts = ApplyOptions::new(); + apply_opts.hunk_callback(|cb_hunk| { + cb_hunk.map_or(false, |cb_hunk| { + for hunk in hunks { + if hunk.new_start == cb_hunk.new_start() + && hunk.new_start + hunk.new_lines == cb_hunk.new_start() + cb_hunk.new_lines() + { + return true; + } + } + false + }) + }); + + let diff = diff_workdir_to_index(repo, path)?; + repo.apply(&diff, ApplyLocation::Index, Some(&mut apply_opts))?; + Ok(()) +} + +pub fn stage_files( + ctx: &CommandContext, + files_to_stage: &Vec<(PathBuf, Vec)>, +) -> Result<()> { + for (path_to_stage, hunks_to_stage) in files_to_stage { + stage_file(ctx, path_to_stage, hunks_to_stage)?; + } + Ok(()) +} + +pub fn unstage_all(ctx: &CommandContext) -> Result<()> { + let repo = ctx.repo(); + // Get the HEAD commit (current commit) + let head_commit = repo.head()?.peel_to_commit()?; + // Reset the index to match the HEAD commit + repo.reset(head_commit.as_object(), ResetType::Mixed, None)?; + Ok(()) +} + +fn diff_workdir_to_index<'a>(repo: &'a Repository, path: &PathBuf) -> Result> { + let index = repo.index()?; + let mut diff_opts = git2::DiffOptions::new(); + diff_opts + .recurse_untracked_dirs(true) + .include_untracked(true) + .show_binary(true) + .show_untracked_content(true) + .ignore_submodules(true) + .context_lines(3) + .pathspec(path); + Ok(repo.diff_index_to_workdir(Some(&index), Some(&mut diff_opts))?) +} From 5e6909a2046386f86dcc0b00844dbed352d17171 Mon Sep 17 00:00:00 2001 From: Mattias Granlund Date: Tue, 7 Jan 2025 00:06:21 +0100 Subject: [PATCH 3/4] Add run hooks tauri command --- Cargo.lock | 1 + .../src/lib/commit/CommitDialog.svelte | 11 ++++++ .../src/lib/vbranches/branchController.ts | 12 +++++++ crates/gitbutler-branch-actions/src/lib.rs | 3 +- .../gitbutler-branch-actions/src/ownership.rs | 36 +++++++++++++++++++ .../gitbutler-branch-actions/src/virtual.rs | 26 ++------------ crates/gitbutler-tauri/Cargo.toml | 1 + crates/gitbutler-tauri/src/main.rs | 1 + crates/gitbutler-tauri/src/repo.rs | 35 ++++++++++++++++-- 9 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 crates/gitbutler-branch-actions/src/ownership.rs diff --git a/Cargo.lock b/Cargo.lock index 41cd8179f7..a8bf5e0f3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2830,6 +2830,7 @@ dependencies = [ "gitbutler-user", "gitbutler-watcher", "gix", + "itertools 0.14.0", "log", "once_cell", "open", diff --git a/apps/desktop/src/lib/commit/CommitDialog.svelte b/apps/desktop/src/lib/commit/CommitDialog.svelte index 81fe116612..0e66202a8f 100644 --- a/apps/desktop/src/lib/commit/CommitDialog.svelte +++ b/apps/desktop/src/lib/commit/CommitDialog.svelte @@ -59,6 +59,10 @@ } } + async function runHooks() { + await branchController.runHooks(projectId, $selectedOwnership.toString()); + } + function close() { $expanded = false; } @@ -149,6 +153,13 @@ {/snippet} {:else} + + {#if $expanded} + + {/if} {/if} {#if $expanded && canShowCommitAndPublish} @@ -153,13 +175,6 @@ {/snippet} {:else} -