github.com/zaquestion/lab@v0.25.1/internal/git/git.go (about) 1 package git 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "regexp" 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 giturls "github.com/whilp/git-urls" 17 "github.com/zaquestion/lab/internal/logger" 18 ) 19 20 // Get internal lab logger instance 21 var log = logger.GetInstance() 22 23 // New looks up the git binary and returns a cmd which outputs to stdout 24 func New(args ...string) *exec.Cmd { 25 gitPath, err := exec.LookPath("git") 26 if err != nil { 27 log.Fatal(err) 28 } 29 30 cmd := exec.Command(gitPath, args...) 31 cmd.Stdin = os.Stdin 32 cmd.Stdout = os.Stdout 33 cmd.Stderr = os.Stderr 34 return cmd 35 } 36 37 // Dir returns the full path to the .git directory 38 func Dir() (string, error) { 39 cmd := New("rev-parse", "-q", "--git-dir") 40 cmd.Stdout = nil 41 cmd.Stderr = nil 42 d, err := cmd.Output() 43 if err != nil { 44 return "", err 45 } 46 dir := string(d) 47 dir = strings.TrimSpace(dir) 48 if !filepath.IsAbs(dir) { 49 dir, err = filepath.Abs(dir) 50 if err != nil { 51 return "", err 52 } 53 } 54 55 return filepath.Clean(dir), nil 56 } 57 58 // WorkingDir returns the full path to the root of the current git repository 59 func WorkingDir() (string, error) { 60 cmd := New("rev-parse", "--show-toplevel") 61 cmd.Stdout = nil 62 d, err := cmd.Output() 63 if err != nil { 64 return "", err 65 } 66 return strings.TrimSpace(string(d)), nil 67 } 68 69 // CommentChar returns active comment char and defaults to '#' 70 func CommentChar() string { 71 char, err := gitconfig.Entire("core.commentchar") 72 if err == nil { 73 return char 74 } 75 return "#" 76 } 77 78 // PagerCommand returns the commandline and environment for the pager 79 func PagerCommand() (string, []string) { 80 // Set up environment for common pagers, see the documentation 81 // for "core.pager" in git-config(1) 82 env := os.Environ() 83 if _, ok := os.LookupEnv("LESS"); !ok { 84 env = append(env, "LESS=FRX") 85 } 86 if _, ok := os.LookupEnv("LESSSECURE"); !ok { 87 env = append(env, "LESSSECURE=1") 88 } 89 if _, ok := os.LookupEnv("LV"); !ok { 90 env = append(env, "LV=-c") 91 } 92 93 // Find an appropriate pager command, following git's preference 94 cmd, ok := os.LookupEnv("GIT_PAGER") 95 if ok { 96 return cmd, env 97 } 98 cmd, err := gitconfig.Entire("core.pager") 99 if err == nil { 100 return cmd, env 101 } 102 cmd, ok = os.LookupEnv("PAGER") 103 if ok { 104 return cmd, env 105 } 106 return "less", env 107 } 108 109 // LastCommitMessage returns the last commits message as one line 110 func LastCommitMessage(sha string) (string, error) { 111 cmd := New("show", "-s", "--format=%s%n%+b", sha) 112 cmd.Stdout = nil 113 msg, err := cmd.Output() 114 if err != nil { 115 return "", err 116 } 117 return strings.TrimSpace(string(msg)), nil 118 } 119 120 // Log produces a formatted gitlog between 2 git shas 121 func Log(sha1, sha2 string) (string, error) { 122 cmd := New("-c", "log.showSignature=false", 123 "log", 124 "--no-color", 125 "--format=%h (%aN)%n%w(78,3,3)%s%n", 126 "--cherry", 127 fmt.Sprintf("%s..%s", sha1, sha2)) 128 cmd.Stdout = nil 129 outputs, err := cmd.Output() 130 if err != nil { 131 return "", errors.Errorf("Can't load git log %s..%s", sha1, sha2) 132 } 133 134 diffCmd := New("diff", "--stat", fmt.Sprintf("%s...%s", sha1, sha2)) 135 diffCmd.Stdout = nil 136 diffOutput, err := diffCmd.Output() 137 if err != nil { 138 return "", errors.Errorf("Can't load diffstat") 139 } 140 141 return string(outputs) + string(diffOutput), nil 142 } 143 144 // CurrentBranch returns the currently checked out branch 145 func CurrentBranch() (string, error) { 146 cmd := New("rev-parse", "--abbrev-ref", "HEAD") 147 cmd.Stdout = nil 148 branch, err := cmd.Output() 149 if err != nil { 150 return "", err 151 } 152 return strings.TrimSpace(string(branch)), nil 153 } 154 155 // RevParse returns the output of "git rev-parse". 156 func RevParse(args ...string) (string, error) { 157 cmd := New(append([]string{"rev-parse"}, args...)...) 158 cmd.Stdout = nil 159 d, err := cmd.Output() 160 if err != nil { 161 return "", err 162 } 163 return strings.TrimSpace(string(d)), nil 164 } 165 166 // UpstreamBranch returns the upstream of the specified branch 167 func UpstreamBranch(branch string) (string, error) { 168 upstreamBranch, err := gitconfig.Local("branch." + branch + ".merge") 169 if err != nil { 170 return "", errors.Errorf("No upstream for branch '%s'", branch) 171 } 172 return strings.TrimPrefix(upstreamBranch, "refs/heads/"), nil 173 } 174 175 // PathWithNamespace returns the owner/repository for the current repo 176 // Such as zaquestion/lab 177 // Respects GitLab subgroups (https://docs.gitlab.com/ce/user/group/subgroups/) 178 func PathWithNamespace(remote string) (string, error) { 179 remoteURL, err := gitconfig.Local("remote." + remote + ".pushurl") 180 if err != nil || remoteURL == "" { 181 remoteURL, err = gitconfig.Local("remote." + remote + ".url") 182 if err != nil { 183 return "", err 184 } 185 if remoteURL == "" { 186 // Branches can track remote based on ther URL, thus we don't 187 // really have a remote entity in the git config, but only the 188 // URL of the remote. 189 // https://git-scm.com/docs/git-push#Documentation/git-push.txt-ltrepositorygt 190 remoteURL = remote 191 } 192 } 193 194 u, err := giturls.Parse(remoteURL) 195 if err != nil { 196 return "", err 197 } 198 199 // remote URLs can't refer to other files or local paths, ie., other remote 200 // names. 201 if u.Scheme == "file" { 202 return "", errors.Errorf("invalid remote URL format for %s", remote) 203 } 204 205 path := strings.TrimPrefix(u.Path, "/") 206 path = strings.TrimSuffix(path, ".git") 207 return path, nil 208 } 209 210 // RepoName returns the name of the repository, such as "lab" 211 func RepoName() (string, error) { 212 o, err := PathWithNamespace("origin") 213 if err != nil { 214 return "", err 215 } 216 parts := strings.Split(o, "/") 217 return parts[len(parts)-1:][0], nil 218 } 219 220 // RemoteAdd both adds a remote and fetches it 221 func RemoteAdd(name, url, dir string) error { 222 cmd := New("remote", "add", name, url) 223 cmd.Dir = dir 224 if err := cmd.Run(); err != nil { 225 return err 226 } 227 fmt.Println("Updating", name) 228 229 err := retry.Do(func() error { 230 cmd = New("fetch", name) 231 cmd.Dir = dir 232 return cmd.Run() 233 }, retry.Attempts(3), retry.Delay(time.Second)) 234 if err != nil { 235 return err 236 } 237 fmt.Println("new remote:", name) 238 return nil 239 } 240 241 // Remotes get the list of remotes available in the current repo dir 242 func Remotes() ([]string, error) { 243 cmd := New("remote") 244 cmd.Stderr = nil 245 cmd.Stdout = nil 246 out, err := cmd.Output() 247 if err != nil { 248 return nil, err 249 } 250 251 names := strings.Split(string(out), "\n") 252 253 return names, nil 254 } 255 256 // RemoteBranches get the list of branches the specified remote has 257 func RemoteBranches(remote string) ([]string, error) { 258 cmd := New("for-each-ref", "refs/remotes/") 259 cmd.Stderr = nil 260 cmd.Stdout = nil 261 out, err := cmd.Output() 262 if err != nil { 263 return nil, err 264 } 265 266 refsData := strings.Split(string(out), "\n") 267 re := regexp.MustCompile(`^refs/remotes/[^/]+/`) 268 269 names := []string{} 270 for _, refData := range refsData { 271 // refData = <sha> <objtype>\t<refname> 272 dataParts := strings.Split(refData, "\t") 273 refname := dataParts[len(dataParts)-1] 274 if strings.HasPrefix(refname, "refs/remotes/"+remote) { 275 names = append(names, re.ReplaceAllString(refname, "")) 276 } 277 } 278 279 return names, nil 280 } 281 282 // IsRemote returns true when passed a valid remote in the git repo 283 func IsRemote(remote string) (bool, error) { 284 cmd := New("remote") 285 cmd.Stdout = nil 286 cmd.Stderr = nil 287 remotes, err := cmd.Output() 288 if err != nil { 289 return false, err 290 } 291 292 return bytes.Contains(remotes, []byte(remote+"\n")), nil 293 } 294 295 // InsideGitRepo returns true when the current working directory is inside the 296 // working tree of a git repo 297 func InsideGitRepo() bool { 298 cmd := New("rev-parse", "--is-inside-work-tree") 299 cmd.Stdout = nil 300 cmd.Stderr = nil 301 out, _ := cmd.CombinedOutput() 302 return bytes.Contains(out, []byte("true\n")) 303 } 304 305 // Fetch a commit from a given remote 306 func Fetch(remote, commit string) error { 307 gitcmd := []string{"fetch", remote, commit} 308 cmd := New(gitcmd...) 309 cmd.Stdout = nil 310 cmd.Stderr = nil 311 err := cmd.Run() 312 if err != nil { 313 return errors.Errorf("Can't fetch git commit %s from remote %s", commit, remote) 314 } 315 return nil 316 } 317 318 // Show all the commits between 2 git commits 319 func Show(commit1, commit2 string, reverse bool) { 320 gitcmd := []string{"show"} 321 if reverse { 322 gitcmd = append(gitcmd, "--reverse") 323 } 324 gitcmd = append(gitcmd, fmt.Sprintf("%s..%s", commit1, commit2)) 325 New(gitcmd...).Run() 326 } 327 328 // GetLocalRemotes returns a string of local remote names and URLs 329 func GetLocalRemotes() (string, error) { 330 cmd := New("remote", "-v") 331 cmd.Stdout = nil 332 remotes, err := cmd.Output() 333 if err != nil { 334 return "", err 335 } 336 337 return string(remotes), nil 338 } 339 340 // GetLocalRemotesFromFile returns a string of local remote names and URLs based 341 // on their placement within .git/config file, which holds a different ordering 342 // compared to the alternatives presented by Remotes() and GetLocalRemotes(). 343 func GetLocalRemotesFromFile() (string, error) { 344 cmd := New("config", "--local", "--name-only", "--get-regex", "^remote.*") 345 cmd.Stdout = nil 346 remotes, err := cmd.Output() 347 if err != nil { 348 return "", err 349 } 350 351 return string(remotes), nil 352 } 353 354 // NumberCommits returns the number of commits between two commit refs 355 func NumberCommits(sha1, sha2 string) int { 356 cmd := New("log", "--oneline", fmt.Sprintf("%s..%s", sha1, sha2)) 357 cmd.Stdout = nil 358 cmd.Stderr = nil 359 CmdOut, err := cmd.Output() 360 if err != nil { 361 // silently fail and handle the return of 0 at caller 362 return 0 363 } 364 numLines := strings.Count(string(CmdOut), "\n") 365 return numLines 366 }