github.com/decred/politeia@v1.4.0/politeiad/backend/gitbe/git.go (about)

     1  // Copyright (c) 2017-2019 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package gitbe
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"crypto/sha1"
    11  	"encoding/hex"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"strings"
    18  )
    19  
    20  // gitError contains all the components of a git invocation.
    21  type gitError struct {
    22  	cmd    []string
    23  	stdout []string
    24  	stderr []string
    25  }
    26  
    27  // log pretty prints a gitError.
    28  func (e gitError) log() {
    29  	var cmd string
    30  	for _, v := range e.cmd {
    31  		cmd += v + " "
    32  	}
    33  	log.Infof("Git command     : %v", cmd)
    34  	s := "Git stdout :"
    35  	for _, v := range e.stdout {
    36  		log.Infof("%v %v", s, v)
    37  		s = ""
    38  	}
    39  	s = "Git stderr :"
    40  	for _, v := range e.stderr {
    41  		log.Infof("%v %v", s, v)
    42  		s = ""
    43  	}
    44  }
    45  
    46  func outputReader(r io.Reader) ([]string, error) {
    47  	rv := make([]string, 0, 128)
    48  	scanner := bufio.NewScanner(r)
    49  	for scanner.Scan() {
    50  		rv = append(rv, scanner.Text())
    51  	}
    52  	return rv, scanner.Err()
    53  }
    54  
    55  // git excutes the git command using the provided arguments.  If the path
    56  // argument is set it'll be copied to the GIT_DIR environment variable.
    57  func (g *gitBackEnd) git(path string, args ...string) ([]string, error) {
    58  	if len(args) == 0 {
    59  		return nil, fmt.Errorf("git requires arguments")
    60  	}
    61  
    62  	// Setup gitError
    63  	ge := gitError{
    64  		cmd:    make([]string, 0, len(args)+1),
    65  		stdout: make([]string, 0, 128),
    66  		stderr: make([]string, 0, 128),
    67  	}
    68  
    69  	ge.cmd = append(ge.cmd, g.gitPath)
    70  	ge.cmd = append(ge.cmd, args...)
    71  
    72  	if g.gitTrace {
    73  		defer func() { ge.log() }()
    74  	}
    75  
    76  	// Execute git command
    77  	var stdout, stderr bytes.Buffer
    78  	cmd := exec.Command(g.gitPath, args...)
    79  	cmd.Stdout = &stdout
    80  	cmd.Stderr = &stderr
    81  
    82  	// Determine if we need to set GIT_DIR
    83  	if path != "" {
    84  		cmd.Dir = path
    85  	}
    86  
    87  	doneError := cmd.Run()
    88  
    89  	// Prepare output
    90  	var err error
    91  	ge.stdout, err = outputReader(bytes.NewReader(stdout.Bytes()))
    92  	if err != nil {
    93  		log.Errorf("git stdout scanner error: %v", err)
    94  	}
    95  	ge.stderr, err = outputReader(bytes.NewReader(stderr.Bytes()))
    96  	if err != nil {
    97  		log.Errorf("git stderr scanner error: %v", err)
    98  	}
    99  
   100  	return ge.stdout, doneError
   101  }
   102  
   103  // gitVersion returns the version of git.
   104  func (g *gitBackEnd) gitVersion() (string, error) {
   105  	out, err := g.git("", "version")
   106  	if err != nil {
   107  		return "", err
   108  	}
   109  
   110  	if len(out) != 1 {
   111  		return "", fmt.Errorf("unexpected git output")
   112  	}
   113  
   114  	return out[0], nil
   115  }
   116  
   117  func (g *gitBackEnd) gitHasChanges(path string) (rv bool) {
   118  	if _, err := g.git(path, "diff", "--quiet"); err != nil {
   119  		rv = true
   120  	} else if _, err := g.git(path, "diff", "--cached",
   121  		"--quiet"); err != nil {
   122  		rv = true
   123  	}
   124  	return rv
   125  }
   126  
   127  func (g *gitBackEnd) gitDiff(path string) ([]string, error) {
   128  	return g.git(path, "diff")
   129  }
   130  
   131  func (g *gitBackEnd) gitStash(path string) error {
   132  	_, err := g.git(path, "stash")
   133  	return err
   134  }
   135  
   136  func (g *gitBackEnd) gitStashDrop(path string) error {
   137  	_, err := g.git(path, "stash", "drop")
   138  	return err
   139  }
   140  
   141  func (g *gitBackEnd) gitRm(path, filename string, force bool) error {
   142  	var err error
   143  	if force {
   144  		_, err = g.git(path, "rm", "-f", filename)
   145  	} else {
   146  		_, err = g.git(path, "rm", filename)
   147  	}
   148  	return err
   149  }
   150  
   151  func (g *gitBackEnd) gitAdd(path, filename string) error {
   152  	_, err := g.git(path, "add", filename)
   153  	return err
   154  }
   155  
   156  func (g *gitBackEnd) gitCommit(path, message string) error {
   157  	_, err := g.git(path, "commit", "-m", message)
   158  	return err
   159  }
   160  
   161  func (g *gitBackEnd) gitCheckout(path, branch string) error {
   162  	_, err := g.git(path, "checkout", branch)
   163  	return err
   164  }
   165  
   166  func (g *gitBackEnd) gitBranchDelete(path, branch string) error {
   167  	_, err := g.git(path, "branch", "-D", branch)
   168  	return err
   169  }
   170  
   171  func (g *gitBackEnd) gitClean(path string) error {
   172  	_, err := g.git(path, "clean", "-xdf")
   173  	return err
   174  }
   175  
   176  func (g *gitBackEnd) gitBranches(path string) ([]string, error) {
   177  	branches, err := g.git(path, "branch")
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	b := make([]string, 0, len(branches))
   183  	for _, v := range branches {
   184  		b = append(b, strings.Trim(v, " *\t\n"))
   185  	}
   186  
   187  	return b, nil
   188  }
   189  
   190  func (g *gitBackEnd) gitBranchNow(path string) (string, error) {
   191  	branches, err := g.git(path, "branch")
   192  	if err != nil {
   193  		return "", err
   194  	}
   195  
   196  	for _, v := range branches {
   197  		if strings.Contains(v, "*") {
   198  			return strings.Trim(v, " *\t\n"), nil
   199  		}
   200  	}
   201  
   202  	return "", fmt.Errorf("unexpected git output")
   203  }
   204  
   205  func (g *gitBackEnd) gitPull(path string, fastForward bool) error {
   206  	var err error
   207  	if fastForward {
   208  		_, err = g.git(path, "pull", "--ff-only", "--rebase")
   209  	} else {
   210  		_, err = g.git(path, "pull")
   211  	}
   212  	if err != nil {
   213  		return err
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  func (g *gitBackEnd) gitRebase(path, branch string) error {
   220  	_, err := g.git(path, "rebase", branch)
   221  	return err
   222  }
   223  
   224  func (g *gitBackEnd) gitPush(path, remote, branch string, upstream bool) error {
   225  	var err error
   226  	if upstream {
   227  		_, err = g.git(path, "push", "--set-upstream", remote, branch)
   228  	} else {
   229  		_, err = g.git(path, "push")
   230  	}
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	return nil
   236  }
   237  
   238  func (g *gitBackEnd) gitUnwind(path string) error {
   239  	_, err := g.git(path, "checkout", "-f")
   240  	if err != nil {
   241  		return err
   242  	}
   243  	return g.gitClean(path)
   244  }
   245  
   246  func (g *gitBackEnd) gitUnwindBranch(path, branch string) error {
   247  	err := g.gitUnwind(path)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	err = g.gitCheckout(path, "master")
   252  	if err != nil {
   253  		return err
   254  	}
   255  	return g.gitBranchDelete(path, branch)
   256  }
   257  
   258  func (g *gitBackEnd) gitNewBranch(path, branch string) error {
   259  	_, err := g.git(path, "checkout", "-b", branch)
   260  	return err
   261  }
   262  
   263  func (g *gitBackEnd) gitLastDigest(path string) ([]byte, error) {
   264  	out, err := g.git(path, "log", "--pretty=oneline", "-n 1")
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	if len(out) == 0 {
   269  		return nil, fmt.Errorf("invalid git output")
   270  	}
   271  
   272  	// Returned data is "<digest> <commit message>"
   273  	ds := strings.SplitN(out[0], " ", 2)
   274  	if len(ds) == 0 {
   275  		return nil, fmt.Errorf("invalid log")
   276  	}
   277  
   278  	d, err := hex.DecodeString(ds[0])
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	if len(d) != sha1.Size {
   284  		return nil, fmt.Errorf("invalid sha1 size")
   285  	}
   286  
   287  	return d, nil
   288  }
   289  
   290  func (g *gitBackEnd) gitLog(path string) ([]string, error) {
   291  	out, err := g.git(path, "log")
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  
   296  	return out, nil
   297  }
   298  
   299  func (g *gitBackEnd) gitFsck(path string) ([]string, error) {
   300  	out, err := g.git(path, "fsck", "--full", "--strict")
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  
   305  	return out, nil
   306  }
   307  
   308  // gitConfig sets a config value for the provided repo.
   309  func (g *gitBackEnd) gitConfig(path, name, value string) error {
   310  	_, err := g.git(path, "config", name, value)
   311  	return err
   312  }
   313  
   314  // gitClone clones a git repository.  This functions exits without an error
   315  // if the directory is already a git repo.
   316  func (g *gitBackEnd) gitClone(from, to string, repoConfig map[string]string) error {
   317  	_, err := os.Stat(filepath.Join(from, ".git"))
   318  	if os.IsNotExist(err) {
   319  		return fmt.Errorf("source repo does not exist")
   320  	}
   321  	_, err = os.Stat(filepath.Join(to, ".git"))
   322  	if !os.IsNotExist(err) {
   323  		return err
   324  	}
   325  
   326  	log.Infof("Cloning git repo %v to %v", from, to)
   327  
   328  	// Clone the repo (with config, if applicable).
   329  	args := []string{"clone", from, to}
   330  	for k, v := range repoConfig {
   331  		args = append(args, "-c", k+"="+v)
   332  	}
   333  	_, err = g.git("", args...)
   334  	return err
   335  }
   336  
   337  // gitInit initializes a new repository.  If the repository exists
   338  // it does not reinit it; it reutns failure instead.
   339  func (g *gitBackEnd) gitInit(path string) (string, error) {
   340  	out, err := g.git("", "init", path)
   341  	if err != nil {
   342  		return "", err
   343  	}
   344  
   345  	if len(out) != 1 {
   346  		return "", fmt.Errorf("unexpected git output")
   347  	}
   348  
   349  	return out[0], nil
   350  }
   351  
   352  // gitInitRepo initializes a directory as a git repo.  This functions exits
   353  // without an error if the directory is already a git repo.  The git repo is
   354  // initialized with a .gitignore file so that a) have a master and b) always
   355  // ignore the lock file.
   356  func (g *gitBackEnd) gitInitRepo(path string, repoConfig map[string]string) error {
   357  	_, err := os.Stat(filepath.Join(path, ".git"))
   358  	// This test is unreadable but correct.
   359  	if !os.IsNotExist(err) {
   360  		return err
   361  	}
   362  
   363  	// Containing directory
   364  	log.Infof("Initializing git repo: %v", path)
   365  	err = os.MkdirAll(path, 0755)
   366  	if err != nil {
   367  		return err
   368  	}
   369  
   370  	// Initialize git repo
   371  	_, err = g.gitInit(path)
   372  	if err != nil {
   373  		return err
   374  	}
   375  
   376  	// Apply repo config
   377  	for k, v := range repoConfig {
   378  		err = g.gitConfig(path, k, v)
   379  		if err != nil {
   380  			return err
   381  		}
   382  	}
   383  
   384  	// Add empty .gitignore
   385  	// This makes the repo ready to go and we'll always use this as the
   386  	// initial commit.
   387  	err = os.WriteFile(filepath.Join(path, ".gitignore"), []byte{},
   388  		0664)
   389  	if err != nil {
   390  		return err
   391  	}
   392  	err = g.gitAdd(path, ".gitignore")
   393  	if err != nil {
   394  		return err
   395  	}
   396  
   397  	err = g.gitCommit(path, "Add .gitignore")
   398  	if err != nil {
   399  		return err
   400  	}
   401  
   402  	// Add README.md
   403  	err = os.WriteFile(filepath.Join(path, "README.md"),
   404  		[]byte(defaultReadme), 0644)
   405  	if err != nil {
   406  		return err
   407  	}
   408  	err = g.gitAdd(path, "README.md")
   409  	if err != nil {
   410  		return err
   411  	}
   412  
   413  	return g.gitCommit(path, "Add README.md")
   414  }