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 }