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  }