diff --git a/cmd/git-sync/main.go b/cmd/git-sync/main.go index ef9a1ac..f12a05c 100644 --- a/cmd/git-sync/main.go +++ b/cmd/git-sync/main.go @@ -720,7 +720,7 @@ func (git *repoSync) SanityCheck(ctx context.Context) bool { } // Check that this is actually the root of the repo. - if root, err := git.run.Run(ctx, git.root, git.cmd, "rev-parse", "--show-toplevel"); err != nil { + if root, err := git.run.Run(ctx, git.root, nil, git.cmd, "rev-parse", "--show-toplevel"); err != nil { git.log.Error(err, "can't get repo toplevel", "repo", git.root) return false } else { @@ -732,7 +732,7 @@ func (git *repoSync) SanityCheck(ctx context.Context) bool { } // Consistency-check the repo. - if _, err := git.run.Run(ctx, git.root, git.cmd, "fsck", "--no-progress", "--connectivity-only"); err != nil { + if _, err := git.run.Run(ctx, git.root, nil, git.cmd, "fsck", "--no-progress", "--connectivity-only"); err != nil { git.log.Error(err, "repo sanity check failed", "repo", git.root) return false } @@ -848,12 +848,12 @@ func (git *repoSync) UpdateSymlink(ctx context.Context, newDir string) (string, const tmplink = "tmp-link" git.log.V(1).Info("creating tmp symlink", "root", linkDir, "dst", newDirRelative, "src", tmplink) - if _, err := git.run.Run(ctx, linkDir, "ln", "-snf", newDirRelative, tmplink); err != nil { + if _, err := git.run.Run(ctx, linkDir, nil, "ln", "-snf", newDirRelative, tmplink); err != nil { return "", fmt.Errorf("error creating symlink: %v", err) } git.log.V(1).Info("renaming symlink", "root", linkDir, "oldName", tmplink, "newName", linkFile) - if _, err := git.run.Run(ctx, linkDir, "mv", "-T", tmplink, linkFile); err != nil { + if _, err := git.run.Run(ctx, linkDir, nil, "mv", "-T", tmplink, linkFile); err != nil { return "", fmt.Errorf("error replacing symlink: %v", err) } @@ -882,7 +882,7 @@ func (git *repoSync) CleanupWorkTree(ctx context.Context, gitRoot, worktree stri git.log.V(1).Info("removing worktree", "path", worktree) if err := os.RemoveAll(worktree); err != nil { return fmt.Errorf("error removing directory: %v", err) - } else if _, err := git.run.Run(ctx, gitRoot, git.cmd, "worktree", "prune"); err != nil { + } else if _, err := git.run.Run(ctx, git.root, nil, git.cmd, "worktree", "prune"); err != nil { return err } return nil @@ -903,7 +903,7 @@ func (git *repoSync) AddWorktreeAndSwap(ctx context.Context, hash string) error args = append(args, "origin", fetch) // Update from the remote. - if _, err := git.run.Run(ctx, git.root, git.cmd, args...); err != nil { + if _, err := git.run.Run(ctx, git.root, nil, git.cmd, args...); err != nil { return err } @@ -926,14 +926,14 @@ func (git *repoSync) AddWorktreeAndSwap(ctx context.Context, hash string) error return err } - _, err := git.run.Run(ctx, git.root, git.cmd, "worktree", "add", "--detach", worktreePath, hash, "--no-checkout") + _, err := git.run.Run(ctx, git.root, nil, git.cmd, "worktree", "add", "--detach", worktreePath, hash, "--no-checkout") git.log.V(0).Info("adding worktree", "path", worktreePath, "hash", hash) if err != nil { return err } // GC clone - if _, err := git.run.Run(ctx, git.root, git.cmd, "gc", "--prune=all"); err != nil { + if _, err := git.run.Run(ctx, git.root, nil, git.cmd, "gc", "--prune=all"); err != nil { return err } @@ -986,14 +986,14 @@ func (git *repoSync) AddWorktreeAndSwap(ctx context.Context, hash string) error } args := []string{"sparse-checkout", "init"} - _, err = git.run.Run(ctx, worktreePath, git.cmd, args...) + _, err = git.run.Run(ctx, worktreePath, nil, git.cmd, args...) if err != nil { return err } } // Reset the worktree's working copy to the specific rev. - _, err = git.run.Run(ctx, worktreePath, git.cmd, "reset", "--hard", hash, "--") + _, err = git.run.Run(ctx, worktreePath, nil, git.cmd, "reset", "--hard", hash, "--") if err != nil { return err } @@ -1010,7 +1010,7 @@ func (git *repoSync) AddWorktreeAndSwap(ctx context.Context, hash string) error if git.depth != 0 { submodulesArgs = append(submodulesArgs, "--depth", strconv.Itoa(git.depth)) } - _, err = git.run.Run(ctx, worktreePath, git.cmd, submodulesArgs...) + _, err = git.run.Run(ctx, worktreePath, nil, git.cmd, submodulesArgs...) if err != nil { return err } @@ -1020,14 +1020,14 @@ func (git *repoSync) AddWorktreeAndSwap(ctx context.Context, hash string) error if git.chmod != 0 { mode := fmt.Sprintf("%#o", git.chmod) git.log.V(0).Info("changing file permissions", "mode", mode) - _, err = git.run.Run(ctx, "", "chmod", "-R", mode, worktreePath) + _, err = git.run.Run(ctx, "", nil, "chmod", "-R", mode, worktreePath) if err != nil { return err } } // Reset the root's rev (so we can prune and so we can rely on it later). - _, err = git.run.Run(ctx, git.root, git.cmd, "reset", "--hard", hash, "--") + _, err = git.run.Run(ctx, git.root, nil, git.cmd, "reset", "--hard", hash, "--") if err != nil { return err } @@ -1066,7 +1066,7 @@ func (git *repoSync) CloneRepo(ctx context.Context) error { args = append(args, git.repo, git.root) git.log.V(0).Info("cloning repo", "origin", git.repo, "path", git.root) - _, err := git.run.Run(ctx, "", git.cmd, args...) + _, err := git.run.Run(ctx, "", nil, git.cmd, args...) if err != nil { if strings.Contains(err.Error(), "already exists and is not an empty directory") { // Maybe a previous run crashed? Git won't use this dir. @@ -1078,7 +1078,7 @@ func (git *repoSync) CloneRepo(ctx context.Context) error { if err != nil { return err } - _, err = git.run.Run(ctx, "", git.cmd, args...) + _, err = git.run.Run(ctx, "", nil, git.cmd, args...) if err != nil { return err } @@ -1123,7 +1123,7 @@ func (git *repoSync) CloneRepo(ctx context.Context) error { } args := []string{"sparse-checkout", "init"} - _, err = git.run.Run(ctx, git.root, git.cmd, args...) + _, err = git.run.Run(ctx, git.root, nil, git.cmd, args...) if err != nil { return err } @@ -1134,7 +1134,7 @@ func (git *repoSync) CloneRepo(ctx context.Context) error { // LocalHashForRev returns the locally known hash for a given rev. func (git *repoSync) LocalHashForRev(ctx context.Context, rev string) (string, error) { - output, err := git.run.Run(ctx, git.root, git.cmd, "rev-parse", rev) + output, err := git.run.Run(ctx, git.root, nil, git.cmd, "rev-parse", rev) if err != nil { return "", err } @@ -1143,7 +1143,7 @@ func (git *repoSync) LocalHashForRev(ctx context.Context, rev string) (string, e // RemoteHashForRef returns the upstream hash for a given ref. func (git *repoSync) RemoteHashForRef(ctx context.Context, ref string) (string, error) { - output, err := git.run.Run(ctx, git.root, git.cmd, "ls-remote", "-q", "origin", ref) + output, err := git.run.Run(ctx, git.root, nil, git.cmd, "ls-remote", "-q", "origin", ref) if err != nil { return "", err } @@ -1161,7 +1161,7 @@ func (git *repoSync) RevIsHash(ctx context.Context) (bool, error) { func (git *repoSync) ResolveRef(ctx context.Context, ref string) (string, error) { // If git doesn't identify rev as a commit, we're done. - output, err := git.run.Run(ctx, git.root, git.cmd, "cat-file", "-t", ref) + output, err := git.run.Run(ctx, git.root, nil, git.cmd, "cat-file", "-t", ref) if err != nil { return "", err } @@ -1258,13 +1258,13 @@ func (git *repoSync) GetRevs(ctx context.Context) (string, string, error) { func (git *repoSync) SetupAuth(ctx context.Context, username, password string) error { git.log.V(1).Info("setting up git credential store") - _, err := git.run.Run(ctx, "", git.cmd, "config", "--global", "credential.helper", "store") + _, err := git.run.Run(ctx, "", nil, git.cmd, "config", "--global", "credential.helper", "store") if err != nil { return fmt.Errorf("can't configure git credential helper: %w", err) } creds := fmt.Sprintf("url=%v\nusername=%v\npassword=%v\n", git.repo, username, password) - _, err = git.run.RunWithStdin(ctx, "", creds, git.cmd, "credential", "approve") + _, err = git.run.RunWithStdin(ctx, "", nil, creds, git.cmd, "credential", "approve") if err != nil { return fmt.Errorf("can't configure git credentials: %w", err) } @@ -1308,7 +1308,7 @@ func (git *repoSync) SetupCookieFile(ctx context.Context) error { return fmt.Errorf("can't access git cookiefile: %w", err) } - if _, err = git.run.Run(ctx, "", git.cmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil { + if _, err = git.run.Run(ctx, "", nil, git.cmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil { return fmt.Errorf("can't configure git cookiefile: %w", err) } @@ -1384,7 +1384,7 @@ func (git *repoSync) setupExtraGitConfigs(ctx context.Context, configsFlag strin return fmt.Errorf("can't parse --git-config flag: %v", err) } for _, kv := range configs { - if _, err := git.run.Run(ctx, "", git.cmd, "config", "--global", kv.key, kv.val); err != nil { + if _, err := git.run.Run(ctx, "", nil, git.cmd, "config", "--global", kv.key, kv.val); err != nil { return fmt.Errorf("error configuring additional git configs %q %q: %v", kv.key, kv.val, err) } } @@ -1613,10 +1613,11 @@ OPTIONS An optional command to be executed after syncing a new hash of the remote repository. This command does not take any arguments and executes with the synced repo as its working directory. The - execution is subject to the overall --sync-timeout flag and will - extend the effective period between sync attempts. This flag - obsoletes --sync-hook-command, but if sync-hook-command is - specified, it will take precedence. + environment variable $GITSYNC_HASH will be set to the git SHA that + was synced. The execution is subject to the overall --sync-timeout + flag and will extend the effective period between sync attempts. + This flag obsoletes --sync-hook-command, but if sync-hook-command + is specified, it will take precedence. --exechook-timeout , $GIT_SYNC_EXECHOOK_TIMEOUT The timeout for the --exechook-command. (default: 30s) @@ -1752,7 +1753,8 @@ OPTIONS The timeout for the --webhook-url. (default: 1s) --webhook-url , $GIT_SYNC_WEBHOOK_URL - A URL for optional webhook notifications when syncs complete. + A URL for optional webhook notifications when syncs complete. The + header 'Gitsync-Hash' will be set to the git SHA that was synced. EXAMPLE USAGE @@ -1791,14 +1793,21 @@ AUTHENTICATION When --cookie-file (GIT_COOKIE_FILE) is specified, the associated cookies can contain authentication information. -WEBHOOKS +HOOKS - Webhooks are executed asynchronously from the main git-sync process. If a - --webhook-url is configured, whenever a new hash is synced a call is sent - using the method defined in --webhook-method. Git-sync will retry this - webhook call until it succeeds (based on --webhook-success-status). If - unsuccessful, git-sync will wait --webhook-backoff (default 3s) before - re-attempting the webhook call. + Webhooks and exechooks are executed asynchronously from the main git-sync + process. If a --webhook-url or --exechook-command is configured, whenever + a new hash is synced the hook(s) will be invoked. For exechook, that means + the command is exec()'ed, and for webhooks that means an HTTP request is + sent using the method defined in --webhook-method. Git-sync will retry + both forms of hooks until they succeed (exit code 0 for exechooks, or + --webhook-success-status for webhooks). If unsuccessful, git-sync will + wait --exechook-backoff or --webhook-backoff (as appropriate) before + re-trying the hook. + + Hooks are not guaranteed to succeed on every single SHA change. For example, + if a hook fails and a new SHA is synced during the backoff period, the + retried hook will fire for the newest SHA. ` func printManPage() { diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 372e928..a081699 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -38,12 +38,12 @@ func NewRunner(logger *logging.Logger) *Runner { } // Run runs given command -func (r *Runner) Run(ctx context.Context, cwd, command string, args ...string) (string, error) { - return r.RunWithStdin(ctx, cwd, "", command, args...) +func (r *Runner) Run(ctx context.Context, cwd string, env []string, command string, args ...string) (string, error) { + return r.RunWithStdin(ctx, cwd, env, "", command, args...) } // RunWithStdin runs given command with stardart input -func (r *Runner) RunWithStdin(ctx context.Context, cwd, stdin, command string, args ...string) (string, error) { +func (r *Runner) RunWithStdin(ctx context.Context, cwd string, env []string, stdin, command string, args ...string) (string, error) { cmdStr := cmdForLog(command, args...) r.logger.V(5).Info("running command", "cwd", cwd, "cmd", cmdStr) @@ -51,6 +51,9 @@ func (r *Runner) RunWithStdin(ctx context.Context, cwd, stdin, command string, a if cwd != "" { cmd.Dir = cwd } + if len(env) != 0 { + cmd.Env = env + } outbuf := bytes.NewBuffer(nil) errbuf := bytes.NewBuffer(nil) cmd.Stdout = outbuf diff --git a/pkg/hook/exechook.go b/pkg/hook/exechook.go index 8b4b544..a871331 100644 --- a/pkg/hook/exechook.go +++ b/pkg/hook/exechook.go @@ -18,6 +18,7 @@ package hook import ( "context" + "fmt" "path/filepath" "time" @@ -66,6 +67,10 @@ func (h *Exechook) Do(ctx context.Context, hash string) error { worktreePath := filepath.Join(h.gitRoot, hash) h.logger.V(0).Info("running exechook", "command", h.command, "timeout", h.timeout) - _, err := h.cmdrunner.Run(ctx, worktreePath, h.command, h.args...) + _, err := h.cmdrunner.Run(ctx, worktreePath, []string{envKV("GITSYNC_HASH", hash)}, h.command, h.args...) return err } + +func envKV(k, v string) string { + return fmt.Sprintf("%s=%s", k, v) +} diff --git a/test_exechook_command.sh b/test_exechook_command.sh index 6b5aadd..cfce004 100755 --- a/test_exechook_command.sh +++ b/test_exechook_command.sh @@ -17,5 +17,9 @@ # Use for e2e test of --exechook-command. # This option takes no command arguments, so requires a wrapper script. +if [ -z "${GITSYNC_HASH}" ]; then + echo "GITSYNC_HASH is not set" > exechook + exit 1 +fi cat file > exechook cat ../link/file > link-exechook