Skip to content

Commit

Permalink
Fix leaking GIT_DIR due to git hooks ... ⚓ (#84)
Browse files Browse the repository at this point in the history
Fixes environment problems when running the CLI over `git hooks ...` and Git sets the `GIT_DIR` (etc.)
when randomly inside a `.git` directory.
  • Loading branch information
gabyx authored Dec 28, 2021
1 parent 0f5a4d0 commit 366bf5f
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 142 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ orbs:

jobs:
linux:
resource_class: small
resource_class: medium
parameters:
test:
description: "The test script name"
Expand Down
2 changes: 1 addition & 1 deletion githooks/cmd/installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ func buildFromSource(

// Checkout the remote commit sha
log.InfoF("Checkout out commit '%s'", commitSHA[0:6])
gitx := git.NewCtxAt(tempDir)
gitx := git.NewCtxSanitizedAt(tempDir)
err = gitx.Check("checkout",
"-b", "update-to-"+commitSHA[0:6],
commitSHA)
Expand Down
7 changes: 5 additions & 2 deletions githooks/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ func NewSettings(

var promptx prompt.IContext
var err error

cwd, err := os.Getwd()
gitx := git.NewCtxAt(cwd)
log.AssertNoErrorPanic(err, "Could not get current working directory.")

// Since this CLI is executed over `git ...` it sets
// environment variables we are not interested in when running this executable.
log.AssertNoError(git.SanitizeOsEnv(), "Could not sanitize OS environment.")
gitx := git.NewCtx()

installDir := inst.LoadInstallDir(log, gitx)

promptx, err = prompt.CreateContext(log, false, false)
Expand Down
49 changes: 36 additions & 13 deletions githooks/common/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ func (c *CmdContext) Get(args ...string) (string, error) {
stdout, err := cmd.Output()

if err != nil {
var errS string
if exitErr, ok := err.(*exec.ExitError); ok {
errS = string(exitErr.Stderr)
}
err = CombineErrors(
ErrorF("Command failed: '%s %q'.", c.baseCmd, args), err)
ErrorF("Command failed: '%s %q' [cwd: '%s', env: %q, err: '%s'].",
c.baseCmd, args, cmd.Dir, cmd.Env, errS), err)
}

return strings.TrimSpace(string(stdout)), err
Expand All @@ -62,27 +67,38 @@ func (c *CmdContext) GetCombined(args ...string) (string, error) {
stdout, err := cmd.CombinedOutput()

if err != nil {
var errS string
if exitErr, ok := err.(*exec.ExitError); ok {
errS = string(exitErr.Stderr)
}
err = CombineErrors(
ErrorF("Command failed: '%s %q'.", c.baseCmd, args), err)
ErrorF("Command failed: '%s %q' [cwd: '%s', env: %q, err: '%s'].",
c.baseCmd, args, cmd.Dir, cmd.Env, errS), err)
}

return strings.TrimSpace(string(stdout)), err
}

// Check checks if a command executed successfully.
func (c *CmdContext) Check(args ...string) error {
func (c *CmdContext) Check(args ...string) (err error) {
cmd := exec.Command(c.baseCmd, args...)
cmd.Dir = c.cwd
cmd.Env = c.Env

err := cmd.Run()
err = cmd.Run()

if err != nil {
return CombineErrors(
ErrorF("Command failed: '%s %q'.", c.baseCmd, args), err)
var errS string
if exitErr, ok := err.(*exec.ExitError); ok {
errS = string(exitErr.Stderr)
}

err = CombineErrors(
ErrorF("Command failed: '%s %q' [cwd: '%s', env: %q, err: '%s'].",
c.baseCmd, args, cmd.Dir, cmd.Env, errS), err)
}

return nil
return
}

// GetExitCode get the exit code of the command.
Expand All @@ -102,24 +118,31 @@ func (c *CmdContext) GetExitCode(args ...string) (int, error) {
}

return -1, CombineErrors(
ErrorF("Could get exit status of '%s %s'.", c.baseCmd, args), err)
ErrorF("Could get exit status of '%s %q' [cwd: '%s', env: %q].",
c.baseCmd, args, cmd.Dir, cmd.Env), err)
}

// CheckPiped checks if a command executed successfully.
func (c *CmdContext) CheckPiped(args ...string) error {
func (c *CmdContext) CheckPiped(args ...string) (err error) {
cmd := exec.Command(c.baseCmd, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = c.cwd
cmd.Env = c.Env

err := cmd.Run()
err = cmd.Run()

if err != nil {
return CombineErrors(
ErrorF("Command failed: '%s %q'.", c.baseCmd, args), err)
var errS string
if exitErr, ok := err.(*exec.ExitError); ok {
errS = string(exitErr.Stderr)
}

err = CombineErrors(
ErrorF("Command failed: '%s %q' [cwd: '%s', env: %q, err: '%s'].",
c.baseCmd, args, cmd.Dir, cmd.Env, errS), err)
}

return nil
return
}
9 changes: 9 additions & 0 deletions githooks/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,12 @@ func SanitizeEnv(env []string) []string {
!strings.Contains(s, "GIT_WORK_TREE")
})
}

// SanitizeOsEnv santizes the process environement from unwanted Git (possibly leaking)
// Git variables which might interfere with certain buggy Git commands.
func SanitizeOsEnv() error {
err := os.Unsetenv("GIT_DIR")
err = cm.CombineErrors(err, os.Unsetenv("GIT_WORK_TREE"))

return err
}
134 changes: 134 additions & 0 deletions githooks/hooks/lfs-hooks-cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package hooks

import (
"io/ioutil"
"os"
"path"
"strings"

cm "github.com/gabyx/githooks/githooks/common"
"github.com/gabyx/githooks/githooks/git"
"github.com/hashicorp/go-version"
)

// A file cache containing LFS hooks.
type LFSHooksCache interface {
// Returns all LFS paths and file names inside the cache.
GetLFSHooks() ([]string, []string, error)
}

type lfsHooksCache struct {
lfsHookNames []string
repoDir string
requiredLFSVersion *version.Version
initialized bool

failure error
}

// Returns all LFS paths and file names inside the cache.
func (l *lfsHooksCache) GetLFSHooks() ([]string, []string, error) {
if !l.initialized && l.failure == nil {
l.failure = l.init()
}

if l.failure != nil {
return nil, nil, l.failure
}

lfsHookFiles := make([]string, len(l.lfsHookNames))
for i := range l.lfsHookNames {
f := path.Join(l.repoDir, "hooks", l.lfsHookNames[i])
if cm.IsFile(f) {
lfsHookFiles[i] = f
}
}

return lfsHookFiles, l.lfsHookNames, nil
}

// Creates a new LFS hooks cache.
func NewLFSHooksCache(tempDir string) (_ LFSHooksCache, err error) {
if !git.IsLFSAvailable() {
return nil, nil
}

var l lfsHooksCache
l.repoDir = path.Join(tempDir, "lfs-hooks")
l.requiredLFSVersion, err = git.GetGitLFSVersion()

return &l, err
}

func gitLFSInstall(gitx *git.Context) (err error) {
err = gitx.Check("lfs", "install")

if err != nil {
err = cm.CombineErrors(err, cm.ErrorF("Could not install Git LFS hooks in\n"+
"'%s'.\n"+
"Please try manually by invoking:\n"+
" $ git -C '%[1]s' lfs install", gitx.GetCwd()))
}

return
}

// Initializes the cache.
func (l *lfsHooksCache) init() (err error) {

if l.initialized {
return nil
}

versionFile := path.Join(l.repoDir, "lfs-version.info")

reinit := true

if cm.IsFile(versionFile) {
ver, err := os.ReadFile(versionFile)
if err == nil {
v, err := version.NewVersion(strings.TrimSpace(string(ver)))
reinit = err != nil || !v.Equal(l.requiredLFSVersion)
}
}

gitx := git.NewCtxSanitizedAt(l.repoDir)
hooksDir := path.Join(l.repoDir, "hooks")
if !cm.IsDirectory(hooksDir) || !gitx.IsGitRepo() {
reinit = true
}

if reinit {
err = os.MkdirAll(l.repoDir, cm.DefaultFileModeDirectory)

if err != nil {
return cm.CombineErrors(err, cm.ErrorF("Could not create LFS hooks cache in '%s'.", l.repoDir))
}

err = git.Init(l.repoDir, true)
if err != nil {
return
}

err = gitLFSInstall(gitx)
if err != nil {
return
}
}

for i := range ManagedHookNames {
hook := path.Join(l.repoDir, "hooks", ManagedHookNames[i])
if cm.IsFile(hook) {
l.lfsHookNames = append(l.lfsHookNames, ManagedHookNames[i])
}
}

err = ioutil.WriteFile(versionFile, []byte(l.requiredLFSVersion.String()), cm.DefaultFileModeFile)
if err != nil {
err = cm.CombineErrors(err, cm.ErrorF("Could not write version file in LFS hooks cache '%s'.", l.repoDir))
}

l.initialized = true

return
}
Loading

0 comments on commit 366bf5f

Please sign in to comment.