1package git23import (4 "context"5 "fmt"6 "path"7 "runtime"8 "sort"9 "strconv"10 "strings"11 "sync"12 "sync/atomic"13)1415// EntryMode is the unix file mode of a tree entry.16type EntryMode int1718// There are only a few file modes in Git. They look like unix file modes, but19// they can only be one of these.20const (21 EntryTree EntryMode = 004000022 EntryBlob EntryMode = 010064423 EntryExec EntryMode = 010075524 EntrySymlink EntryMode = 012000025 EntryCommit EntryMode = 016000026)2728type TreeEntry struct {29 mode EntryMode30 typ ObjectType31 id Oid32 name string3334 parent *Tree3536 size int6437 sizeMu sync.Mutex38 sizeSet bool39}4041// Mode returns the entry mode if the tree entry.42func (e *TreeEntry) Mode() EntryMode {43 return e.mode44}4546// IsTree returns tree if the entry itself is another tree (i.e. a directory).47func (e *TreeEntry) IsTree() bool {48 return e.mode == EntryTree49}5051// IsBlob returns true if the entry is a blob.52func (e *TreeEntry) IsBlob() bool {53 return e.mode == EntryBlob54}5556// IsExec returns tree if the entry is an executable.57func (e *TreeEntry) IsExec() bool {58 return e.mode == EntryExec59}6061// IsSymlink returns true if the entry is a symbolic link.62func (e *TreeEntry) IsSymlink() bool {63 return e.mode == EntrySymlink64}6566// IsCommit returns true if the entry is a commit (i.e. a submodule).67func (e *TreeEntry) IsCommit() bool {68 return e.mode == EntryCommit69}7071// Type returns the object type of the entry.72func (e *TreeEntry) Type() ObjectType {73 return e.typ74}7576// ID returns the SHA-1 hash of the entry.77func (e *TreeEntry) ID() Oid {78 return e.id79}8081// Name returns name of the entry.82func (e *TreeEntry) Name() string {83 return e.name84}8586// Size returns the size of the entry. It runs a git command to determine the87// size on first call. Successful results are cached; failed attempts are not88// cached, allowing retries with a fresh context.89func (e *TreeEntry) Size(ctx context.Context) int64 {90 e.sizeMu.Lock()91 defer e.sizeMu.Unlock()9293 if e.sizeSet || e.IsTree() {94 return e.size95 }9697 stdout, err := exec(ctx, e.parent.repo.path, []string{"cat-file", "-s", e.id.String()}, nil)98 if err != nil {99 return 0100 }101 e.size, _ = strconv.ParseInt(strings.TrimSpace(string(stdout)), 10, 64)102 e.sizeSet = true103 return e.size104}105106// Blob returns a blob object from the entry.107func (e *TreeEntry) Blob() *Blob {108 return &Blob{109 TreeEntry: e,110 }111}112113// Entries is a sortable list of tree entries.114type Entries []*TreeEntry115116var sorters = []func(t1, t2 *TreeEntry) bool{117 func(t1, t2 *TreeEntry) bool {118 return (t1.IsTree() || t1.IsCommit()) && !t2.IsTree() && !t2.IsCommit()119 },120 func(t1, t2 *TreeEntry) bool {121 return t1.name < t2.name122 },123}124125func (es Entries) Len() int { return len(es) }126func (es Entries) Swap(i, j int) { es[i], es[j] = es[j], es[i] }127func (es Entries) Less(i, j int) bool {128 t1, t2 := es[i], es[j]129 var k int130 for k = 0; k < len(sorters)-1; k++ {131 sorter := sorters[k]132 switch {133 case sorter(t1, t2):134 return true135 case sorter(t2, t1):136 return false137 }138 }139 return sorters[k](t1, t2)140}141142func (es Entries) Sort() {143 sort.Sort(es)144}145146// EntryCommitInfo contains a tree entry with its commit information.147type EntryCommitInfo struct {148 Entry *TreeEntry149 Index int150 Commit *Commit151 Submodule *Submodule152}153154// CommitsInfoOptions contains optional arguments for getting commits155// information.156type CommitsInfoOptions struct {157 // The relative path of the repository.158 Path string159 // The maximum number of goroutines to be used for getting commits information.160 // When not set (i.e. <=0), runtime.GOMAXPROCS is used to determine the value.161 MaxConcurrency int162}163164var defaultConcurrency = runtime.GOMAXPROCS(0)165166// CommitsInfo returns a list of commit information for these tree entries in167// the state of given commit and subpath. It takes advantages of concurrency to168// speed up the process. The returned list has the same number of items as tree169// entries, so the caller can access them via slice indices.170func (es Entries) CommitsInfo(ctx context.Context, commit *Commit, opts ...CommitsInfoOptions) ([]*EntryCommitInfo, error) {171 if len(es) == 0 {172 return []*EntryCommitInfo{}, nil173 }174175 var opt CommitsInfoOptions176 if len(opts) > 0 {177 opt = opts[0]178 }179180 if opt.MaxConcurrency <= 0 {181 opt.MaxConcurrency = defaultConcurrency182 }183184 // Length of bucket determines how many goroutines (subprocesses) can run at the same time.185 bucket := make(chan struct{}, opt.MaxConcurrency)186 results := make(chan *EntryCommitInfo, len(es))187 errs := make(chan error, 1)188189 var errored int64190 hasErrored := func() bool {191 return atomic.LoadInt64(&errored) != 0192 }193 // Only count for the first error, discard the rest194 setError := func(err error) {195 if !atomic.CompareAndSwapInt64(&errored, 0, 1) {196 return197 }198 errs <- err199 }200201 var wg sync.WaitGroup202 wg.Add(len(es))203 go func() {204 for i, e := range es {205 // Shrink down the counter and exit when there is an error206 if hasErrored() {207 wg.Add(i - len(es))208 return209 }210211 // Block until there is an empty slot to control the maximum concurrency212 bucket <- struct{}{}213214 go func(e *TreeEntry, i int) {215 defer func() {216 wg.Done()217 <-bucket218 }()219220 // Avoid expensive operations if has errored221 if hasErrored() {222 return223 }224225 info := &EntryCommitInfo{226 Entry: e,227 Index: i,228 }229 epath := path.Join(opt.Path, e.Name())230231 var err error232 info.Commit, err = commit.CommitByPath(ctx, CommitByRevisionOptions{233 Path: epath,234 })235 if err != nil {236 setError(fmt.Errorf("get commit by path %q: %v", epath, err))237 return238 }239240 // Get extra information for submodules241 if e.IsCommit() {242 // Be tolerant to implicit submodules243 info.Submodule, err = commit.Submodule(ctx, epath)244 if err != nil {245 info.Submodule = &Submodule{Name: epath}246 }247 }248249 results <- info250 }(e, i)251 }252 }()253254 wg.Wait()255 if hasErrored() {256 return nil, <-errs257 }258259 close(results)260261 commitsInfo := make([]*EntryCommitInfo, len(es))262 for info := range results {263 commitsInfo[info.Index] = info264 }265 return commitsInfo, nil266}