prompt

My shell prompt
git clone https://hhvn.uk/prompt
git clone git://hhvn.uk/prompt
Log | Files | Refs

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 }