mirror of
https://codeberg.org/davrot/forgejo.git
synced 2025-06-10 14:31:57 +02:00

Partial implementation of b1f42a0cdd
Docs update: https://codeberg.org/forgejo/docs/pulls/1197
Closes #6561
* issue author only provided a reason for the option for mirrors, and there's no known reason for implementing the same option for templates yet, but this change will not make it harder to add that separately.
Pull requests and Actions do not make sense for mirrors to have them, at least by default. Pull requests because changes will get overridden by upstream, Actions get triggered and are failing, filling the actions table in the DB with unwanted content, and there's a security concern, too.
## Testing
* log in to https://v12.next.forgejo.org
* migrate repository
* example lightweight repo: https://codeberg.org/forgejo-contrib/delightful-forgejo
* tick "This repository will be a mirror"
* verify that the repo doesn't have these tabs: Pull requests, Actions
Testing note: there's `models/unit/unit_test.go`, but I don't completely get how it works and was not able to append it with the new setting while keeping it working.
Co-authored-by: Zettat123 <zettat123@gmail.com>
Idea-by: lng2020
<!--start release-notes-assistant-->
## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
- [PR](https://codeberg.org/forgejo/forgejo/pulls/7902): <!--number 7902 --><!--line 0 --><!--description Y29uZmlndXJhYmxlIGRlZmF1bHQgdW5pdHMgZm9yIG1pcnJvcnM=-->configurable default units for mirrors<!--description-->
<!--end release-notes-assistant-->
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7902
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Co-committed-by: 0ko <0ko@noreply.codeberg.org>
308 lines
9.3 KiB
Go
308 lines
9.3 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repository
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"forgejo.org/models"
|
|
activities_model "forgejo.org/models/activities"
|
|
"forgejo.org/models/db"
|
|
git_model "forgejo.org/models/git"
|
|
"forgejo.org/models/organization"
|
|
"forgejo.org/models/perm"
|
|
access_model "forgejo.org/models/perm/access"
|
|
repo_model "forgejo.org/models/repo"
|
|
"forgejo.org/models/unit"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/models/webhook"
|
|
issue_indexer "forgejo.org/modules/indexer/issues"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/setting"
|
|
api "forgejo.org/modules/structs"
|
|
"forgejo.org/modules/util"
|
|
)
|
|
|
|
// CreateRepositoryByExample creates a repository for the user/organization.
|
|
func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) {
|
|
if err = repo_model.IsUsableRepoName(repo.Name); err != nil {
|
|
return err
|
|
}
|
|
|
|
has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("IsRepositoryExist: %w", err)
|
|
} else if has {
|
|
return repo_model.ErrRepoAlreadyExist{
|
|
Uname: u.Name,
|
|
Name: repo.Name,
|
|
}
|
|
}
|
|
|
|
repoPath := repo_model.RepoPath(u.Name, repo.Name)
|
|
isExist, err := util.IsExist(repoPath)
|
|
if err != nil {
|
|
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
|
|
return err
|
|
}
|
|
if !overwriteOrAdopt && isExist {
|
|
log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
|
|
return repo_model.ErrRepoFilesAlreadyExist{
|
|
Uname: u.Name,
|
|
Name: repo.Name,
|
|
}
|
|
}
|
|
|
|
if err = db.Insert(ctx, repo); err != nil {
|
|
return err
|
|
}
|
|
if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil {
|
|
return err
|
|
}
|
|
|
|
// insert units for repo
|
|
defaultUnits := unit.DefaultRepoUnits
|
|
switch {
|
|
case isFork:
|
|
defaultUnits = unit.DefaultForkRepoUnits
|
|
case repo.IsMirror:
|
|
defaultUnits = unit.DefaultMirrorRepoUnits
|
|
}
|
|
units := make([]repo_model.RepoUnit, 0, len(defaultUnits))
|
|
for _, tp := range defaultUnits {
|
|
switch tp {
|
|
case unit.TypeIssues:
|
|
units = append(units, repo_model.RepoUnit{
|
|
RepoID: repo.ID,
|
|
Type: tp,
|
|
Config: &repo_model.IssuesConfig{
|
|
EnableTimetracker: setting.Service.DefaultEnableTimetracking,
|
|
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
|
|
EnableDependencies: setting.Service.DefaultEnableDependencies,
|
|
},
|
|
})
|
|
case unit.TypePullRequests:
|
|
units = append(units, repo_model.RepoUnit{
|
|
RepoID: repo.ID,
|
|
Type: tp,
|
|
Config: &repo_model.PullRequestsConfig{
|
|
AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
|
|
DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
|
|
DefaultUpdateStyle: repo_model.UpdateStyle(setting.Repository.PullRequest.DefaultUpdateStyle),
|
|
AllowRebaseUpdate: true,
|
|
},
|
|
})
|
|
default:
|
|
units = append(units, repo_model.RepoUnit{
|
|
RepoID: repo.ID,
|
|
Type: tp,
|
|
})
|
|
}
|
|
}
|
|
|
|
if err = db.Insert(ctx, units); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remember visibility preference.
|
|
u.LastRepoVisibility = repo.IsPrivate
|
|
if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil {
|
|
return fmt.Errorf("UpdateUserCols: %w", err)
|
|
}
|
|
|
|
if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil {
|
|
return fmt.Errorf("IncrUserRepoNum: %w", err)
|
|
}
|
|
u.NumRepos++
|
|
|
|
// Give access to all members in teams with access to all repositories.
|
|
if u.IsOrganization() {
|
|
teams, err := organization.FindOrgTeams(ctx, u.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("FindOrgTeams: %w", err)
|
|
}
|
|
for _, t := range teams {
|
|
if t.IncludesAllRepositories {
|
|
if err := models.AddRepository(ctx, t, repo); err != nil {
|
|
return fmt.Errorf("AddRepository: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil {
|
|
return fmt.Errorf("IsUserRepoAdmin: %w", err)
|
|
} else if !isAdmin {
|
|
// Make creator repo admin if it wasn't assigned automatically
|
|
if err = AddCollaborator(ctx, repo, doer); err != nil {
|
|
return fmt.Errorf("AddCollaborator: %w", err)
|
|
}
|
|
if err = repo_model.ChangeCollaborationAccessMode(ctx, repo, doer.ID, perm.AccessModeAdmin); err != nil {
|
|
return fmt.Errorf("ChangeCollaborationAccessModeCtx: %w", err)
|
|
}
|
|
}
|
|
} else if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
|
|
// Organization automatically called this in AddRepository method.
|
|
return fmt.Errorf("RecalculateAccesses: %w", err)
|
|
}
|
|
|
|
if setting.Service.AutoWatchNewRepos {
|
|
if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
|
|
return fmt.Errorf("WatchRepo: %w", err)
|
|
}
|
|
}
|
|
|
|
if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil {
|
|
return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular
|
|
|
|
// getDirectorySize returns the disk consumption for a given path
|
|
func getDirectorySize(path string) (int64, error) {
|
|
var size int64
|
|
err := filepath.WalkDir(path, func(_ string, entry os.DirEntry, err error) error {
|
|
if os.IsNotExist(err) { // ignore the error because some files (like temp/lock file) may be deleted during traversing.
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
if entry.IsDir() {
|
|
return nil
|
|
}
|
|
info, err := entry.Info()
|
|
if os.IsNotExist(err) { // ignore the error as above
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
if (info.Mode() & notRegularFileMode) == 0 {
|
|
size += info.Size()
|
|
}
|
|
return nil
|
|
})
|
|
return size, err
|
|
}
|
|
|
|
// UpdateRepoSize updates the repository size, calculating it using getDirectorySize
|
|
func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error {
|
|
size, err := getDirectorySize(repo.RepoPath())
|
|
if err != nil {
|
|
return fmt.Errorf("updateSize: %w", err)
|
|
}
|
|
|
|
lfsSize, err := git_model.GetRepoLFSSize(ctx, repo.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("updateSize: GetLFSMetaObjects: %w", err)
|
|
}
|
|
|
|
return repo_model.UpdateRepoSize(ctx, repo.ID, size, lfsSize)
|
|
}
|
|
|
|
// 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 := path.Join(repo.RepoPath(), `git-daemon-export-ok`)
|
|
|
|
isExist, err := util.IsExist(daemonExportFile)
|
|
if err != nil {
|
|
log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
|
|
return err
|
|
}
|
|
|
|
isPublic := !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePublic
|
|
if !isPublic && isExist {
|
|
if err = util.Remove(daemonExportFile); err != nil {
|
|
log.Error("Failed to remove %s: %v", daemonExportFile, err)
|
|
}
|
|
} else if isPublic && !isExist {
|
|
if f, err := os.Create(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 the repository was reported as abusive, a shadow copy should be created before first update.
|
|
if err := repo_model.IfNeededCreateShadowCopyForRepository(ctx, repo, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil {
|
|
return fmt.Errorf("update: %w", err)
|
|
}
|
|
|
|
if err = 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
|
|
}
|
|
}
|
|
|
|
// 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 == api.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
|
|
}
|