cuelang.org/go@v0.13.0/internal/vcs/vcs.go (about) 1 // Copyright 2024 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package vcs provides access to operations on the version control 16 // systems supported by the source field in module.cue. 17 package vcs 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "runtime" 27 "time" 28 ) 29 30 // VCS provides the operations on a particular instance of a VCS. 31 type VCS interface { 32 // Root returns the root of the directory controlled by 33 // the VCS (e.g. the directory containing .git). 34 Root() string 35 36 // ListFiles returns a list of files tracked by VCS, rooted at dir. The 37 // optional paths determine what should be listed. If no paths are provided, 38 // then all of the files under VCS control under dir are returned. An empty 39 // dir is interpretted as [VCS.Root]. A non-empty relative dir is 40 // interpretted relative to [VCS.Root]. It us up to the caller to ensure 41 // that dir and paths are contained by the VCS root Filepaths are relative 42 // to dir and returned in lexical order. 43 // 44 // Note that ListFiles is generally silent in the case an arg is provided 45 // that does correspond to a VCS-controlled file. For example, calling 46 // with an arg of "BANANA" where no such file is controlled by VCS will 47 // result in no filepaths being returned. 48 ListFiles(ctx context.Context, dir string, paths ...string) ([]string, error) 49 50 // Status returns the current state of the repository holding the given paths. 51 // If paths is not provided it implies the state of 52 // the VCS repository in its entirety, including untracked files. paths are 53 // interpretted relative to the [VCS.Root]. 54 Status(ctx context.Context, paths ...string) (Status, error) 55 } 56 57 // Status is the current state of a local repository. 58 type Status struct { 59 Revision string // Optional. 60 CommitTime time.Time // Optional. 61 Uncommitted bool // Required. 62 } 63 64 var vcsTypes = map[string]func(dir string) (VCS, error){ 65 "git": newGitVCS, 66 } 67 68 // New returns a new VCS value representing the 69 // version control system of the given type that 70 // controls the given directory dir. 71 // 72 // It returns an error if a VCS of the specified type 73 // cannot be found. 74 func New(vcsType string, dir string) (VCS, error) { 75 vf := vcsTypes[vcsType] 76 if vf == nil { 77 return nil, fmt.Errorf("unrecognized VCS type %q", vcsType) 78 } 79 return vf(dir) 80 } 81 82 // findRoot inspects dir and its parents to find the VCS repository 83 // signified the presence of one of the given root names. 84 // 85 // If no repository is found, findRoot returns the empty string. 86 func findRoot(dir string, rootNames ...string) string { 87 dir = filepath.Clean(dir) 88 for { 89 if isVCSRoot(dir, rootNames) { 90 return dir 91 } 92 ndir := filepath.Dir(dir) 93 if len(ndir) >= len(dir) { 94 break 95 } 96 dir = ndir 97 } 98 return "" 99 } 100 101 // isVCSRoot identifies a VCS root by checking whether the directory contains 102 // any of the listed root names. 103 func isVCSRoot(dir string, rootNames []string) bool { 104 for _, root := range rootNames { 105 if _, err := os.Stat(filepath.Join(dir, root)); err == nil { 106 // TODO return false if it's not the expected file type. 107 // For now, this is only used by git which can use both 108 // files and directories, so we'll allow either. 109 return true 110 } 111 } 112 return false 113 } 114 115 func runCmd(ctx context.Context, dir string, cmdName string, args ...string) (string, error) { 116 cmd := exec.CommandContext(ctx, cmdName, args...) 117 cmd.Dir = dir 118 119 out, err := cmd.Output() 120 if exitErr, ok := err.(*exec.ExitError); ok { 121 // git's stderr often ends with a newline, which is unnecessary. 122 return "", fmt.Errorf("running %q %q: %v: %s", cmdName, args, err, bytes.TrimSpace(exitErr.Stderr)) 123 } else if err != nil { 124 return "", fmt.Errorf("running %q %q: %v", cmdName, args, err) 125 } 126 return string(out), nil 127 } 128 129 type vcsNotFoundError struct { 130 kind string 131 dir string 132 } 133 134 func (e *vcsNotFoundError) Error() string { 135 return fmt.Sprintf("%s VCS not found in any parent of %q", e.kind, e.dir) 136 } 137 138 func homeEnvName() string { 139 switch runtime.GOOS { 140 case "windows": 141 return "USERPROFILE" 142 case "plan9": 143 return "home" 144 default: 145 return "HOME" 146 } 147 } 148 149 // TestEnv builds an environment so that any executed VCS command with it 150 // won't be affected by the outer level environment. 151 // 152 // Note that this function is exposed so we can reuse it from other test packages 153 // which also need to use Go tests with VCS systems. 154 // Exposing a test helper is fine for now, given this is an internal package. 155 func TestEnv() []string { 156 env := []string{ 157 "PATH=" + os.Getenv("PATH"), 158 homeEnvName() + "=/no-home", 159 } 160 // Must preserve SYSTEMROOT on Windows: https://github.com/golang/go/issues/25513 et al 161 if runtime.GOOS == "windows" { 162 env = append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT")) 163 } 164 return env 165 }