git.go (4050B)
1 package main 2 3 import ( 4 "io" 5 "fmt" 6 "sync" 7 "errors" 8 "strings" 9 "path/filepath" 10 11 "github.com/go-git/go-git/v5" 12 "github.com/go-git/go-git/v5/plumbing" 13 ) 14 15 // Safely ignorable errors 16 var GitErrIgns = []error{ 17 plumbing.ErrReferenceNotFound, 18 git.ErrRepositoryNotExists, 19 ErrCmdNotRepo, 20 } 21 22 var ( 23 ErrBadPrefix = errors.New("bad git prefix") 24 ErrMalformedDiff = errors.New("malfored git diff --name-status") 25 ErrCmdNotRepo = errors.New("git has been run in a directory that is not a repo") 26 ) 27 28 func gitCmd(name string, arg ...string) ([]string, error) { 29 sl, err := Cmd(name, arg...) 30 31 // That's not a type. Some cmds output 'Not', some output 'not'. 32 // And I'm too lazy to do case insensitive matching. 33 if err != nil && strings.Contains(err.Error(), "ot a git repository") { 34 return nil, ErrCmdNotRepo 35 } 36 37 return sl, err 38 } 39 40 type gpCache struct { 41 sync.Mutex 42 sync.Once 43 P string 44 E error 45 } 46 47 var gpc gpCache 48 49 func gitPrefix() (string, error) { 50 gpc.Lock() 51 defer gpc.Unlock() 52 53 gpc.Do(func(){ 54 prefix, err := gitCmd("git", "rev-parse", "--show-prefix") 55 if err != nil { 56 gpc.E = err 57 } else if len(prefix) > 1 { 58 gpc.E = fmt.Errorf("%w: %v", ErrBadPrefix, prefix) 59 } else if len(prefix) == 1 { 60 gpc.P = filepath.Clean(prefix[0]) 61 } 62 }) 63 64 return gpc.P, gpc.E 65 } 66 67 func gitRepo() (*git.Repository, error) { 68 // Ignore the error - let's try PlainOpen anyway 69 p, _ := gitPrefix() 70 71 d, err := filepath.Abs(".") 72 if err != nil { 73 // Fine. Okay. We'll use "." 74 d = "." 75 } else if strings.HasSuffix(d, p) { 76 d = d[:len(d) - len(p)] 77 } 78 79 return git.PlainOpen(d) 80 } 81 82 func gitResolve(r *git.Repository, ref string) (*plumbing.Hash, error) { 83 return r.ResolveRevision(plumbing.Revision(ref)) 84 } 85 86 func GitHEAD() (string, error) { 87 r, err := gitRepo() 88 if err != nil { 89 return "", err 90 } 91 92 head, err := r.Head() 93 if err != nil { 94 return "", err 95 } 96 97 brch := head.Name().Short() 98 99 if brch == "HEAD" { 100 // On a detached head, so let's show the commit. 101 hash, err := gitResolve(r, "HEAD") 102 if err != nil { 103 return "", err 104 } 105 106 return hash.String()[:7], nil 107 } 108 109 switch { 110 case cnf.Smallbranch && cnf.Uniqbranch: 111 s := Smalltxt(brch) 112 113 iter, err := r.Branches() 114 if err != nil { 115 return "", err 116 } 117 118 for { 119 b, err := iter.Next() 120 if err == io.EOF { 121 return s, nil 122 } else if err != nil { 123 return "", err 124 } 125 126 n := b.Name().Short() 127 128 // If n == brch, then it is the current branch, so it's fine. 129 if n != brch && (n == s || Smalltxt(n) == s) { 130 return brch, nil 131 } 132 } 133 case cnf.Smallbranch: 134 return Smalltxt(brch), nil 135 default: 136 return brch, nil 137 } 138 } 139 140 func GitUnpushed() (int, error) { 141 r, err := gitRepo() 142 if err != nil { 143 return 0, err 144 } 145 146 head, err := r.Head() 147 if err != nil { 148 return 0, err 149 } 150 151 b := head.Name().Short() 152 153 if b == "HEAD" { 154 // Alright we're on a detached head so we have no reference 155 // point any more. Say nothing. 156 return 0, nil 157 } 158 159 rmo, err := r.Remotes() 160 if err != nil { 161 return 0, err 162 } 163 164 rmh := make([]string, len(rmo)) 165 for i := range rmo { 166 n := rmo[i].Config().Name 167 h, err := gitResolve(r, fmt.Sprintf("refs/remotes/%s/%s", n, b)) 168 if err != nil { 169 return 0, err 170 } 171 rmh[i] = h.String() 172 } 173 174 iter, err := r.Log(&git.LogOptions{From: head.Hash()}) 175 if err != nil { 176 return 0, err 177 } 178 179 var ret int 180 counter: for { 181 c, err := iter.Next() 182 if err == io.EOF { 183 break counter 184 } else if err != nil { 185 return 0, err 186 } 187 188 for i := range rmh { 189 if rmh[i] == c.Hash.String() { 190 break counter 191 } 192 } 193 194 ret++ 195 } 196 197 return ret, nil 198 } 199 200 func malformedDiff(str string) error { 201 return fmt.Errorf("%w: \"%v\"", ErrMalformedDiff, str) 202 } 203 204 func GitDiff() (m, a, d, s int, err error) { 205 sl, err := gitCmd("git", "diff", "--name-status") 206 if err != nil { 207 return 208 } 209 210 // not in a git repo 211 if len(sl) == 1 && sl[0] == "" { 212 return 213 } 214 215 for i := range sl { 216 if len(sl[i]) < 1 { 217 err = malformedDiff(sl[i]) 218 return 219 } 220 221 switch sl[i][0] { 222 case 'M': 223 m++ 224 case 'A': 225 a++ 226 case 'D': 227 d++ 228 default: 229 err = malformedDiff(sl[i]) 230 } 231 } 232 233 return 234 }