6
pkg/vcs/_all/all.go
Normal file
6
pkg/vcs/_all/all.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package all
|
||||
|
||||
import (
|
||||
_ "github.com/StackExchange/blackbox/v2/pkg/vcs/git"
|
||||
_ "github.com/StackExchange/blackbox/v2/pkg/vcs/none"
|
||||
)
|
||||
226
pkg/vcs/git/git.go
Normal file
226
pkg/vcs/git/git.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/blackbox/v2/pkg/bbutil"
|
||||
"github.com/StackExchange/blackbox/v2/pkg/commitlater"
|
||||
"github.com/StackExchange/blackbox/v2/pkg/makesafe"
|
||||
"github.com/StackExchange/blackbox/v2/pkg/vcs"
|
||||
)
|
||||
|
||||
var pluginName = "GIT"
|
||||
|
||||
func init() {
|
||||
vcs.Register(pluginName, 100, newGit)
|
||||
}
|
||||
|
||||
// VcsHandle is the handle
|
||||
type VcsHandle struct {
|
||||
commitTitle string
|
||||
commitHeaderPrinted bool // Has the "NEXT STEPS" header been printed?
|
||||
toCommit *commitlater.List // List of future commits
|
||||
}
|
||||
|
||||
func newGit() (vcs.Vcs, error) {
|
||||
l := &commitlater.List{}
|
||||
return &VcsHandle{toCommit: l}, nil
|
||||
}
|
||||
|
||||
// Name returns my name.
|
||||
func (v VcsHandle) Name() string {
|
||||
return pluginName
|
||||
}
|
||||
|
||||
func ultimate(s string) int { return len(s) - 1 }
|
||||
|
||||
// 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).
|
||||
func (v VcsHandle) Discover() (bool, string) {
|
||||
out, err := bbutil.RunBashOutputSilent("git", "rev-parse", "--show-toplevel")
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
if out == "" {
|
||||
fmt.Printf("WARNING: git rev-parse --show-toplevel has NO output??. Seems broken.")
|
||||
return false, ""
|
||||
}
|
||||
if out[ultimate(out)] == '\n' {
|
||||
out = out[0:ultimate(out)]
|
||||
}
|
||||
return err == nil, out
|
||||
}
|
||||
|
||||
// SetFileTypeUnix informs the VCS that files should maintain unix-style line endings.
|
||||
func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error {
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// Add to the .gitattributes in the same directory as the file.
|
||||
for _, file := range files {
|
||||
d, n := filepath.Split(file)
|
||||
af := filepath.Join(repobasedir, d, ".gitattributes")
|
||||
err := bbutil.Touch(af)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bbutil.AddLinesToFile(af, fmt.Sprintf("%q text eol=lf", n))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seen[af] = true
|
||||
}
|
||||
|
||||
var changedfiles []string
|
||||
for k := range seen {
|
||||
changedfiles = append(changedfiles, k)
|
||||
}
|
||||
|
||||
v.NeedsCommit(
|
||||
"set gitattr=UNIX "+strings.Join(makesafe.RedactMany(files), " "),
|
||||
repobasedir,
|
||||
changedfiles,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo.
|
||||
func (v VcsHandle) IgnoreAnywhere(repobasedir string, files []string) error {
|
||||
// Add to the .gitignore file in the repobasedir.
|
||||
ignore := filepath.Join(repobasedir, ".gitignore")
|
||||
err := bbutil.Touch(ignore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bbutil.AddLinesToFile(ignore, files...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.NeedsCommit(
|
||||
"gitignore "+strings.Join(makesafe.RedactMany(files), " "),
|
||||
repobasedir,
|
||||
[]string{".gitignore"},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitSafeFilename(name string) string {
|
||||
// TODO(tlim): Add unit tests.
|
||||
// TODO(tlim): Confirm that *?[] escaping works.
|
||||
if name == "" {
|
||||
return "ERROR"
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(name) + 2)
|
||||
for _, r := range name {
|
||||
if r == ' ' || r == '*' || r == '?' || r == '[' || r == ']' {
|
||||
b.WriteRune('\\')
|
||||
b.WriteRune(r)
|
||||
} else {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if name[0] == '!' || name[0] == '#' {
|
||||
return `\` + b.String()
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// IgnoreFiles tells the VCS to ignore these files, specified relative to RepoBaseDir.
|
||||
func (v VcsHandle) IgnoreFiles(repobasedir string, files []string) error {
|
||||
|
||||
var lines []string
|
||||
for _, f := range files {
|
||||
lines = append(lines, "/"+gitSafeFilename(f))
|
||||
}
|
||||
|
||||
// Add to the .gitignore file in the repobasedir.
|
||||
ignore := filepath.Join(repobasedir, ".gitignore")
|
||||
err := bbutil.Touch(ignore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bbutil.AddLinesToFile(ignore, lines...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.NeedsCommit(
|
||||
"gitignore "+strings.Join(makesafe.RedactMany(files), " "),
|
||||
repobasedir,
|
||||
[]string{".gitignore"},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add makes a file visible to the VCS (like "git add").
|
||||
func (v VcsHandle) Add(repobasedir string, files []string) error {
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(tlim): Make sure that files are within repobasedir.
|
||||
|
||||
var gpgnames []string
|
||||
for _, n := range files {
|
||||
gpgnames = append(gpgnames, n+".gpg")
|
||||
}
|
||||
return bbutil.RunBash("git", append([]string{"add"}, gpgnames...)...)
|
||||
}
|
||||
|
||||
// CommitTitle indicates what the next commit title will be.
|
||||
// This is used if a group of commits are merged into one.
|
||||
func (v *VcsHandle) CommitTitle(title string) {
|
||||
v.commitTitle = title
|
||||
}
|
||||
|
||||
// NeedsCommit queues up commits for later execution.
|
||||
func (v *VcsHandle) NeedsCommit(message string, repobasedir string, names []string) {
|
||||
v.toCommit.Add(message, repobasedir, names)
|
||||
}
|
||||
|
||||
// DebugCommits dumps the list of future commits.
|
||||
func (v VcsHandle) DebugCommits() commitlater.List {
|
||||
return *v.toCommit
|
||||
}
|
||||
|
||||
// FlushCommits informs the VCS to do queued up commits.
|
||||
func (v VcsHandle) FlushCommits() error {
|
||||
return v.toCommit.Flush(
|
||||
v.commitTitle,
|
||||
func(files []string) error {
|
||||
return bbutil.RunBash("git", append([]string{"add"}, files...)...)
|
||||
},
|
||||
v.suggestCommit,
|
||||
)
|
||||
// TODO(tlim): Some day we can add a command line flag that indicates that commits are
|
||||
// to be done for real, not just suggested to the user. At that point, this function
|
||||
// can call v.toCommit.Flush() with a function that actually does the commits instead
|
||||
// of suggesting them. Flag could be called --commit=auto vs --commit=suggest.
|
||||
}
|
||||
|
||||
// suggestCommit tells the user what commits are needed.
|
||||
func (v *VcsHandle) suggestCommit(messages []string, repobasedir string, files []string) error {
|
||||
if !v.commitHeaderPrinted {
|
||||
fmt.Printf("NEXT STEP: You need to manually check these in:\n")
|
||||
}
|
||||
v.commitHeaderPrinted = true
|
||||
|
||||
fmt.Print(` git commit -m'`, strings.Join(messages, `' -m'`)+`'`)
|
||||
fmt.Print(" ")
|
||||
fmt.Print(strings.Join(makesafe.ShellMany(files), " "))
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
// The following are "secret" functions only used by the integration testing system.
|
||||
|
||||
// TestingInitRepo initializes a repo.
|
||||
func (v VcsHandle) TestingInitRepo() error {
|
||||
return bbutil.RunBash("git", "init")
|
||||
|
||||
}
|
||||
79
pkg/vcs/none/none.go
Normal file
79
pkg/vcs/none/none.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package none
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/StackExchange/blackbox/v2/pkg/commitlater"
|
||||
"github.com/StackExchange/blackbox/v2/pkg/vcs"
|
||||
)
|
||||
|
||||
var pluginName = "NONE"
|
||||
|
||||
func init() {
|
||||
vcs.Register(pluginName, 0, newNone)
|
||||
}
|
||||
|
||||
// VcsHandle is
|
||||
type VcsHandle struct {
|
||||
repoRoot string
|
||||
}
|
||||
|
||||
func newNone() (vcs.Vcs, error) {
|
||||
return &VcsHandle{}, nil
|
||||
}
|
||||
|
||||
// Name returns my name.
|
||||
func (v VcsHandle) Name() string {
|
||||
return pluginName
|
||||
}
|
||||
|
||||
// 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).
|
||||
func (v VcsHandle) Discover() (bool, string) {
|
||||
return true, "" // We don't know the root.
|
||||
}
|
||||
|
||||
//// SetRepoRoot informs the Vcs of the VCS root.
|
||||
//func (v *VcsHandle) SetRepoRoot(dir string) {
|
||||
// v.repoRoot = dir
|
||||
//}
|
||||
|
||||
// SetFileTypeUnix informs the VCS that files should maintain unix-style line endings.
|
||||
func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IgnoreAnywhere tells the VCS to ignore these files anywhere in the repo.
|
||||
func (v VcsHandle) IgnoreAnywhere(repobasedir string, files []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IgnoreFiles tells the VCS to ignore these files anywhere in the repo.
|
||||
func (v VcsHandle) IgnoreFiles(repobasedir string, files []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitTitle sets the title of the next commit.
|
||||
func (v VcsHandle) CommitTitle(title string) {}
|
||||
|
||||
// NeedsCommit queues up commits for later execution.
|
||||
func (v VcsHandle) NeedsCommit(message string, repobasedir string, names []string) {
|
||||
return
|
||||
}
|
||||
|
||||
// DebugCommits dumps a list of future commits.
|
||||
func (v VcsHandle) DebugCommits() commitlater.List {
|
||||
return commitlater.List{}
|
||||
}
|
||||
|
||||
// FlushCommits informs the VCS to do queued up commits.
|
||||
func (v VcsHandle) FlushCommits() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The following are "secret" functions only used by the integration testing system.
|
||||
|
||||
// TestingInitRepo initializes a repo.
|
||||
func (v VcsHandle) TestingInitRepo() error {
|
||||
fmt.Println("VCS=none, TestingInitRepo")
|
||||
return nil
|
||||
}
|
||||
82
pkg/vcs/vcs.go
Normal file
82
pkg/vcs/vcs.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package vcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/blackbox/v2/models"
|
||||
)
|
||||
|
||||
// Vcs is the handle
|
||||
type Vcs interface {
|
||||
models.Vcs
|
||||
}
|
||||
|
||||
// NewFnSig function signature needed by reg.
|
||||
type NewFnSig func() (Vcs, error)
|
||||
|
||||
// Item stores one item
|
||||
type Item struct {
|
||||
Name string
|
||||
New NewFnSig
|
||||
Priority int
|
||||
}
|
||||
|
||||
// Catalog is the list of registered vcs's.
|
||||
var Catalog []*Item
|
||||
|
||||
// Discover polls the VCS plug-ins to determine the VCS of directory.
|
||||
// The first to succeed is returned.
|
||||
// It never returns nil, since "NONE" is always valid.
|
||||
func Discover() (Vcs, string) {
|
||||
for _, v := range Catalog {
|
||||
h, err := v.New()
|
||||
if err != nil {
|
||||
return nil, "" // No idea how that would happen.
|
||||
}
|
||||
if b, repodir := h.Discover(); b {
|
||||
|
||||
// Try to find the rel path from CWD to RepoBase
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: Can not determine cwd! Failing!\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
//fmt.Printf("DISCCOVER: WD=%q REPO=%q\n", wd, repodir)
|
||||
if repodir != wd && strings.HasSuffix(repodir, wd) {
|
||||
// This is a terrible hack. We're basically guessing
|
||||
// at the filesystem layout. That said, it works on macOS.
|
||||
// TODO(tlim): Abstract this out into a separate function
|
||||
// so we can do integration tests on it (to know if it fails on
|
||||
// a particular operating system.)
|
||||
repodir = wd
|
||||
}
|
||||
r, err := filepath.Rel(wd, repodir)
|
||||
if err != nil {
|
||||
// Wait, we're not relative to each other? Give up and
|
||||
// just return the abs repodir.
|
||||
return h, repodir
|
||||
}
|
||||
return h, r
|
||||
}
|
||||
}
|
||||
// This can't happen. If it does, we'll panic and that's ok.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Register a new VCS.
|
||||
func Register(name string, priority int, newfn NewFnSig) {
|
||||
//fmt.Printf("VCS registered: %v\n", name)
|
||||
item := &Item{
|
||||
Name: name,
|
||||
New: newfn,
|
||||
Priority: priority,
|
||||
}
|
||||
Catalog = append(Catalog, item)
|
||||
|
||||
// Keep the list sorted.
|
||||
sort.Slice(Catalog, func(i, j int) bool { return Catalog[j].Priority < Catalog[i].Priority })
|
||||
}
|
||||
Reference in New Issue
Block a user