302 lines
		
	
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2017 The Gitea Authors. All rights reserved.
 | |
| // Use of this source code is governed by a MIT-style
 | |
| // license that can be found in the LICENSE file.
 | |
| 
 | |
| package models
 | |
| 
 | |
| import (
 | |
| 	"io/ioutil"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"code.gitea.io/git"
 | |
| 	"code.gitea.io/gitea/modules/base"
 | |
| 	"code.gitea.io/gitea/modules/indexer"
 | |
| 	"code.gitea.io/gitea/modules/log"
 | |
| 	"code.gitea.io/gitea/modules/setting"
 | |
| 
 | |
| 	"github.com/Unknwon/com"
 | |
| )
 | |
| 
 | |
| // RepoIndexerStatus status of a repo's entry in the repo indexer
 | |
| // For now, implicitly refers to default branch
 | |
| type RepoIndexerStatus struct {
 | |
| 	ID        int64  `xorm:"pk autoincr"`
 | |
| 	RepoID    int64  `xorm:"INDEX"`
 | |
| 	CommitSha string `xorm:"VARCHAR(40)"`
 | |
| }
 | |
| 
 | |
| func (repo *Repository) getIndexerStatus() error {
 | |
| 	if repo.IndexerStatus != nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 	status := &RepoIndexerStatus{RepoID: repo.ID}
 | |
| 	has, err := x.Get(status)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	} else if !has {
 | |
| 		status.CommitSha = ""
 | |
| 	}
 | |
