in internal/gitaly/linguist/linguist.go [54:187]
func (inst *Instance) Stats(ctx context.Context, commitID git.ObjectID) (ByteCountPerLanguage, error) {
stats, err := initLanguageStats(ctx, inst.repo)
if err != nil {
inst.logger.WithError(err).InfoContext(ctx, "linguist load from cache")
}
if stats.CommitID == commitID {
return stats.Totals, nil
}
objectReader, cancel, err := inst.catfileCache.ObjectReader(ctx, inst.repo)
if err != nil {
return nil, fmt.Errorf("linguist create object reader: %w", err)
}
defer cancel()
checkAttr, finishAttr, err := gitattributes.CheckAttr(ctx, inst.repo, commitID.Revision(), linguistAttrs)
if err != nil {
return nil, fmt.Errorf("linguist create check attr: %w", err)
}
defer finishAttr()
var revlistIt gitpipe.RevisionIterator
full, err := inst.needsFullRecalculation(ctx, stats.CommitID, commitID)
if err != nil {
return nil, fmt.Errorf("linguist cannot determine full recalculation: %w", err)
}
if full {
stats = newLanguageStats()
skipFunc := func(result *gitpipe.RevisionResult) (bool, error) {
f, err := newFileInstance(string(result.ObjectName), checkAttr)
if err != nil {
return true, fmt.Errorf("new file instance: %w", err)
}
// Skip files that are an excluded filetype based on filename.
return f.IsExcluded(), nil
}
// Full recalculation is needed, so get all the files for the
// commit using git-ls-tree(1).
revlistIt = gitpipe.LsTree(ctx, inst.repo,
commitID.String(),
gitpipe.LsTreeWithRecursive(),
gitpipe.LsTreeWithBlobFilter(),
gitpipe.LsTreeWithSkip(skipFunc),
)
} else {
// Stats are cached for one commit, so get the git-diff-tree(1)
// between that commit and the one we're calculating stats for.
hash, err := inst.repo.ObjectHash(ctx)
if err != nil {
return nil, fmt.Errorf("linguist: detect object hash: %w", err)
}
skipFunc := func(result *gitpipe.RevisionResult) (bool, error) {
var skip bool
// Skip files that are deleted, or
// an excluded filetype based on filename.
if hash.IsZeroOID(result.OID) {
skip = true
} else {
f, err := newFileInstance(string(result.ObjectName), checkAttr)
if err != nil {
return false, fmt.Errorf("new file instance: %w", err)
}
skip = f.IsExcluded()
}
if skip {
// It's a little bit of a hack to use this skip
// function, but for every file that's deleted,
// remove the stats.
stats.drop(string(result.ObjectName))
return true, nil
}
return false, nil
}
revlistIt = gitpipe.DiffTree(ctx, inst.repo,
stats.CommitID.String(), commitID.String(),
gitpipe.DiffTreeWithRecursive(),
gitpipe.DiffTreeWithIgnoreSubmodules(),
gitpipe.DiffTreeWithSkip(skipFunc),
)
}
objectIt, err := gitpipe.CatfileObject(ctx, objectReader, revlistIt)
if err != nil {
return nil, fmt.Errorf("linguist gitpipe: %w", err)
}
for objectIt.Next() {
object := objectIt.Result()
filename := string(object.ObjectName)
f, err := newFileInstance(filename, checkAttr)
if err != nil {
return nil, fmt.Errorf("linguist new file instance: %w", err)
}
lang, size, err := f.DetermineStats(object)
if err != nil {
return nil, fmt.Errorf("linguist determine stats: %w", err)
}
// Ensure object content is completely consumed
if _, err := io.Copy(io.Discard, object); err != nil {
return nil, fmt.Errorf("linguist discard excess blob: %w", err)
}
if len(lang) == 0 {
stats.drop(filename)
continue
}
stats.add(filename, lang, size)
}
if err := objectIt.Err(); err != nil {
return nil, fmt.Errorf("linguist object iterator: %w", err)
}
if err := stats.save(ctx, inst.repo, commitID); err != nil {
return nil, fmt.Errorf("linguist language stats save: %w", err)
}
return stats.Totals, nil
}