package main import ( "bytes" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "golang.org/x/tools/go/vcs" ) // VCS represents a version control system. type VCS struct { vcs *vcs.Cmd IdentifyCmd string DescribeCmd string DiffCmd string ListCmd string RootCmd string // run in sandbox repos ExistsCmd string } var vcsBzr = &VCS{ vcs: vcs.ByCmd("bzr"), IdentifyCmd: "version-info --custom --template {revision_id}", DescribeCmd: "revno", // TODO(kr): find tag names if possible DiffCmd: "diff -r {rev}", ListCmd: "ls --from-root -R", RootCmd: "root", } var vcsGit = &VCS{ vcs: vcs.ByCmd("git"), IdentifyCmd: "rev-parse HEAD", DescribeCmd: "describe --tags", DiffCmd: "diff {rev}", ListCmd: "ls-files --full-name", RootCmd: "rev-parse --show-cdup", ExistsCmd: "cat-file -e {rev}", } var vcsHg = &VCS{ vcs: vcs.ByCmd("hg"), IdentifyCmd: "parents --template {node}", DescribeCmd: "log -r . --template {latesttag}-{latesttagdistance}", DiffCmd: "diff -r {rev}", ListCmd: "status --all --no-status", RootCmd: "root", ExistsCmd: "cat -r {rev} .", } var cmd = map[*vcs.Cmd]*VCS{ vcsBzr.vcs: vcsBzr, vcsGit.vcs: vcsGit, vcsHg.vcs: vcsHg, } // VCSFromDir returns a VCS value from a directory. func VCSFromDir(dir, srcRoot string) (*VCS, string, error) { vcscmd, reporoot, err := vcs.FromDir(dir, srcRoot) if err != nil { return nil, "", fmt.Errorf("error while inspecting %q: %v", dir, err) } vcsext := cmd[vcscmd] if vcsext == nil { return nil, "", fmt.Errorf("%s is unsupported: %s", vcscmd.Name, dir) } return vcsext, reporoot, nil } func (v *VCS) identify(dir string) (string, error) { out, err := v.runOutput(dir, v.IdentifyCmd) return string(bytes.TrimSpace(out)), err } func absRoot(dir, out string) string { if filepath.IsAbs(out) { return filepath.Clean(out) } return filepath.Join(dir, out) } func (v *VCS) root(dir string) (string, error) { out, err := v.runOutput(dir, v.RootCmd) return absRoot(dir, string(bytes.TrimSpace(out))), err } func (v *VCS) describe(dir, rev string) string { out, err := v.runOutputVerboseOnly(dir, v.DescribeCmd, "rev", rev) if err != nil { return "" } return string(bytes.TrimSpace(out)) } func (v *VCS) isDirty(dir, rev string) bool { out, err := v.runOutput(dir, v.DiffCmd, "rev", rev) return err != nil || len(out) != 0 } type vcsFiles map[string]bool func (vf vcsFiles) Contains(path string) bool { // Fast path, we have the path if vf[path] { return true } // Slow path for case insensitive filesystems // See #310 for f := range vf { if pathEqual(f, path) { return true } // git's root command (maybe other vcs as well) resolve symlinks, so try that too // FIXME: rev-parse --show-cdup + extra logic will fix this for git but also need to validate the other vcs commands. This is maybe temporary. p, err := filepath.EvalSymlinks(path) if err != nil { return false } if pathEqual(f, p) { return true } } // No matches by either method return false } // listFiles tracked by the VCS in the repo that contains dir, converted to absolute path. func (v *VCS) listFiles(dir string) vcsFiles { root, err := v.root(dir) debugln("vcs dir", dir) debugln("vcs root", root) ppln(v) if err != nil { return nil } out, err := v.runOutput(dir, v.ListCmd) if err != nil { return nil } files := make(vcsFiles) for _, file := range bytes.Split(out, []byte{'\n'}) { if len(file) > 0 { path, err := filepath.Abs(filepath.Join(root, string(file))) if err != nil { panic(err) // this should not happen } if pathEqual(filepath.Dir(path), dir) { files[path] = true } } } return files } func (v *VCS) exists(dir, rev string) bool { err := v.runVerboseOnly(dir, v.ExistsCmd, "rev", rev) return err == nil } // RevSync checks out the revision given by rev in dir. // The dir must exist and rev must be a valid revision. func (v *VCS) RevSync(dir, rev string) error { return v.run(dir, v.vcs.TagSyncCmd, "tag", rev) } // run runs the command line cmd in the given directory. // keyval is a list of key, value pairs. run expands // instances of {key} in cmd into value, but only after // splitting cmd into individual arguments. // If an error occurs, run prints the command line and the // command's combined stdout+stderr to standard error. // Otherwise run discards the command's output. func (v *VCS) run(dir string, cmdline string, kv ...string) error { _, err := v.run1(dir, cmdline, kv, true) return err } // runVerboseOnly is like run but only generates error output to standard error in verbose mode. func (v *VCS) runVerboseOnly(dir string, cmdline string, kv ...string) error { _, err := v.run1(dir, cmdline, kv, false) return err } // runOutput is like run but returns the output of the command. func (v *VCS) runOutput(dir string, cmdline string, kv ...string) ([]byte, error) { return v.run1(dir, cmdline, kv, true) } // runOutputVerboseOnly is like runOutput but only generates error output to standard error in verbose mode. func (v *VCS) runOutputVerboseOnly(dir string, cmdline string, kv ...string) ([]byte, error) { return v.run1(dir, cmdline, kv, false) } // run1 is the generalized implementation of run and runOutput. func (v *VCS) run1(dir string, cmdline string, kv []string, verbose bool) ([]byte, error) { m := make(map[string]string) for i := 0; i < len(kv); i += 2 { m[kv[i]] = kv[i+1] } args := strings.Fields(cmdline) for i, arg := range args { args[i] = expand(m, arg) } _, err := exec.LookPath(v.vcs.Cmd) if err != nil { fmt.Fprintf(os.Stderr, "godep: missing %s command.\n", v.vcs.Name) return nil, err } cmd := exec.Command(v.vcs.Cmd, args...) cmd.Dir = dir var buf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = &buf err = cmd.Run() out := buf.Bytes() if err != nil { if verbose { fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.vcs.Cmd, strings.Join(args, " ")) os.Stderr.Write(out) } return nil, err } return out, nil } func expand(m map[string]string, s string) string { for k, v := range m { s = strings.Replace(s, "{"+k+"}", v, -1) } return s } func gitDetached(r string) (bool, error) { o, err := vcsGit.runOutput(r, "status") if err != nil { return false, errors.New("unable to determine git status " + err.Error()) } return bytes.Contains(o, []byte("HEAD detached at")), nil } func gitDefaultBranch(r string) (string, error) { o, err := vcsGit.runOutput(r, "remote show origin") if err != nil { return "", errors.New("Running git remote show origin errored with: " + err.Error()) } return gitDetermineDefaultBranch(r, string(o)) } func gitDetermineDefaultBranch(r, o string) (string, error) { e := "Unable to determine HEAD branch: " hb := "HEAD branch:" lbcfgp := "Local branch configured for 'git pull':" s := strings.Index(o, hb) if s < 0 { b := strings.Index(o, lbcfgp) if b < 0 { return "", errors.New(e + "Remote HEAD is ambiguous. Before godep can pull new commits you will need to:" + ` cd ` + r + ` git checkout Here is what was reported: ` + o) } s = b + len(lbcfgp) } else { s += len(hb) } f := strings.Fields(o[s:]) if len(f) < 3 { return "", errors.New(e + "git output too short") } return f[0], nil } func gitCheckout(r, b string) error { return vcsGit.run(r, "checkout "+b) }