fork of https://github.com/sourcegraph/zoekt
1// Copyright 2016 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package zoekt
16
17import (
18 "context"
19 "fmt"
20 "log"
21 "math"
22 "regexp/syntax"
23 "sort"
24 "strconv"
25 "strings"
26 "time"
27
28 enry_data "github.com/go-enry/go-enry/v2/data"
29 "github.com/grafana/regexp"
30
31 "github.com/sourcegraph/zoekt/query"
32)
33
34const maxUInt16 = 0xffff
35
36// addScore increments the score of the FileMatch by the computed score. If
37// debugScore is true, it also adds a debug string to the FileMatch. If raw is
38// -1, it is ignored. Otherwise, it is added to the debug string.
39func (m *FileMatch) addScore(what string, computed float64, raw float64, debugScore bool) {
40 if computed != 0 && debugScore {
41 var b strings.Builder
42 fmt.Fprintf(&b, "%s", what)
43 if raw != -1 {
44 fmt.Fprintf(&b, "(%s)", strconv.FormatFloat(raw, 'f', -1, 64))
45 }
46 fmt.Fprintf(&b, ":%.2f, ", computed)
47 m.Debug += b.String()
48 }
49 m.Score += computed
50}
51
52func (m *FileMatch) addKeywordScore(score float64, sumTf float64, L float64, debugScore bool) {
53 if debugScore {
54 m.Debug += fmt.Sprintf("keyword-score:%.2f (sum-tf: %.2f, length-ratio: %.2f)", score, sumTf, L)
55 }
56 m.Score += score
57}
58
59// simplifyMultiRepo takes a query and a predicate. It returns Const(true) if all
60// repository names fulfill the predicate, Const(false) if none of them do, and q
61// otherwise.
62func (d *indexData) simplifyMultiRepo(q query.Q, predicate func(*Repository) bool) query.Q {
63 count := 0
64 alive := len(d.repoMetaData)
65 for i := range d.repoMetaData {
66 if d.repoMetaData[i].Tombstone {
67 alive--
68 } else if predicate(&d.repoMetaData[i]) {
69 count++
70 }
71 }
72 if count == alive {
73 return &query.Const{Value: true}
74 }
75 if count > 0 {
76 return q
77 }
78 return &query.Const{Value: false}
79}
80
81func (d *indexData) simplify(in query.Q) query.Q {
82 eval := query.Map(in, func(q query.Q) query.Q {
83 switch r := q.(type) {
84 case *query.Repo:
85 return d.simplifyMultiRepo(q, func(repo *Repository) bool {
86 return r.Regexp.MatchString(repo.Name)
87 })
88 case *query.RepoRegexp:
89 return d.simplifyMultiRepo(q, func(repo *Repository) bool {
90 return r.Regexp.MatchString(repo.Name)
91 })
92 case *query.BranchesRepos:
93 for i := range d.repoMetaData {
94 for _, br := range r.List {
95 if br.Repos.Contains(d.repoMetaData[i].ID) {
96 return q
97 }
98 }
99 }
100 return &query.Const{Value: false}
101 case *query.RepoSet:
102 return d.simplifyMultiRepo(q, func(repo *Repository) bool {
103 return r.Set[repo.Name]
104 })
105 case *query.RepoIDs:
106 return d.simplifyMultiRepo(q, func(repo *Repository) bool {
107 return r.Repos.Contains(repo.ID)
108 })
109 case *query.Language:
110 _, has := d.metaData.LanguageMap[r.Language]
111 if !has && d.metaData.IndexFeatureVersion < 12 {
112 // For index files that haven't been re-indexed by go-enry,
113 // fall back to file-based matching and continue even if this
114 // repo doesn't have the specific language present.
115 extsForLang := enry_data.ExtensionsByLanguage[r.Language]
116 if extsForLang != nil {
117 extFrags := make([]string, 0, len(extsForLang))
118 for _, ext := range extsForLang {
119 extFrags = append(extFrags, regexp.QuoteMeta(ext))
120 }
121 if len(extFrags) > 0 {
122 pattern := fmt.Sprintf("(?i)(%s)$", strings.Join(extFrags, "|"))
123 // inlined copy of query.regexpQuery
124 re, err := syntax.Parse(pattern, syntax.Perl)
125 if err != nil {
126 return &query.Const{Value: false}
127 }
128 if re.Op == syntax.OpLiteral {
129 return &query.Substring{
130 Pattern: string(re.Rune),
131 FileName: true,
132 }
133 }
134 return &query.Regexp{
135 Regexp: re,
136 FileName: true,
137 }
138 }
139 }
140 }
141 if !has {
142 return &query.Const{Value: false}
143 }
144 }
145 return q
146 })
147 return query.Simplify(eval)
148}
149
150func (o *SearchOptions) SetDefaults() {
151 if o.ShardMaxMatchCount == 0 {
152 // We cap the total number of matches, so overly broad
153 // searches don't crash the machine.
154 o.ShardMaxMatchCount = 100000
155 }
156 if o.TotalMaxMatchCount == 0 {
157 o.TotalMaxMatchCount = 10 * o.ShardMaxMatchCount
158 }
159}
160
161func (d *indexData) Search(ctx context.Context, q query.Q, opts *SearchOptions) (sr *SearchResult, err error) {
162 timer := newTimer()
163
164 copyOpts := *opts
165 opts = ©Opts
166 opts.SetDefaults()
167
168 var res SearchResult
169 if len(d.fileNameIndex) == 0 {
170 return &res, nil
171 }
172
173 select {
174 case <-ctx.Done():
175 res.Stats.ShardsSkipped++
176 return &res, nil
177 default:
178 }
179
180 q = d.simplify(q)
181 if c, ok := q.(*query.Const); ok && !c.Value {
182 return &res, nil
183 }
184
185 if opts.EstimateDocCount {
186 res.Stats.ShardFilesConsidered = len(d.fileBranchMasks)
187 return &res, nil
188 }
189
190 q = query.Map(q, query.ExpandFileContent)
191
192 mt, err := d.newMatchTree(q, matchTreeOpt{})
193 if err != nil {
194 return nil, err
195 }
196
197 // Capture the costs of construction before pruning
198 updateMatchTreeStats(mt, &res.Stats)
199
200 mt, err = pruneMatchTree(mt)
201 if err != nil {
202 return nil, err
203 }
204 res.Stats.MatchTreeConstruction = timer.Elapsed()
205 if mt == nil {
206 res.Stats.ShardsSkippedFilter++
207 return &res, nil
208 }
209
210 res.Stats.ShardsScanned++
211
212 cp := &contentProvider{
213 id: d,
214 stats: &res.Stats,
215 }
216
217 // Track the number of documents found in a repository for
218 // ShardRepoMaxMatchCount
219 var (
220 lastRepoID uint16
221 repoMatchCount int
222 )
223
224 docCount := uint32(len(d.fileBranchMasks))
225 lastDoc := int(-1)
226
227nextFileMatch:
228 for {
229 canceled := false
230 select {
231 case <-ctx.Done():
232 canceled = true
233 default:
234 }
235
236 nextDoc := mt.nextDoc()
237 if int(nextDoc) <= lastDoc {
238 nextDoc = uint32(lastDoc + 1)
239 }
240
241 for ; nextDoc < docCount; nextDoc++ {
242 repoID := d.repos[nextDoc]
243 repoMetadata := &d.repoMetaData[repoID]
244
245 // Skip tombstoned repositories
246 if repoMetadata.Tombstone {
247 continue
248 }
249
250 // Skip documents that are tombstoned
251 if len(repoMetadata.FileTombstones) > 0 {
252 if _, tombstoned := repoMetadata.FileTombstones[string(d.fileName(nextDoc))]; tombstoned {
253 continue
254 }
255 }
256
257 // Skip documents over ShardRepoMaxMatchCount if specified.
258 if opts.ShardRepoMaxMatchCount > 0 {
259 if repoMatchCount >= opts.ShardRepoMaxMatchCount && repoID == lastRepoID {
260 res.Stats.FilesSkipped++
261 continue
262 }
263 }
264
265 break
266 }
267
268 if nextDoc >= docCount {
269 break
270 }
271
272 lastDoc = int(nextDoc)
273
274 // We track lastRepoID for ShardRepoMaxMatchCount
275 if lastRepoID != d.repos[nextDoc] {
276 lastRepoID = d.repos[nextDoc]
277 repoMatchCount = 0
278 }
279
280 if canceled || (res.Stats.MatchCount >= opts.ShardMaxMatchCount && opts.ShardMaxMatchCount > 0) {
281 res.Stats.FilesSkipped += int(docCount - nextDoc)
282 break
283 }
284
285 res.Stats.FilesConsidered++
286 mt.prepare(nextDoc)
287
288 cp.setDocument(nextDoc)
289
290 known := make(map[matchTree]bool)
291 md := d.repoMetaData[d.repos[nextDoc]]
292
293 for cost := costMin; cost <= costMax; cost++ {
294 v, ok := mt.matches(cp, cost, known)
295 if ok && !v {
296 continue nextFileMatch
297 }
298
299 if cost == costMax && !ok {
300 log.Panicf("did not decide. Repo %s, doc %d, known %v",
301 md.Name, nextDoc, known)
302 }
303 }
304
305 fileMatch := FileMatch{
306 Repository: md.Name,
307 RepositoryID: md.ID,
308 RepositoryPriority: md.priority,
309 FileName: string(d.fileName(nextDoc)),
310 Checksum: d.getChecksum(nextDoc),
311 Language: d.languageMap[d.getLanguage(nextDoc)],
312 }
313
314 if s := d.subRepos[nextDoc]; s > 0 {
315 if s >= uint32(len(d.subRepoPaths[d.repos[nextDoc]])) {
316 log.Panicf("corrupt index: subrepo %d beyond %v", s, d.subRepoPaths)
317 }
318 path := d.subRepoPaths[d.repos[nextDoc]][s]
319 fileMatch.SubRepositoryPath = path
320 sr := md.SubRepoMap[path]
321 fileMatch.SubRepositoryName = sr.Name
322 if idx := d.branchIndex(nextDoc); idx >= 0 {
323 fileMatch.Version = sr.Branches[idx].Version
324 }
325 } else {
326 idx := d.branchIndex(nextDoc)
327 if idx >= 0 {
328 fileMatch.Version = md.Branches[idx].Version
329 }
330 }
331
332 shouldMergeMatches := !opts.ChunkMatches
333 finalCands := gatherMatches(mt, known, shouldMergeMatches)
334
335 if len(finalCands) == 0 {
336 nm := d.fileName(nextDoc)
337 finalCands = append(finalCands,
338 &candidateMatch{
339 caseSensitive: false,
340 fileName: true,
341 substrBytes: nm,
342 substrLowered: nm,
343 file: nextDoc,
344 runeOffset: 0,
345 byteOffset: 0,
346 byteMatchSz: uint32(len(nm)),
347 })
348 }
349
350 if opts.ChunkMatches {
351 fileMatch.ChunkMatches = cp.fillChunkMatches(finalCands, opts.NumContextLines, fileMatch.Language, opts.DebugScore)
352 } else {
353 fileMatch.LineMatches = cp.fillMatches(finalCands, opts.NumContextLines, fileMatch.Language, opts.DebugScore)
354 }
355
356 if opts.UseKeywordScoring {
357 d.scoreFileUsingBM25(&fileMatch, nextDoc, finalCands, opts)
358 } else {
359 // Use the standard, non-experimental scoring method by default
360 d.scoreFile(&fileMatch, nextDoc, mt, known, opts)
361 }
362
363 fileMatch.Branches = d.gatherBranches(nextDoc, mt, known)
364 sortMatchesByScore(fileMatch.LineMatches)
365 sortChunkMatchesByScore(fileMatch.ChunkMatches)
366 if opts.Whole {
367 fileMatch.Content = cp.data(false)
368 }
369
370 matchedChunkRanges := 0
371 for _, cm := range fileMatch.ChunkMatches {
372 matchedChunkRanges += len(cm.Ranges)
373 }
374
375 repoMatchCount += len(fileMatch.LineMatches)
376 repoMatchCount += matchedChunkRanges
377
378 if opts.DebugScore {
379 fileMatch.Debug = fmt.Sprintf("score:%.2f <- %s", fileMatch.Score, fileMatch.Debug)
380 }
381
382 res.Files = append(res.Files, fileMatch)
383 res.Stats.MatchCount += len(fileMatch.LineMatches)
384 res.Stats.MatchCount += matchedChunkRanges
385 res.Stats.FileCount++
386 }
387
388 // We do not sort Files here, instead we rely on the shards pkg to do file
389 // ranking. If we sorted now, we would break the assumption that results
390 // from the same repo in a shard appear next to each other.
391
392 for _, md := range d.repoMetaData {
393 r := md
394 addRepo(&res, &r)
395 for _, v := range r.SubRepoMap {
396 addRepo(&res, v)
397 }
398 }
399
400 // Update stats based on work done during document search.
401 updateMatchTreeStats(mt, &res.Stats)
402
403 // If document ranking is enabled, then we can rank and truncate the files to save memory.
404 if opts.UseDocumentRanks {
405 res.Files = SortAndTruncateFiles(res.Files, opts)
406 }
407
408 res.Stats.MatchTreeSearch = timer.Elapsed()
409
410 return &res, nil
411}
412
413// scoreFile computes a score for the file match using various scoring signals, like
414// whether there's an exact match on a symbol, the number of query clauses that matched, etc.
415func (d *indexData) scoreFile(fileMatch *FileMatch, doc uint32, mt matchTree, known map[matchTree]bool, opts *SearchOptions) {
416 atomMatchCount := 0
417 visitMatches(mt, known, func(mt matchTree) {
418 atomMatchCount++
419 })
420
421 addScore := func(what string, computed float64) {
422 fileMatch.addScore(what, computed, -1, opts.DebugScore)
423 }
424
425 // atom-count boosts files with matches from more than 1 atom. The
426 // maximum boost is scoreFactorAtomMatch.
427 if atomMatchCount > 0 {
428 fileMatch.addScore("atom", (1.0-1.0/float64(atomMatchCount))*scoreFactorAtomMatch, float64(atomMatchCount), opts.DebugScore)
429 }
430
431 maxFileScore := 0.0
432 for i := range fileMatch.LineMatches {
433 if maxFileScore < fileMatch.LineMatches[i].Score {
434 maxFileScore = fileMatch.LineMatches[i].Score
435 }
436
437 // Order by ordering in file.
438 fileMatch.LineMatches[i].Score += scoreLineOrderFactor * (1.0 - (float64(i) / float64(len(fileMatch.LineMatches))))
439 }
440
441 for i := range fileMatch.ChunkMatches {
442 if maxFileScore < fileMatch.ChunkMatches[i].Score {
443 maxFileScore = fileMatch.ChunkMatches[i].Score
444 }
445
446 // Order by ordering in file.
447 fileMatch.ChunkMatches[i].Score += scoreLineOrderFactor * (1.0 - (float64(i) / float64(len(fileMatch.ChunkMatches))))
448 }
449
450 // Maintain ordering of input files. This
451 // strictly dominates the in-file ordering of
452 // the matches.
453 addScore("fragment", maxFileScore)
454
455 if opts.UseDocumentRanks && len(d.ranks) > int(doc) {
456 weight := scoreFileRankFactor
457 if opts.DocumentRanksWeight > 0.0 {
458 weight = opts.DocumentRanksWeight
459 }
460
461 ranks := d.ranks[doc]
462 // The ranks slice always contains one entry representing the file rank (unless it's empty since the
463 // file doesn't have a rank). This is left over from when documents could have multiple rank signals,
464 // and we plan to clean this up.
465 if len(ranks) > 0 {
466 // The file rank represents a log (base 2) count. The log ranks should be bounded at 32, but we
467 // cap it just in case to ensure it falls in the range [0, 1].
468 normalized := math.Min(1.0, ranks[0]/32.0)
469 addScore("file-rank", weight*normalized)
470 }
471 }
472
473 md := d.repoMetaData[d.repos[doc]]
474 addScore("doc-order", scoreFileOrderFactor*(1.0-float64(doc)/float64(len(d.boundaries))))
475 addScore("repo-rank", scoreRepoRankFactor*float64(md.Rank)/maxUInt16)
476
477 if opts.DebugScore {
478 fileMatch.Debug = strings.TrimSuffix(fileMatch.Debug, ", ")
479 }
480}
481
482// scoreFileUsingBM25 computes a score for the file match using an approximation to BM25, the most common scoring
483// algorithm for keyword search: https://en.wikipedia.org/wiki/Okapi_BM25. It implements all parts of the formula
484// except inverse document frequency (idf), since we don't have access to global term frequency statistics.
485//
486// This scoring strategy ignores all other signals including document ranks. This keeps things simple for now,
487// since BM25 is not normalized and can be tricky to combine with other scoring signals.
488func (d *indexData) scoreFileUsingBM25(fileMatch *FileMatch, doc uint32, cands []*candidateMatch, opts *SearchOptions) {
489 // Treat each candidate match as a term and compute the frequencies. For now, ignore case
490 // sensitivity and treat filenames and symbols the same as content.
491 termFreqs := map[string]int{}
492 for _, cand := range cands {
493 term := string(cand.substrLowered)
494 termFreqs[term]++
495 }
496
497 // Compute the file length ratio. Usually the calculation would be based on terms, but using
498 // bytes should work fine, as we're just computing a ratio.
499 fileLength := float64(d.boundaries[doc+1] - d.boundaries[doc])
500 numFiles := len(d.boundaries)
501 averageFileLength := float64(d.boundaries[numFiles-1]) / float64(numFiles)
502 L := fileLength / averageFileLength
503
504 // Use standard parameter defaults (used in Lucene and academic papers)
505 k, b := 1.2, 0.75
506 sumTf := 0.0 // Just for debugging
507 score := 0.0
508 for _, freq := range termFreqs {
509 tf := float64(freq)
510 sumTf += tf
511 score += ((k + 1.0) * tf) / (k*(1.0-b+b*L) + tf)
512 }
513
514 fileMatch.addKeywordScore(score, sumTf, L, opts.DebugScore)
515}
516
517func addRepo(res *SearchResult, repo *Repository) {
518 if res.RepoURLs == nil {
519 res.RepoURLs = map[string]string{}
520 }
521 res.RepoURLs[repo.Name] = repo.FileURLTemplate
522
523 if res.LineFragments == nil {
524 res.LineFragments = map[string]string{}
525 }
526 res.LineFragments[repo.Name] = repo.LineFragmentTemplate
527}
528
529type sortByOffsetSlice []*candidateMatch
530
531func (m sortByOffsetSlice) Len() int { return len(m) }
532func (m sortByOffsetSlice) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
533func (m sortByOffsetSlice) Less(i, j int) bool {
534 return m[i].byteOffset < m[j].byteOffset
535}
536
537// Gather matches from this document. This never returns a mixture of
538// filename/content matches: if there are content matches, all
539// filename matches are trimmed from the result. The matches are
540// returned in document order and are non-overlapping.
541//
542// If `merge` is set, overlapping and adjacent matches will be merged
543// into a single match. Otherwise, overlapping matches will be removed,
544// but adjacent matches will remain.
545func gatherMatches(mt matchTree, known map[matchTree]bool, merge bool) []*candidateMatch {
546 var cands []*candidateMatch
547 visitMatches(mt, known, func(mt matchTree) {
548 if smt, ok := mt.(*substrMatchTree); ok {
549 cands = append(cands, smt.current...)
550 }
551 if rmt, ok := mt.(*regexpMatchTree); ok {
552 cands = append(cands, rmt.found...)
553 }
554 if rmt, ok := mt.(*wordMatchTree); ok {
555 cands = append(cands, rmt.found...)
556 }
557 if smt, ok := mt.(*symbolRegexpMatchTree); ok {
558 cands = append(cands, smt.found...)
559 }
560 })
561
562 foundContentMatch := false
563 for _, c := range cands {
564 if !c.fileName {
565 foundContentMatch = true
566 break
567 }
568 }
569
570 res := cands[:0]
571 for _, c := range cands {
572 if !foundContentMatch || !c.fileName {
573 res = append(res, c)
574 }
575 }
576 cands = res
577
578 if merge {
579 // Merge adjacent candidates. This guarantees that the matches
580 // are non-overlapping.
581 sort.Sort((sortByOffsetSlice)(cands))
582 res = cands[:0]
583 for i, c := range cands {
584 if i == 0 {
585 res = append(res, c)
586 continue
587 }
588 last := res[len(res)-1]
589 lastEnd := last.byteOffset + last.byteMatchSz
590 end := c.byteOffset + c.byteMatchSz
591 if lastEnd >= c.byteOffset {
592 if end > lastEnd {
593 last.byteMatchSz = end - last.byteOffset
594 }
595 continue
596 }
597
598 res = append(res, c)
599 }
600 } else {
601 // Remove overlapping candidates. This guarantees that the matches
602 // are non-overlapping, but also preserves expected match counts.
603 sort.Sort((sortByOffsetSlice)(cands))
604 res = cands[:0]
605 for i, c := range cands {
606 if i == 0 {
607 res = append(res, c)
608 continue
609 }
610 last := res[len(res)-1]
611 lastEnd := last.byteOffset + last.byteMatchSz
612 if lastEnd > c.byteOffset {
613 continue
614 }
615
616 res = append(res, c)
617 }
618 }
619
620 return res
621}
622
623func (d *indexData) branchIndex(docID uint32) int {
624 mask := d.fileBranchMasks[docID]
625 idx := 0
626 for mask != 0 {
627 if mask&0x1 != 0 {
628 return idx
629 }
630 idx++
631 mask >>= 1
632 }
633 return -1
634}
635
636// gatherBranches returns a list of branch names taking into account any branch
637// filters in the query. If the query contains a branch filter, it returns all
638// branches containing the docID and matching the branch filter. Otherwise, it
639// returns all branches containing docID.
640func (d *indexData) gatherBranches(docID uint32, mt matchTree, known map[matchTree]bool) []string {
641 var mask uint64
642 visitMatches(mt, known, func(mt matchTree) {
643 bq, ok := mt.(*branchQueryMatchTree)
644 if !ok {
645 return
646 }
647
648 mask = mask | bq.branchMask()
649 })
650
651 if mask == 0 {
652 mask = d.fileBranchMasks[docID]
653 }
654
655 var branches []string
656 id := uint32(1)
657 branchNames := d.branchNames[d.repos[docID]]
658 for mask != 0 {
659 if mask&0x1 != 0 {
660 branches = append(branches, branchNames[uint(id)])
661 }
662 id <<= 1
663 mask >>= 1
664 }
665
666 return branches
667}
668
669func (d *indexData) List(ctx context.Context, q query.Q, opts *ListOptions) (rl *RepoList, err error) {
670 var include func(rle *RepoListEntry) bool
671
672 q = d.simplify(q)
673 if c, ok := q.(*query.Const); ok {
674 if !c.Value {
675 return &RepoList{}, nil
676 }
677 include = func(rle *RepoListEntry) bool {
678 return true
679 }
680 } else {
681 sr, err := d.Search(ctx, q, &SearchOptions{
682 ShardRepoMaxMatchCount: 1,
683 })
684 if err != nil {
685 return nil, err
686 }
687
688 foundRepos := make(map[string]struct{}, len(sr.Files))
689 for _, file := range sr.Files {
690 foundRepos[file.Repository] = struct{}{}
691 }
692
693 include = func(rle *RepoListEntry) bool {
694 _, ok := foundRepos[rle.Repository.Name]
695 return ok
696 }
697 }
698
699 var l RepoList
700
701 field, err := opts.GetField()
702 if err != nil {
703 return nil, err
704 }
705 switch field {
706 case RepoListFieldRepos:
707 l.Repos = make([]*RepoListEntry, 0, len(d.repoListEntry))
708 case RepoListFieldReposMap:
709 l.ReposMap = make(ReposMap, len(d.repoListEntry))
710 }
711
712 for i := range d.repoListEntry {
713 if d.repoMetaData[i].Tombstone {
714 continue
715 }
716 rle := &d.repoListEntry[i]
717 if !include(rle) {
718 continue
719 }
720
721 l.Stats.Add(&rle.Stats)
722
723 // Backwards compat for when ID is missing
724 if rle.Repository.ID == 0 {
725 l.Repos = append(l.Repos, rle)
726 continue
727 }
728
729 switch field {
730 case RepoListFieldRepos:
731 l.Repos = append(l.Repos, rle)
732 case RepoListFieldReposMap:
733 l.ReposMap[rle.Repository.ID] = MinimalRepoListEntry{
734 HasSymbols: rle.Repository.HasSymbols,
735 Branches: rle.Repository.Branches,
736 IndexTimeUnix: rle.IndexMetadata.IndexTime.Unix(),
737 }
738 }
739
740 }
741
742 // Only one of these fields is populated and in all cases the size of that
743 // field is the number of Repos in this shard.
744 l.Stats.Repos = len(l.Repos) + len(l.ReposMap)
745
746 return &l, nil
747}
748
749// regexpToMatchTreeRecursive converts a regular expression to a matchTree mt. If
750// mt is equivalent to the input r, isEqual = true and the matchTree can be used
751// in place of the regex r. If singleLine = true, then the matchTree and all
752// its children only match terms on the same line. singleLine is used during
753// recursion to decide whether to return an andLineMatchTree (singleLine = true)
754// or a andMatchTree (singleLine = false).
755func (d *indexData) regexpToMatchTreeRecursive(r *syntax.Regexp, minTextSize int, fileName bool, caseSensitive bool) (mt matchTree, isEqual bool, singleLine bool, err error) {
756 // TODO - we could perhaps transform Begin/EndText in '\n'?
757 // TODO - we could perhaps transform CharClass in (OrQuery )
758 // if there are just a few runes, and part of a OpConcat?
759 switch r.Op {
760 case syntax.OpLiteral:
761 s := string(r.Rune)
762 if len(s) >= minTextSize {
763 mt, err := d.newSubstringMatchTree(&query.Substring{Pattern: s, FileName: fileName, CaseSensitive: caseSensitive})
764 return mt, true, !strings.Contains(s, "\n"), err
765 }
766 case syntax.OpCapture:
767 return d.regexpToMatchTreeRecursive(r.Sub[0], minTextSize, fileName, caseSensitive)
768
769 case syntax.OpPlus:
770 return d.regexpToMatchTreeRecursive(r.Sub[0], minTextSize, fileName, caseSensitive)
771
772 case syntax.OpRepeat:
773 if r.Min == 1 {
774 return d.regexpToMatchTreeRecursive(r.Sub[0], minTextSize, fileName, caseSensitive)
775 } else if r.Min > 1 {
776 // (x){2,} can't be expressed precisely by the matchTree
777 mt, _, singleLine, err := d.regexpToMatchTreeRecursive(r.Sub[0], minTextSize, fileName, caseSensitive)
778 return mt, false, singleLine, err
779 }
780 case syntax.OpConcat, syntax.OpAlternate:
781 var qs []matchTree
782 isEq := true
783 singleLine = true
784 for _, sr := range r.Sub {
785 if sq, subIsEq, subSingleLine, err := d.regexpToMatchTreeRecursive(sr, minTextSize, fileName, caseSensitive); sq != nil {
786 if err != nil {
787 return nil, false, false, err
788 }
789 isEq = isEq && subIsEq
790 singleLine = singleLine && subSingleLine
791 qs = append(qs, sq)
792 }
793 }
794 if r.Op == syntax.OpConcat {
795 if len(qs) > 1 {
796 isEq = false
797 }
798 newQs := make([]matchTree, 0, len(qs))
799 for _, q := range qs {
800 if _, ok := q.(*bruteForceMatchTree); ok {
801 continue
802 }
803 newQs = append(newQs, q)
804 }
805 if len(newQs) == 1 {
806 return newQs[0], isEq, singleLine, nil
807 }
808 if len(newQs) == 0 {
809 return &bruteForceMatchTree{}, isEq, singleLine, nil
810 }
811 if singleLine {
812 return &andLineMatchTree{andMatchTree{children: newQs}}, isEq, singleLine, nil
813 }
814 return &andMatchTree{newQs}, isEq, singleLine, nil
815 }
816 for _, q := range qs {
817 if _, ok := q.(*bruteForceMatchTree); ok {
818 return q, isEq, false, nil
819 }
820 }
821 if len(qs) == 0 {
822 return &noMatchTree{Why: "const"}, isEq, false, nil
823 }
824 return &orMatchTree{qs}, isEq, false, nil
825 case syntax.OpStar:
826 if r.Sub[0].Op == syntax.OpAnyCharNotNL {
827 return &bruteForceMatchTree{}, false, true, nil
828 }
829 }
830 return &bruteForceMatchTree{}, false, false, nil
831}
832
833type timer struct {
834 last time.Time
835}
836
837func newTimer() *timer {
838 return &timer{
839 last: time.Now(),
840 }
841}
842
843func (t *timer) Elapsed() time.Duration {
844 now := time.Now()
845 d := now.Sub(t.last)
846 t.last = now
847 return d
848}