mirror of https://github.com/k3s-io/k3s
294 lines
7.2 KiB
Go
294 lines
7.2 KiB
Go
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 --abbrev=14",
|
|
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 <a HEAD branch>
|
|
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)
|
|
}
|