Skip to content

Commit 425b0ca

Browse files
author
merge-queue-bot
committed
Merge PR #185: Fix pre-merge-commit hook executable permissions
2 parents 0d12cb3 + ae759f9 commit 425b0ca

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

cmd/mdsmith/mergedriver.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,12 @@ func ensurePreMergeCommitHook(repoRoot string, files []string) error {
491491
if err := os.WriteFile(hookPath, []byte(content), 0o755); err != nil {
492492
return fmt.Errorf("writing %s: %w", hookPath, err)
493493
}
494+
// Explicitly set execute permissions after writing. WriteFile's perm
495+
// argument is masked by umask on creation and ignored when the file
496+
// already exists, so a separate Chmod ensures the hook is executable.
497+
if err := chmodFunc(hookPath, 0o755); err != nil {
498+
return fmt.Errorf("setting permissions on %s: %w", hookPath, err)
499+
}
494500
return nil
495501
}
496502

@@ -521,6 +527,10 @@ func registerMergeDriver() error {
521527
// Overridden in tests to exercise the non-temporary-exe branch.
522528
var executableFunc = os.Executable
523529

530+
// chmodFunc is the function used to set file permissions.
531+
// Overridden in tests to exercise the Chmod error path.
532+
var chmodFunc = os.Chmod
533+
524534
// resolveInstalledBinary returns the absolute path to the mdsmith
525535
// binary to use as the git merge driver. It prefers the current
526536
// executable when it lives outside the OS temp directory (i.e. it

cmd/mdsmith/mergedriver_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,31 @@ func TestEnsurePreMergeCommitHook_OverwritesManagedHook(t *testing.T) {
508508
assert.Contains(t, string(data), "'PLAN.md'")
509509
}
510510

511+
func TestEnsurePreMergeCommitHook_SetsExecutableBitOnExistingHook(t *testing.T) {
512+
if runtime.GOOS == "windows" {
513+
t.Skip("POSIX permission semantics not applicable on Windows")
514+
}
515+
dir := t.TempDir()
516+
hooksDir := filepath.Join(dir, ".git", "hooks")
517+
require.NoError(t, os.MkdirAll(hooksDir, 0o755))
518+
hookPath := filepath.Join(hooksDir, "pre-merge-commit")
519+
// Pre-existing hook with our marker but NO execute permissions.
520+
old := "#!/bin/sh\n" + preMergeCommitHookMarker + "\n# old\n"
521+
require.NoError(t, os.WriteFile(hookPath, []byte(old), 0o644))
522+
523+
orig := executableFunc
524+
t.Cleanup(func() { executableFunc = orig })
525+
executableFunc = func() (string, error) { return "/usr/local/bin/mdsmith", nil }
526+
527+
require.NoError(t, ensurePreMergeCommitHook(dir, []string{"PLAN.md"}))
528+
529+
info, err := os.Stat(hookPath)
530+
require.NoError(t, err)
531+
// Verify execute bit is set despite the file existing without it.
532+
assert.NotZero(t, info.Mode()&0o111,
533+
"hook must have execute bit set even when overwriting non-executable file")
534+
}
535+
511536
func TestEnsurePreMergeCommitHook_RefusesUnmanagedHook(t *testing.T) {
512537
dir := t.TempDir()
513538
hooksDir := filepath.Join(dir, ".git", "hooks")
@@ -646,6 +671,26 @@ func TestEnsurePreMergeCommitHook_WriteFileFails(t *testing.T) {
646671
assert.Contains(t, err.Error(), "writing")
647672
}
648673

674+
func TestEnsurePreMergeCommitHook_ChmodFails(t *testing.T) {
675+
dir := t.TempDir()
676+
hooksDir := filepath.Join(dir, ".git", "hooks")
677+
require.NoError(t, os.MkdirAll(hooksDir, 0o755))
678+
679+
origExe := executableFunc
680+
t.Cleanup(func() { executableFunc = origExe })
681+
executableFunc = func() (string, error) { return "/usr/local/bin/mdsmith", nil }
682+
683+
origChmod := chmodFunc
684+
t.Cleanup(func() { chmodFunc = origChmod })
685+
chmodFunc = func(string, os.FileMode) error {
686+
return os.ErrPermission
687+
}
688+
689+
err := ensurePreMergeCommitHook(dir, []string{"PLAN.md"})
690+
require.Error(t, err)
691+
assert.Contains(t, err.Error(), "setting permissions")
692+
}
693+
649694
// --- resolveHooksDir ---
650695

651696
func TestResolveHooksDir_NotGitRepo(t *testing.T) {

0 commit comments

Comments
 (0)