// Copyright (C) 2015-2020 Nexedi SA and Contributors. // Kirill Smelkov <kirr@nexedi.com> // // This program is free software: you can Use, Study, Modify and Redistribute // it under the terms of the GNU General Public License version 3, or (at your // option) any later version, as published by the Free Software Foundation. // // You can also Link and Combine this program with other software covered by // the terms of any of the Free Software licenses or any of the Open Source // Initiative approved licenses and Convey the resulting work. Corresponding // source of such a combination shall include the source code for all other // software used. // // This program is distributed WITHOUT ANY WARRANTY; without even the implied // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // // See COPYING file for full licensing terms. // See https://www.nexedi.com/licensing for rationale and options. package main // Git-backup | Run git subprocess import ( "bytes" "context" "fmt" "os" "os/exec" "strings" "lab.nexedi.com/kirr/go123/exc" "lab.nexedi.com/kirr/go123/mem" ) // how/whether to redirect stdio of spawned process type StdioRedirect int const ( PIPE StdioRedirect = iota // connect stdio channel via PIPE to parent (default value) DontRedirect ) type RunWith struct { stdin string stdout StdioRedirect // PIPE | DontRedirect stderr StdioRedirect // PIPE | DontRedirect raw bool // !raw -> stdout, stderr are stripped env map[string]string // !nil -> subprocess environment setup from env } // run `git *argv` -> error, stdout, stderr func _git(ctx context.Context, argv []string, rctx RunWith) (err error, stdout, stderr string) { debugf("git %s", strings.Join(argv, " ")) // XXX exec.CommandContext does `kill -9` on ctx cancel // XXX -> rework to `kill -TERM` so that spawned process can finish cleanly? cmd := exec.CommandContext(ctx, "git", argv...) stdoutBuf := bytes.Buffer{} stderrBuf := bytes.Buffer{} if rctx.stdin != "" { cmd.Stdin = strings.NewReader(rctx.stdin) } switch rctx.stdout { case PIPE: cmd.Stdout = &stdoutBuf case DontRedirect: cmd.Stdout = os.Stdout default: panic("git: stdout redirect mode invalid") } switch rctx.stderr { case PIPE: cmd.Stderr = &stderrBuf case DontRedirect: cmd.Stderr = os.Stderr default: panic("git: stderr redirect mode invalid") } if rctx.env != nil { env := []string{} for k, v := range rctx.env { env = append(env, k+"="+v) } cmd.Env = env } err = cmd.Run() stdout = mem.String(stdoutBuf.Bytes()) stderr = mem.String(stderrBuf.Bytes()) if !rctx.raw { // prettify stdout (e.g. so that 'sha1\n' becomes 'sha1' and can be used directly stdout = strings.TrimSpace(stdout) stderr = strings.TrimSpace(stderr) } return err, stdout, stderr } // error a git command returned type GitError struct { GitErrContext *exec.ExitError } type GitErrContext struct { argv []string stdin string stdout string stderr string } func (e *GitError) Error() string { msg := e.GitErrContext.Error() if e.stderr == "" { msg += "(failed)\n" } return msg } func (e *GitErrContext) Error() string { msg := "git " + strings.Join(e.argv, " ") if e.stdin == "" { msg += " </dev/null\n" } else { msg += " <<EOF\n" + e.stdin if !strings.HasSuffix(msg, "\n") { msg += "\n" } msg += "EOF\n" } msg += e.stderr if !strings.HasSuffix(msg, "\n") { msg += "\n" } return msg } // ctx, argv -> ctx, []string, rctx (for passing argv + RunWith handy - see ggit() for details) func _gitargv(ctx context.Context, argv ...interface{}) (_ context.Context, argvs []string, rctx RunWith) { rctx_seen := false for _, arg := range argv { switch arg := arg.(type) { case string: argvs = append(argvs, arg) default: argvs = append(argvs, fmt.Sprint(arg)) case RunWith: if rctx_seen { panic("git: multiple RunWith contexts") } rctx, rctx_seen = arg, true } } return ctx, argvs, rctx } // run `git *argv` -> err, stdout, stderr // - arguments are automatically converted to strings // - RunWith argument is passed as rctx // - error is returned only when git command could run and exits with error status // - on other errors - exception is raised // // NOTE err is concrete *GitError, not error func ggit(ctx context.Context, argv ...interface{}) (err *GitError, stdout, stderr string) { return ggit2(_gitargv(ctx, argv...)) } func ggit2(ctx context.Context, argv []string, rctx RunWith) (err *GitError, stdout, stderr string) { e, stdout, stderr := _git(ctx, argv, rctx) eexec, _ := e.(*exec.ExitError) if e != nil && eexec == nil { exc.Raisef("git %s : %s", strings.Join(argv, " "), e) } if eexec != nil { err = &GitError{GitErrContext{argv, rctx.stdin, stdout, stderr}, eexec} } return err, stdout, stderr } // run `git *argv` -> stdout // on error - raise exception func xgit(ctx context.Context, argv ...interface{}) string { return xgit2(_gitargv(ctx, argv...)) } func xgit2(ctx context.Context, argv []string, rctx RunWith) string { gerr, stdout, _ := ggit2(ctx, argv, rctx) if gerr != nil { exc.Raise(gerr) } return stdout } // like xgit(), but automatically parse stdout to Sha1 func xgitSha1(ctx context.Context, argv ...interface{}) Sha1 { return xgit2Sha1(_gitargv(ctx, argv...)) } // error when git output is not valid sha1 type GitSha1Error struct { GitErrContext } func (e *GitSha1Error) Error() string { msg := e.GitErrContext.Error() msg += fmt.Sprintf("expected valid sha1 (got %q)\n", e.stdout) return msg } func xgit2Sha1(ctx context.Context, argv []string, rctx RunWith) Sha1 { gerr, stdout, stderr := ggit2(ctx, argv, rctx) if gerr != nil { exc.Raise(gerr) } sha1, err := Sha1Parse(stdout) if err != nil { exc.Raise(&GitSha1Error{GitErrContext{argv, rctx.stdin, stdout, stderr}}) } return sha1 }