| 	repo.IndexerStatus = status
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (repo *Repository) updateIndexerStatus(sha string) error {
 | |
| 	if err := repo.getIndexerStatus(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if len(repo.IndexerStatus.CommitSha) == 0 {
 | |
| 		repo.IndexerStatus.CommitSha = sha
 | |
| 		_, err := x.Insert(repo.IndexerStatus)
 | |
| 		return err
 | |
| 	}
 | |
| 	repo.IndexerStatus.CommitSha = sha
 | |
| 	_, err := x.ID(repo.IndexerStatus.ID).Cols("commit_sha").
 | |
| 		Update(repo.IndexerStatus)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| type repoIndexerOperation struct {
 | |
| 	repo    *Repository
 | |
| 	deleted bool
 | |
| }
 | |
| 
 | |
| var repoIndexerOperationQueue chan repoIndexerOperation
 | |
| 
 | |
| // InitRepoIndexer initialize the repo indexer
 | |
| func InitRepoIndexer() {
 | |
| 	if !setting.Indexer.RepoIndexerEnabled {
 | |
| 		return
 | |
| 	}
 | |
| 	indexer.InitRepoIndexer(populateRepoIndexer)
 | |
| 	repoIndexerOperationQueue = make(chan repoIndexerOperation, setting.Indexer.UpdateQueueLength)
 | |
| 	go processRepoIndexerOperationQueue()
 | |
| }
 | |
| 
 | |
| // populateRepoIndexer populate the repo indexer with data
 | |
| func populateRepoIndexer() error {
 | |
| 	log.Info("Populating repository indexer (this may take a while)")
 | |
| 	for page := 1; ; page++ {
 | |
| 		repos, _, err := SearchRepositoryByName(&SearchRepoOptions{
 | |
| 			Page:     page,
 | |
| 			PageSize: 10,
 | |
| 			OrderBy:  SearchOrderByID,
 | |
| 			Private:  true,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		} else if len(repos) == 0 {
 | |
| 			return nil
 | |
| 		}
 | |
| 		for _, repo := range repos {
 | |
| 			if err = updateRepoIndexer(repo); err != nil {
 | |
| 				// only log error, since this should not prevent
 | |
| 				// gitea from starting up
 | |
| 				log.Error(4, "updateRepoIndexer: repoID=%d, %v", repo.ID, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type updateBatch struct {
 | |
| 	updates []indexer.RepoIndexerUpdate
 | |
| }
 | |
| 
 | |
| func updateRepoIndexer(repo *Repository) error {
 | |
| 	changes, err := getRepoChanges(repo)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	} else if changes == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	batch := indexer.RepoIndexerBatch()
 | |
| 	for _, filename := range changes.UpdatedFiles {
 | |
| 		if err := addUpdate(filename, repo, batch); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	for _, filename := range changes.RemovedFiles {
 | |
| 		if err := addDelete(filename, repo, batch); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	if err = batch.Flush(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return updateLastIndexSync(repo)
 | |
| }
 | |
| 
 | |
| // repoChanges changes (file additions/updates/removals) to a repo
 | |
| type repoChanges struct {
 | |
| 	UpdatedFiles []string
 | |
| 	RemovedFiles []string
 | |
| }
 | |
| 
 | |
| // getRepoChanges returns changes to repo since last indexer update
 | |
| func getRepoChanges(repo *Repository) (*repoChanges, error) {
 | |
| 	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
 | |
| 	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
 | |
| 
 | |
| 	if err := repo.UpdateLocalCopyBranch(""); err != nil {
 | |
| 		return nil, err
 | |
| 	} else if !git.IsBranchExist(repo.LocalCopyPath(), repo.DefaultBranch) {
 | |
| 		// repo does not have any commits yet, so nothing to update
 | |
| 		return nil, nil
 | |
| 	} else if err = repo.UpdateLocalCopyBranch(repo.DefaultBranch); err != nil {
 | |
| 		return nil, err
 | |
| 	} else if err = repo.getIndexerStatus(); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if len(repo.IndexerStatus.CommitSha) == 0 {
 | |
| 		return genesisChanges(repo)
 | |
| 	}
 | |
| 	return nonGenesisChanges(repo)
 | |
| }
 | |
| 
 | |
| func addUpdate(filename string, repo *Repository, batch *indexer.Batch) error {
 | |
| 	filepath := path.Join(repo.LocalCopyPath(), filename)
 | |
| 	if stat, err := os.Stat(filepath); err != nil {
 | |
| 		return err
 | |
| 	} else if stat.Size() > setting.Indexer.MaxIndexerFileSize {
 | |
| 		return nil
 | |
| 	}
 | |
| 	fileContents, err := ioutil.ReadFile(filepath)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	} else if !base.IsTextFile(fileContents) {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return batch.Add(indexer.RepoIndexerUpdate{
 | |
| 		Filepath: filename,
 | |
| 		Op:       indexer.RepoIndexerOpUpdate,
 | |
| 		Data: &indexer.RepoIndexerData{
 | |
| 			RepoID:  repo.ID,
 | |
| 			Content: string(fileContents),
 | |
| 		},
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func addDelete(filename string, repo *Repository, batch *indexer.Batch) error {
 | |
| 	return batch.Add(indexer.RepoIndexerUpdate{
 | |
| 		Filepath: filename,
 | |
| 		Op:       indexer.RepoIndexerOpDelete,
 | |
| 		Data: &indexer.RepoIndexerData{
 | |
| 			RepoID: repo.ID,
 | |
| 		},
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // genesisChanges get changes to add repo to the indexer for the first time
 | |
| func genesisChanges(repo *Repository) (*repoChanges, error) {
 | |
| 	var changes repoChanges
 | |
| 	stdout, err := git.NewCommand("ls-files").RunInDir(repo.LocalCopyPath())
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	for _, line := range strings.Split(stdout, "\n") {
 | |
| 		filename := strings.TrimSpace(line)
 | |
| 		if len(filename) == 0 {
 | |
| 			continue
 | |
| 		} else if filename[0] == '"' {
 | |
| 			filename, err = strconv.Unquote(filename)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		}
 | |
| 		changes.UpdatedFiles = append(changes.UpdatedFiles, filename)
 | |
| 	}
 | |
| 	return &changes, nil
 | |
| }
 | |
| 
 | |
| // nonGenesisChanges get changes since the previous indexer update
 | |
| func nonGenesisChanges(repo *Repository) (*repoChanges, error) {
 | |
| 	diffCmd := git.NewCommand("diff", "--name-status",
 | |
| 		repo.IndexerStatus.CommitSha, "HEAD")
 | |
| 	stdout, err := diffCmd.RunInDir(repo.LocalCopyPath())
 | |
| 	if err != nil {
 | |
| 		// previous commit sha may have been removed by a force push, so
 | |
| 		// try rebuilding from scratch
 | |
| 		if err = indexer.DeleteRepoFromIndexer(repo.ID); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		return genesisChanges(repo)
 | |
| 	}
 | |
| 	var changes repoChanges
 | |
| 	for _, line := range strings.Split(stdout, "\n") {
 | |
| 		line = strings.TrimSpace(line)
 | |
| 		if len(line) == 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 		filename := strings.TrimSpace(line[1:])
 | |
| 		if len(filename) == 0 {
 | |
| 			continue
 | |
| 		} else if filename[0] == '"' {
 | |
| 			filename, err = strconv.Unquote(filename)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		switch status := line[0]; status {
 | |
| 		case 'M', 'A':
 | |
| 			changes.UpdatedFiles = append(changes.UpdatedFiles, filename)
 | |
| 		case 'D':
 | |
| 			changes.RemovedFiles = append(changes.RemovedFiles, filename)
 | |
| 		default:
 | |
| 			log.Warn("Unrecognized status: %c (line=%s)", status, line)
 | |
| 		}
 | |
| 	}
 | |
| 	return &changes, nil
 | |
| }
 | |
| 
 | |
| func updateLastIndexSync(repo *Repository) error {
 | |
| 	stdout, err := git.NewCommand("rev-parse", "HEAD").RunInDir(repo.LocalCopyPath())
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	sha := strings.TrimSpace(stdout)
 | |
| 	return repo.updateIndexerStatus(sha)
 | |
| }
 | |
| 
 | |
| func processRepoIndexerOperationQueue() {
 | |
| 	for {
 | |
| 		op := <-repoIndexerOperationQueue
 | |
| 		if op.deleted {
 | |
| 			if err := indexer.DeleteRepoFromIndexer(op.repo.ID); err != nil {
 | |
| 				log.Error(4, "DeleteRepoFromIndexer: %v", err)
 | |
| 			}
 | |
| 		} else {
 | |
| 			if err := updateRepoIndexer(op.repo); err != nil {
 | |
| 				log.Error(4, "updateRepoIndexer: %v", err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // DeleteRepoFromIndexer remove all of a repository's entries from the indexer
 | |
| func DeleteRepoFromIndexer(repo *Repository) {
 | |
| 	addOperationToQueue(repoIndexerOperation{repo: repo, deleted: true})
 | |
| }
 | |
| 
 | |
| // UpdateRepoIndexer update a repository's entries in the indexer
 | |
| func UpdateRepoIndexer(repo *Repository) {
 | |
| 	addOperationToQueue(repoIndexerOperation{repo: repo, deleted: false})
 | |
| }
 | |
| 
 | |
| func addOperationToQueue(op repoIndexerOperation) {
 | |
| 	if !setting.Indexer.RepoIndexerEnabled {
 | |
| 		return
 | |
| 	}
 | |
| 	select {
 | |
| 	case repoIndexerOperationQueue <- op:
 | |
| 		break
 | |
| 	default:
 | |
| 		go func() {
 | |
| 			repoIndexerOperationQueue <- op
 | |
| 		}()
 | |
| 	}
 | |
| }
 | 
