git-module

git clone git://git.lin.moe/fork/git-module.git

  1package git
  2
  3import (
  4	"context"
  5	"fmt"
  6	"path"
  7	"runtime"
  8	"sort"
  9	"strconv"
 10	"strings"
 11	"sync"
 12	"sync/atomic"
 13)
 14
 15// EntryMode is the unix file mode of a tree entry.
 16type EntryMode int
 17
 18// There are only a few file modes in Git. They look like unix file modes, but
 19// they can only be one of these.
 20const (
 21	EntryTree    EntryMode = 0040000
 22	EntryBlob    EntryMode = 0100644
 23	EntryExec    EntryMode = 0100755
 24	EntrySymlink EntryMode = 0120000
 25	EntryCommit  EntryMode = 0160000
 26)
 27
 28type TreeEntry struct {
 29	mode EntryMode
 30	typ  ObjectType
 31	id   Oid
 32	name string
 33
 34	parent *Tree
 35
 36	size    int64
 37	sizeMu  sync.Mutex
 38	sizeSet bool
 39}
 40
 41// Mode returns the entry mode if the tree entry.
 42func (e *TreeEntry) Mode() EntryMode {
 43	return e.mode
 44}
 45
 46// IsTree returns tree if the entry itself is another tree (i.e. a directory).
 47func (e *TreeEntry) IsTree() bool {
 48	return e.mode == EntryTree
 49}
 50
 51// IsBlob returns true if the entry is a blob.
 52func (e *TreeEntry) IsBlob() bool {
 53	return e.mode == EntryBlob
 54}
 55
 56// IsExec returns tree if the entry is an executable.
 57func (e *TreeEntry) IsExec() bool {
 58	return e.mode == EntryExec
 59}
 60
 61// IsSymlink returns true if the entry is a symbolic link.
 62func (e *TreeEntry) IsSymlink() bool {
 63	return e.mode == EntrySymlink
 64}
 65
 66// IsCommit returns true if the entry is a commit (i.e. a submodule).
 67func (e *TreeEntry) IsCommit() bool {
 68	return e.mode == EntryCommit
 69}
 70
 71// Type returns the object type of the entry.
 72func (e *TreeEntry) Type() ObjectType {
 73	return e.typ
 74}
 75
 76// ID returns the SHA-1 hash of the entry.
 77func (e *TreeEntry) ID() Oid {
 78	return e.id
 79}
 80
 81// Name returns name of the entry.
 82func (e *TreeEntry) Name() string {
 83	return e.name
 84}
 85
 86// Size returns the size of the entry. It runs a git command to determine the
 87// size on first call. Successful results are cached; failed attempts are not
 88// cached, allowing retries with a fresh context.
 89func (e *TreeEntry) Size(ctx context.Context) int64 {
 90	e.sizeMu.Lock()
 91	defer e.sizeMu.Unlock()
 92
 93	if e.sizeSet || e.IsTree() {
 94		return e.size
 95	}
 96
 97	stdout, err := exec(ctx, e.parent.repo.path, []string{"cat-file", "-s", e.id.String()}, nil)
 98	if err != nil {
 99		return 0
100	}
101	e.size, _ = strconv.ParseInt(strings.TrimSpace(string(stdout)), 10, 64)
102	e.sizeSet = true
103	return e.size
104}
105
106// Blob returns a blob object from the entry.
107func (e *TreeEntry) Blob() *Blob {
108	return &Blob{
109		TreeEntry: e,
110	}
111}
112
113// Entries is a sortable list of tree entries.
114type Entries []*TreeEntry
115
116var 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.name
122	},
123}
124
125func (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 int
130	for k = 0; k < len(sorters)-1; k++ {
131		sorter := sorters[k]
132		switch {
133		case sorter(t1, t2):
134			return true
135		case sorter(t2, t1):
136			return false
137		}
138	}
139	return sorters[k](t1, t2)
140}
141
142func (es Entries) Sort() {
143	sort.Sort(es)
144}
145
146// EntryCommitInfo contains a tree entry with its commit information.
147type EntryCommitInfo struct {
148	Entry     *TreeEntry
149	Index     int
150	Commit    *Commit
151	Submodule *Submodule
152}
153
154// CommitsInfoOptions contains optional arguments for getting commits
155// information.
156type CommitsInfoOptions struct {
157	// The relative path of the repository.
158	Path string
159	// 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 int
162}
163
164var defaultConcurrency = runtime.GOMAXPROCS(0)
165
166// CommitsInfo returns a list of commit information for these tree entries in
167// the state of given commit and subpath. It takes advantages of concurrency to
168// speed up the process. The returned list has the same number of items as tree
169// 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{}, nil
173	}
174
175	var opt CommitsInfoOptions
176	if len(opts) > 0 {
177		opt = opts[0]
178	}
179
180	if opt.MaxConcurrency <= 0 {
181		opt.MaxConcurrency = defaultConcurrency
182	}
183
184	// 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)
188
189	var errored int64
190	hasErrored := func() bool {
191		return atomic.LoadInt64(&errored) != 0
192	}
193	// Only count for the first error, discard the rest
194	setError := func(err error) {
195		if !atomic.CompareAndSwapInt64(&errored, 0, 1) {
196			return
197		}
198		errs <- err
199	}
200
201	var wg sync.WaitGroup
202	wg.Add(len(es))
203	go func() {
204		for i, e := range es {
205			// Shrink down the counter and exit when there is an error
206			if hasErrored() {
207				wg.Add(i - len(es))
208				return
209			}
210
211			// Block until there is an empty slot to control the maximum concurrency
212			bucket <- struct{}{}
213
214			go func(e *TreeEntry, i int) {
215				defer func() {
216					wg.Done()
217					<-bucket
218				}()
219
220				// Avoid expensive operations if has errored
221				if hasErrored() {
222					return
223				}
224
225				info := &EntryCommitInfo{
226					Entry: e,
227					Index: i,
228				}
229				epath := path.Join(opt.Path, e.Name())
230
231				var err error
232				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					return
238				}
239
240				// Get extra information for submodules
241				if e.IsCommit() {
242					// Be tolerant to implicit submodules
243					info.Submodule, err = commit.Submodule(ctx, epath)
244					if err != nil {
245						info.Submodule = &Submodule{Name: epath}
246					}
247				}
248
249				results <- info
250			}(e, i)
251		}
252	}()
253
254	wg.Wait()
255	if hasErrored() {
256		return nil, <-errs
257	}
258
259	close(results)
260
261	commitsInfo := make([]*EntryCommitInfo, len(es))
262	for info := range results {
263		commitsInfo[info.Index] = info
264	}
265	return commitsInfo, nil
266}