cuelang.org/go@v0.10.1/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 "strings" 28 "testing" 29 "time" 30 ) 31 32 // VCS provides the operations on a particular instance of a VCS. 33 type VCS interface { 34 // Root returns the root of the directory controlled by 35 // the VCS (e.g. the directory containing .git). 36 Root() string 37 38 // ListFiles returns a list of files tracked by VCS, rooted at dir. The 39 // optional paths determine what should be listed. If no paths are provided, 40 // then all of the files under VCS control under dir are returned. An empty 41 // dir is interpretted as [VCS.Root]. A non-empty relative dir is 42 // interpretted relative to [VCS.Root]. It us up to the caller to ensure 43 // that dir and paths are contained by the VCS root Filepaths are relative 44 // to dir and returned in lexical order. 45 // 46 // Note that ListFiles is generally silent in the case an arg is provided 47 // that does correspond to a VCS-controlled file. For example, calling 48 // with an arg of "BANANA" where no such file is controlled by VCS will 49 // result in no filepaths being returned. 50 ListFiles(ctx context.Context, dir string, paths ...string) ([]string, error) 51 52 // Status returns the current state of the repository holding the given paths. 53 // If paths is not provided it implies the state of 54 // the VCS repository in its entirety, including untracked files. paths are 55 // interpretted relative to the [VCS.Root]. 56 Status(ctx context.Context, paths ...string) (Status, error) 57 } 58 59 // Status is the current state of a local repository. 60 type Status struct { 61 Revision string // Optional. 62 CommitTime time.Time // Optional. 63 Uncommitted bool // Required. 64 } 65 66 var vcsTypes = map[string]func(dir string) (VCS, error){ 67 "git": newGitVCS, 68 } 69 70 // New returns a new VCS value representing the 71 // version control system of the given type that 72 // controls the given directory dir. 73 // 74 // It returns an error if a VCS of the specified type 75 // cannot be found. 76 func New(vcsType string, dir string) (VCS, error) { 77 vf := vcsTypes[vcsType] 78 if vf == nil { 79 return nil, fmt.Errorf("unrecognized VCS type %q", vcsType) 80 } 81 return vf(dir) 82 } 83 84 // findRoot inspects dir and its parents to find the VCS repository 85 // signified the presence of one of the given root names. 86 // 87 // If no repository is found, findRoot returns the empty string. 88 func findRoot(dir string, rootNames ...string) string { 89 dir = filepath.Clean(dir) 90 for { 91 if isVCSRoot(dir, rootNames) { 92 return dir 93 } 94 ndir := filepath.Dir(dir) 95 if len(ndir) >= len(dir) { 96 break 97 } 98 dir = ndir 99 } 100 return "" 101 } 102 103 // isVCSRoot identifies a VCS root by checking whether the directory contains 104 // any of the listed root names. 105 func isVCSRoot(dir string, rootNames []string) bool { 106 for _, root := range rootNames { 107 if _, err := os.Stat(filepath.Join(dir, root)); err == nil { 108 // TODO return false if it's not the expected file type. 109 // For now, this is only used by git which can use both 110 // files and directories, so we'll allow either. 111 return true 112 } 113 } 114 return false 115 } 116 117 func runCmd(ctx context.Context, dir string, cmdName string, args ...string) (string, error) { 118 cmd := exec.CommandContext(ctx, cmdName, args...) 119 cmd.Dir = dir 120 121 out, err := cmd.Output() 122 if exitErr, ok := err.(*exec.ExitError); ok { 123 // git's stderr often ends with a newline, which is unnecessary. 124 return "", fmt.Errorf("running %q %q: %v: %s", cmdName, args, err, bytes.TrimSpace(exitErr.Stderr)) 125 } else if err != nil { 126 return "", fmt.Errorf("running %q %q: %v", cmdName, args, err) 127 } 128 return string(out), nil 129 } 130 131 type vcsNotFoundError struct { 132 kind string 133 dir string 134 } 135 136 func (e *vcsNotFoundError) Error() string { 137 return fmt.Sprintf("%s VCS not found in any parent of %q", e.kind, e.dir) 138 } 139 140 func homeEnvName() string { 141 switch runtime.GOOS { 142 case "windows": 143 return "USERPROFILE" 144 case "plan9": 145 return "home" 146 default: 147 return "HOME" 148 } 149 } 150 151 // InitTestEnv sets up the environment so that any executed VCS command 152 // won't be affected by the outer level environment. 153 // 154 // Note that this function is exposed so we can reuse it from other test packages 155 // which also need to use Go tests with VCS systems. 156 // Exposing a test helper is fine for now, given this is an internal package. 157 func InitTestEnv(t testing.TB) { 158 t.Helper() 159 path := os.Getenv("PATH") 160 systemRoot := os.Getenv("SYSTEMROOT") 161 // First unset all environment variables to make a pristine environment. 162 for _, kv := range os.Environ() { 163 key, _, _ := strings.Cut(kv, "=") 164 t.Setenv(key, "") 165 os.Unsetenv(key) 166 } 167 os.Setenv("PATH", path) 168 os.Setenv(homeEnvName(), "/no-home") 169 // Must preserve SYSTEMROOT on Windows: https://github.com/golang/go/issues/25513 et al 170 if runtime.GOOS == "windows" { 171 os.Setenv("SYSTEMROOT", systemRoot) 172 } 173 }