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  }