github.com/matthewdale/lab@v0.14.0/internal/git/git.go (about)

     1  package git
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	retry "github.com/avast/retry-go"
    14  	"github.com/pkg/errors"
    15  	gitconfig "github.com/tcnksm/go-gitconfig"
    16  )
    17  
    18  // IsHub is true when using "hub" as the git binary
    19  var IsHub bool
    20  
    21  func init() {
    22  	_, err := exec.LookPath("hub")
    23  	if err == nil {
    24  		IsHub = true
    25  	}
    26  }
    27  
    28  // New looks up the hub or git binary and returns a cmd which outputs to stdout
    29  func New(args ...string) *exec.Cmd {
    30  	gitPath, err := exec.LookPath("hub")
    31  	if err != nil {
    32  		gitPath, err = exec.LookPath("git")
    33  		if err != nil {
    34  			log.Fatal(err)
    35  		}
    36  	}
    37  
    38  	cmd := exec.Command(gitPath, args...)
    39  	cmd.Stdin = os.Stdin
    40  	cmd.Stdout = os.Stdout
    41  	cmd.Stderr = os.Stderr
    42  	return cmd
    43  }
    44  
    45  // GitDir returns the full path to the .git directory
    46  func GitDir() (string, error) {
    47  	cmd := New("rev-parse", "-q", "--git-dir")
    48  	cmd.Stdout = nil
    49  	d, err := cmd.Output()
    50  	if err != nil {
    51  		return "", err
    52  	}
    53  	dir := string(d)
    54  	dir = strings.TrimSpace(dir)
    55  	if !filepath.IsAbs(dir) {
    56  		dir, err = filepath.Abs(dir)
    57  		if err != nil {
    58  			return "", err
    59  		}
    60  	}
    61  
    62  	return filepath.Clean(dir), nil
    63  }
    64  
    65  // WorkingDir returns the full pall to the root of the current git repository
    66  func WorkingDir() (string, error) {
    67  	cmd := New("rev-parse", "--show-toplevel")
    68  	cmd.Stdout = nil
    69  	d, err := cmd.Output()
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  	return strings.TrimSpace(string(d)), nil
    74  }
    75  
    76  // CommentChar returns active comment char and defaults to '#'
    77  func CommentChar() string {
    78  	char, err := gitconfig.Entire("core.commentchar")
    79  	if err == nil {
    80  		return char
    81  	}
    82  	return "#"
    83  }
    84  
    85  // LastCommitMessage returns the last commits message as one line
    86  func LastCommitMessage() (string, error) {
    87  	cmd := New("show", "-s", "--format=%s%n%+b", "HEAD")
    88  	cmd.Stdout = nil
    89  	msg, err := cmd.Output()
    90  	if err != nil {
    91  		return "", err
    92  	}
    93  	return strings.TrimSpace(string(msg)), nil
    94  }
    95  
    96  // Log produces a formatted gitlog between 2 git shas
    97  func Log(sha1, sha2 string) (string, error) {
    98  	cmd := New("-c", "log.showSignature=false",
    99  		"log",
   100  		"--no-color",
   101  		"--format=%h (%aN, %ar)%n%w(78,3,3)%s%n",
   102  		"--cherry",
   103  		fmt.Sprintf("%s...%s", sha1, sha2))
   104  	cmd.Stdout = nil
   105  	outputs, err := cmd.Output()
   106  	if err != nil {
   107  		return "", errors.Errorf("Can't load git log %s..%s", sha1, sha2)
   108  	}
   109  
   110  	return string(outputs), nil
   111  }
   112  
   113  // CurrentBranch returns the currently checked out branch and strips away all
   114  // but the branchname itself.
   115  func CurrentBranch() (string, error) {
   116  	cmd := New("branch")
   117  	cmd.Stdout = nil
   118  	gBranches, err := cmd.Output()
   119  	if err != nil {
   120  		return "", err
   121  	}
   122  	branches := strings.Split(string(gBranches), "\n")
   123  	var branch string
   124  	for _, b := range branches {
   125  		if strings.HasPrefix(b, "* ") {
   126  			branch = b
   127  			break
   128  		}
   129  	}
   130  	if branch == "" {
   131  		return "", errors.New("current branch could not be determined")
   132  	}
   133  	branch = strings.TrimPrefix(branch, "* ")
   134  	branch = strings.TrimSpace(branch)
   135  	return branch, nil
   136  }
   137  
   138  // PathWithNameSpace returns the owner/repository for the current repo
   139  // Such as zaquestion/lab
   140  // Respects GitLab subgroups (https://docs.gitlab.com/ce/user/group/subgroups/)
   141  func PathWithNameSpace(remote string) (string, error) {
   142  	remoteURL, err := gitconfig.Local("remote." + remote + ".url")
   143  	if err != nil {
   144  		return "", err
   145  	}
   146  
   147  	parts := strings.Split(remoteURL, "//")
   148  
   149  	if len(parts) == 1 {
   150  		// scp-like short syntax (e.g. git@gitlab.com...)
   151  		part := parts[0]
   152  		parts = strings.Split(part, ":")
   153  	} else if len(parts) == 2 {
   154  		// every other protocol syntax (e.g. ssh://, http://, git://)
   155  		part := parts[1]
   156  		parts = strings.SplitN(part, "/", 2)
   157  	} else {
   158  		return "", errors.Errorf("cannot parse remote: %s url: %s", remote, remoteURL)
   159  	}
   160  
   161  	if len(parts) != 2 {
   162  		return "", errors.Errorf("cannot parse remote: %s url: %s", remote, remoteURL)
   163  	}
   164  	path := parts[1]
   165  	path = strings.TrimSuffix(path, ".git")
   166  	return path, nil
   167  }
   168  
   169  // RepoName returns the name of the repository, such as "lab"
   170  func RepoName() (string, error) {
   171  	o, err := PathWithNameSpace("origin")
   172  	if err != nil {
   173  		return "", err
   174  	}
   175  	parts := strings.Split(o, "/")
   176  	return parts[len(parts)-1:][0], nil
   177  }
   178  
   179  // RemoteAdd both adds a remote and fetches it
   180  func RemoteAdd(name, url, dir string) error {
   181  	cmd := New("remote", "add", name, url)
   182  	cmd.Dir = dir
   183  	if err := cmd.Run(); err != nil {
   184  		return err
   185  	}
   186  	fmt.Println("Updating", name)
   187  
   188  	err := retry.Do(func() error {
   189  		cmd = New("fetch", name)
   190  		cmd.Dir = dir
   191  		return cmd.Run()
   192  	}, retry.Attempts(3), retry.Delay(time.Second), retry.Units(time.Nanosecond))
   193  	if err != nil {
   194  		return err
   195  	}
   196  	fmt.Println("new remote:", name)
   197  	return nil
   198  }
   199  
   200  // IsRemote returns true when passed a valid remote in the git repo
   201  func IsRemote(remote string) (bool, error) {
   202  	cmd := New("remote")
   203  	cmd.Stdout = nil
   204  	cmd.Stderr = nil
   205  	remotes, err := cmd.Output()
   206  	if err != nil {
   207  		return false, err
   208  	}
   209  
   210  	return bytes.Contains(remotes, []byte(remote+"\n")), nil
   211  }
   212  
   213  // InsideGitRepo returns true when the current working directory is inside the
   214  // working tree of a git repo
   215  func InsideGitRepo() bool {
   216  	cmd := New("rev-parse", "--is-inside-work-tree")
   217  	cmd.Stdout = nil
   218  	cmd.Stderr = nil
   219  	out, _ := cmd.CombinedOutput()
   220  	return bytes.Contains(out, []byte("true\n"))
   221  }