Files
Gitea/services/repository/repository.go
T
wxiaoguang 9dc04289aa refactor: make git package handle all git operations (#38543)
before: gitrepo vs git packages
after: git package fully handle all git operations

by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide
path details.

benefits:

1. remove all unnecessary wrappers, developers no need to struggle with
"which package should be used"
2. simplify code, RepositoryFacade can (will) be used everywhere, all
"path" details are (will be) hidden
2026-07-20 16:07:38 +00:00

345 lines
11 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"errors"
"fmt"
"strings"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/organization"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
git2 "gitea.dev/modules/git"
"gitea.dev/modules/graceful"
issue_indexer "gitea.dev/modules/indexer/issues"
"gitea.dev/modules/log"
"gitea.dev/modules/queue"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
notify_service "gitea.dev/services/notify"
pull_service "gitea.dev/services/pull"
)
// WebSearchRepository represents a repository returned by web search
type WebSearchRepository struct {
Repository *structs.Repository `json:"repository"`
LatestCommitStatus *git.CommitStatus `json:"latest_commit_status"`
LocaleLatestCommitStatus string `json:"locale_latest_commit_status"`
}
// WebSearchResults results of a successful web search
type WebSearchResults struct {
OK bool `json:"ok"`
Data []*WebSearchRepository `json:"data"`
}
// CreateRepository creates a repository for the user/organization.
func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
repo, err := CreateRepositoryDirectly(ctx, doer, owner, opts, true)
if err != nil {
// No need to rollback here we should do this in CreateRepository...
return nil, err
}
notify_service.CreateRepository(ctx, doer, owner, repo)
return repo, nil
}
// DeleteRepository deletes a repository for a user or organization.
func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, notify bool) error {
if err := pull_service.CloseRepoBranchesPulls(ctx, doer, repo); err != nil {
log.Error("CloseRepoBranchesPulls failed: %v", err)
}
if notify {
// If the repo itself has webhooks, we need to trigger them before deleting it...
notify_service.DeleteRepository(ctx, doer, repo)
}
return DeleteRepositoryDirectly(ctx, repo.ID)
}
// PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace
func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoName string) (*repo_model.Repository, error) {
if authUser == nil {
return nil, errors.New("cannot push-create repository anonymously")
}
if !authUser.IsAdmin {
if owner.IsOrganization() {
if ok, err := organization.CanCreateOrgRepo(ctx, owner.ID, authUser.ID); err != nil {
return nil, err
} else if !ok {
return nil, errors.New("cannot push-create repository for org")
}
} else if authUser.ID != owner.ID {
return nil, errors.New("cannot push-create repository for another user")
}
}
repo, err := CreateRepository(ctx, authUser, owner, CreateRepoOptions{
Name: repoName,
IsPrivate: setting.Repository.DefaultPushCreatePrivate || setting.Repository.ForcePrivate,
})
if err != nil {
return nil, err
}
return repo, nil
}
// Init start repository service
func Init(ctx context.Context) error {
licenseUpdaterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "repo_license_updater", repoLicenseUpdater)
if licenseUpdaterQueue == nil {
return errors.New("unable to create repo_license_updater queue")
}
go graceful.GetManager().RunWithCancel(licenseUpdaterQueue)
if err := repo_module.LoadRepoConfig(); err != nil {
return err
}
if err := initPushQueue(); err != nil {
return err
}
return initBranchSyncQueue(graceful.GetManager().ShutdownContext())
}
// UpdateRepository updates a repository
func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {
return db.WithTx(ctx, func(ctx context.Context) error {
if err = updateRepository(ctx, repo, visibilityChanged); err != nil {
return fmt.Errorf("updateRepository: %w", err)
}
return nil
})
}
func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository, private bool) (err error) {
return db.WithTx(ctx, func(ctx context.Context) error {
repo.IsPrivate = private
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
return err
}
if err = repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("LoadOwner: %w", err)
}
if repo.Owner.IsOrganization() {
// Organization repository need to recalculate access table when visibility is changed.
if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
return fmt.Errorf("recalculateTeamAccesses: %w", err)
}
}
// Create/Remove git-daemon-export-ok for git-daemon...
if err := CheckDaemonExportOK(ctx, repo); err != nil {
return err
}
// If repo has become private, we need to set its actions to private, and clear stars and watches.
if private {
_, err = db.GetEngine(ctx).
Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{IsPrivate: true})
if err != nil {
return err
}
if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
return err
}
if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
return err
}
}
shouldUpdateForks := private
if !private && repo.Owner.Visibility != structs.VisibleTypePrivate {
shouldUpdateForks = true
}
if shouldUpdateForks {
forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
if err != nil {
return fmt.Errorf("getRepositoriesByForkID: %w", err)
}
for _, forkRepo := range forkRepos {
if err = MakeRepoPrivate(ctx, forkRepo, private); err != nil {
return fmt.Errorf("MakeRepoPrivate[%d]: %w", forkRepo.ID, err)
}
}
}
// If visibility is changed, we need to update the issue indexer.
// Since the data in the issue indexer have field to indicate if the repo is public or not.
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
return nil
})
}
// GetAttachmentLinkedTypeAndRepoID returns the linked type and repository id of attachment if any
func GetAttachmentLinkedTypeAndRepoID(ctx context.Context, a *repo_model.Attachment) (unit.Type, int64, error) {
if a.IssueID != 0 {
iss, err := issues_model.GetIssueByID(ctx, a.IssueID)
if err != nil {
return unit.TypeIssues, 0, err
}
unitType := unit.TypeIssues
if iss.IsPull {
unitType = unit.TypePullRequests
}
return unitType, iss.RepoID, nil
}
if a.ReleaseID != 0 {
rel, err := repo_model.GetReleaseByID(ctx, a.ReleaseID)
if err != nil {
return unit.TypeReleases, 0, err
}
return unit.TypeReleases, rel.RepoID, nil
}
return unit.TypeInvalid, 0, nil
}
// CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon...
func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error {
if err := repo.LoadOwner(ctx); err != nil {
return err
}
// Create/Remove git-daemon-export-ok for git-daemon...
daemonExportFile := `git-daemon-export-ok`
isExist, err := git2.IsRepoFileExist(ctx, repo, daemonExportFile)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
return err
}
isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
if !isPublic && isExist {
if err = git2.RemoveRepoFileOrDir(ctx, repo, daemonExportFile); err != nil {
log.Error("Failed to remove %s: %v", daemonExportFile, err)
}
} else if isPublic && !isExist {
if f, err := git2.CreateRepoFile(ctx, repo, daemonExportFile); err != nil {
log.Error("Failed to create %s: %v", daemonExportFile, err)
} else {
f.Close()
}
}
return nil
}
// updateRepository updates a repository with db context
func updateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {
repo.LowerName = strings.ToLower(repo.Name)
e := db.GetEngine(ctx)
if _, err = e.ID(repo.ID).NoAutoTime().AllCols().Update(repo); err != nil {
return fmt.Errorf("update: %w", err)
}
if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
log.Error("Failed to update size for repository: %v", err)
}
if visibilityChanged {
if err = repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("LoadOwner: %w", err)
}
if repo.Owner.IsOrganization() {
// Organization repository need to recalculate access table when visibility is changed.
if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
return fmt.Errorf("recalculateTeamAccesses: %w", err)
}
}
// If repo has become private, we need to set its actions to private.
if repo.IsPrivate {
_, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{
IsPrivate: true,
})
if err != nil {
return err
}
if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
return err
}
// watchers who lost access must not keep watching the now-private repo
if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
return err
}
}
// Create/Remove git-daemon-export-ok for git-daemon...
if err := CheckDaemonExportOK(ctx, repo); err != nil {
return err
}
forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
if err != nil {
return fmt.Errorf("getRepositoriesByForkID: %w", err)
}
for i := range forkRepos {
forkRepos[i].IsPrivate = repo.IsPrivate || repo.Owner.Visibility == structs.VisibleTypePrivate
if err = updateRepository(ctx, forkRepos[i], true); err != nil {
return fmt.Errorf("updateRepository[%d]: %w", forkRepos[i].ID, err)
}
}
// If visibility is changed, we need to update the issue indexer.
// Since the data in the issue indexer have field to indicate if the repo is public or not.
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
}
return nil
}
func HasWiki(ctx context.Context, repo *repo_model.Repository) bool {
hasWiki, err := git2.IsRepositoryExist(ctx, repo.WikiStorageRepo())
if err != nil {
log.Error("gitrepo.IsRepositoryExist: %v", err)
}
return hasWiki && err == nil
}
// CheckCreateRepository check if doer could create a repository in new owner
func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, name string, overwriteOrAdopt bool) error {
if !doer.CanCreateRepoIn(owner) {
return repo_model.ErrReachLimitOfRepo{Limit: owner.MaxRepoCreation}
}
if err := repo_model.IsUsableRepoName(name); err != nil {
return err
}
has, err := repo_model.IsRepositoryModelExist(ctx, owner, name)
if err != nil {
return err
} else if has {
return repo_model.ErrRepoAlreadyExist{Uname: owner.Name, Name: name}
}
repo := repo_model.CodeRepoByName(owner.Name, name)
isExist, err := git2.IsRepositoryExist(ctx, repo)
if err != nil {
log.Error("Unable to check if repo %s/%s exists, error: %v", owner.Name, name, err)
return err
}
if !overwriteOrAdopt && isExist {
return repo_model.ErrRepoFilesAlreadyExist{Uname: owner.Name, Name: name}
}
return nil
}