github.com/StackExchange/blackbox/v2@v2.0.1-0.20220331193400-d84e904973ab/pkg/vcs/git/git.go (about)

     1  package git
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	"github.com/StackExchange/blackbox/v2/pkg/bbutil"
     9  	"github.com/StackExchange/blackbox/v2/pkg/commitlater"
    10  	"github.com/StackExchange/blackbox/v2/pkg/makesafe"
    11  	"github.com/StackExchange/blackbox/v2/pkg/vcs"
    12  )
    13  
    14  var pluginName = "GIT"
    15  
    16  func init() {
    17  	vcs.Register(pluginName, 100, newGit)
    18  }
    19  
    20  // VcsHandle is the handle
    21  type VcsHandle struct {
    22  	commitTitle         string
    23  	commitHeaderPrinted bool              // Has the "NEXT STEPS" header been printed?
    24  	toCommit            *commitlater.List // List of future commits
    25  }
    26  
    27  func newGit() (vcs.Vcs, error) {
    28  	l := &commitlater.List{}
    29  	return &VcsHandle{toCommit: l}, nil
    30  }
    31  
    32  // Name returns my name.
    33  func (v VcsHandle) Name() string {
    34  	return pluginName
    35  }
    36  
    37  func ultimate(s string) int { return len(s) - 1 }
    38  
    39  // Discover returns true if we are a repo of this type; along with the Abs path to the repo root (or "" if we don't know).
    40  func (v VcsHandle) Discover() (bool, string) {
    41  	out, err := bbutil.RunBashOutputSilent("git", "rev-parse", "--show-toplevel")
    42  	if err != nil {
    43  		return false, ""
    44  	}
    45  	if out == "" {
    46  		fmt.Printf("WARNING: git rev-parse --show-toplevel has NO output??.  Seems broken.")
    47  		return false, ""
    48  	}
    49  	if out[ultimate(out)] == '\n' {
    50  		out = out[0:ultimate(out)]
    51  	}
    52  	return err == nil, out
    53  }
    54  
    55  // SetFileTypeUnix informs the VCS that files should maintain unix-style line endings.
    56  func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error {
    57  	seen := make(map[string]bool)
    58  
    59  	// Add to the .gitattributes in the same directory as the file.
    60  	for _, file := range files {
    61  		d, n := filepath.Split(file)
    62  		af := filepath.Join(repobasedir, d, ".gitattributes")
    63  		err := bbutil.Touch(af)
    64  		if err != nil {
    65  			return err
    66  		}
    67  		err = bbutil.AddLinesToFile(af, fmt.Sprintf("%q text eol=lf", n))
    68  		if err != nil {
    69  			return err
    70  		}
    71  		seen[af] = true
    72  	}
    73  
    74  	var changedfiles []string
    75  	for k := range seen {
    76  		changedfiles = append(changedfiles, k)
    77  	}
    78  
    79  	v.NeedsCommit(
    80  		"set gitattr=UNIX "+strings.Join(makesafe.RedactMany(files), " "),
    81  		repobasedir,
    82  		changedfiles,
    83  	)
    84  
    85  	return nil
    86  }
    87  
    88  // IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo.
    89  func (v VcsHandle) IgnoreAnywhere(repobasedir string, files []string) error {
    90  	// Add to the .gitignore file in the repobasedir.
    91  	ignore := filepath.Join(repobasedir, ".gitignore")
    92  	err := bbutil.Touch(ignore)
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	err = bbutil.AddLinesToFile(ignore, files...)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	v.NeedsCommit(
   103  		"gitignore "+strings.Join(makesafe.RedactMany(files), " "),
   104  		repobasedir,
   105  		[]string{".gitignore"},
   106  	)
   107  	return nil
   108  }
   109  
   110  func gitSafeFilename(name string) string {
   111  	// TODO(tlim): Add unit tests.
   112  	// TODO(tlim): Confirm that *?[] escaping works.
   113  	if name == "" {
   114  		return "ERROR"
   115  	}
   116  	var b strings.Builder
   117  	b.Grow(len(name) + 2)
   118  	for _, r := range name {
   119  		if r == ' ' || r == '*' || r == '?' || r == '[' || r == ']' {
   120  			b.WriteRune('\\')
   121  			b.WriteRune(r)
   122  		} else {
   123  			b.WriteRune(r)
   124  		}
   125  	}
   126  	if name[0] == '!' || name[0] == '#' {
   127  		return `\` + b.String()
   128  	}
   129  	return b.String()
   130  }
   131  
   132  // IgnoreFiles tells the VCS to ignore these files, specified relative to RepoBaseDir.
   133  func (v VcsHandle) IgnoreFiles(repobasedir string, files []string) error {
   134  
   135  	var lines []string
   136  	for _, f := range files {
   137  		lines = append(lines, "/"+gitSafeFilename(f))
   138  	}
   139  
   140  	// Add to the .gitignore file in the repobasedir.
   141  	ignore := filepath.Join(repobasedir, ".gitignore")
   142  	err := bbutil.Touch(ignore)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	err = bbutil.AddLinesToFile(ignore, lines...)
   147  	if err != nil {
   148  		return err
   149  	}
   150  
   151  	v.NeedsCommit(
   152  		"gitignore "+strings.Join(makesafe.RedactMany(files), " "),
   153  		repobasedir,
   154  		[]string{".gitignore"},
   155  	)
   156  	return nil
   157  }
   158  
   159  // Add makes a file visible to the VCS (like "git add").
   160  func (v VcsHandle) Add(repobasedir string, files []string) error {
   161  
   162  	if len(files) == 0 {
   163  		return nil
   164  	}
   165  
   166  	// TODO(tlim): Make sure that files are within repobasedir.
   167  
   168  	var gpgnames []string
   169  	for _, n := range files {
   170  		gpgnames = append(gpgnames, n+".gpg")
   171  	}
   172  	return bbutil.RunBash("git", append([]string{"add"}, gpgnames...)...)
   173  }
   174  
   175  // CommitTitle indicates what the next commit title will be.
   176  // This is used if a group of commits are merged into one.
   177  func (v *VcsHandle) CommitTitle(title string) {
   178  	v.commitTitle = title
   179  }
   180  
   181  // NeedsCommit queues up commits for later execution.
   182  func (v *VcsHandle) NeedsCommit(message string, repobasedir string, names []string) {
   183  	v.toCommit.Add(message, repobasedir, names)
   184  }
   185  
   186  // DebugCommits dumps the list of future commits.
   187  func (v VcsHandle) DebugCommits() commitlater.List {
   188  	return *v.toCommit
   189  }
   190  
   191  // FlushCommits informs the VCS to do queued up commits.
   192  func (v VcsHandle) FlushCommits() error {
   193  	return v.toCommit.Flush(
   194  		v.commitTitle,
   195  		func(files []string) error {
   196  			return bbutil.RunBash("git", append([]string{"add"}, files...)...)
   197  		},
   198  		v.suggestCommit,
   199  	)
   200  	// TODO(tlim): Some day we can add a command line flag that indicates that commits are
   201  	// to be done for real, not just suggested to the user.  At that point, this function
   202  	// can call v.toCommit.Flush() with a function that actually does the commits instead
   203  	// of suggesting them.  Flag could be called --commit=auto vs --commit=suggest.
   204  }
   205  
   206  // suggestCommit tells the user what commits are needed.
   207  func (v *VcsHandle) suggestCommit(messages []string, repobasedir string, files []string) error {
   208  	if !v.commitHeaderPrinted {
   209  		fmt.Printf("NEXT STEP: You need to manually check these in:\n")
   210  	}
   211  	v.commitHeaderPrinted = true
   212  
   213  	fmt.Print(`     git commit -m'`, strings.Join(messages, `' -m'`)+`'`)
   214  	fmt.Print(" ")
   215  	fmt.Print(strings.Join(makesafe.ShellMany(files), " "))
   216  	fmt.Println()
   217  	return nil
   218  }
   219  
   220  // The following are "secret" functions only used by the integration testing system.
   221  
   222  // TestingInitRepo initializes a repo.
   223  func (v VcsHandle) TestingInitRepo() error {
   224  	return bbutil.RunBash("git", "init")
   225  
   226  }