forked from kevadesu/forgejo
Move some repository related code into sub package (#19711)
* Move some repository related code into sub package * Move more repository functions out of models * Fix lint * Some performance optimization for webhooks and others * some refactors * Fix lint * Fix * Update modules/repository/delete.go Co-authored-by: delvh <dev.lh@web.de> * Fix test * Merge * Fix test * Fix test * Fix test * Fix test Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
parent
ebeb6e7c71
commit
26095115f4
76 changed files with 1756 additions and 1674 deletions
|
@ -26,6 +26,7 @@ import (
|
|||
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
@ -551,14 +552,14 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
|
|||
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues)
|
||||
ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests)
|
||||
|
||||
canSignedUserFork, err := models.CanUserForkRepo(ctx.Doer, ctx.Repo.Repository)
|
||||
canSignedUserFork, err := repo_module.CanUserForkRepo(ctx.Doer, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanUserForkRepo", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["CanSignedUserFork"] = canSignedUserFork
|
||||
|
||||
userAndOrgForks, err := models.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||
userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetForksByUserAndOrgs", err)
|
||||
return
|
||||
|
|
|
@ -291,8 +291,8 @@ func populateIssueIndexer(ctx context.Context) {
|
|||
return
|
||||
default:
|
||||
}
|
||||
repos, _, err := models.SearchRepositoryByName(&models.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{Page: page, PageSize: models.RepositoryListDefaultPageSize},
|
||||
repos, _, err := repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize},
|
||||
OrderBy: db.SearchOrderByID,
|
||||
Private: true,
|
||||
Collaborate: util.OptionalBoolFalse,
|
||||
|
|
|
@ -7,15 +7,20 @@ package repository
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
|
@ -108,7 +113,7 @@ func CreateRepository(doer, u *user_model.User, opts models.CreateRepoOptions) (
|
|||
}
|
||||
}
|
||||
|
||||
if err := models.CheckDaemonExportOK(ctx, repo); err != nil {
|
||||
if err := CheckDaemonExportOK(ctx, repo); err != nil {
|
||||
return fmt.Errorf("checkDaemonExportOK: %v", err)
|
||||
}
|
||||
|
||||
|
@ -133,3 +138,111 @@ func CreateRepository(doer, u *user_model.User, opts models.CreateRepoOptions) (
|
|||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize
|
||||
func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error {
|
||||
size, err := util.GetDirectorySize(repo.RepoPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("updateSize: %v", err)
|
||||
}
|
||||
|
||||
lfsSize, err := models.GetRepoLFSSize(ctx, repo.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updateSize: GetLFSMetaObjects: %v", 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.GetOwner(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)
|
||||
|
||||
if utf8.RuneCountInString(repo.Description) > 255 {
|
||||
repo.Description = string([]rune(repo.Description)[:255])
|
||||
}
|
||||
if utf8.RuneCountInString(repo.Website) > 255 {
|
||||
repo.Website = string([]rune(repo.Website)[:255])
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil {
|
||||
return fmt.Errorf("update: %v", err)
|
||||
}
|
||||
|
||||
if err = UpdateRepoSize(ctx, repo); err != nil {
|
||||
log.Error("Failed to update size for repository: %v", err)
|
||||
}
|
||||
|
||||
if visibilityChanged {
|
||||
if err = repo.GetOwner(ctx); err != nil {
|
||||
return fmt.Errorf("getOwner: %v", 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: %v", 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(&models.Action{
|
||||
IsPrivate: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create/Remove git-daemon-export-ok for git-daemon...
|
||||
if err := CheckDaemonExportOK(db.WithEngine(ctx, e), repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getRepositoriesByForkID: %v", 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]: %v", forkRepos[i].ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
@ -147,3 +148,23 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
|
|||
}
|
||||
assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization")
|
||||
}
|
||||
|
||||
func TestUpdateRepositoryVisibilityChanged(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Get sample repo and change visibility
|
||||
repo, err := repo_model.GetRepositoryByID(9)
|
||||
assert.NoError(t, err)
|
||||
repo.IsPrivate = true
|
||||
|
||||
// Update it
|
||||
err = UpdateRepository(db.DefaultContext, repo, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check visibility of action has become private
|
||||
act := models.Action{}
|
||||
_, err = db.GetEngine(db.DefaultContext).ID(3).Get(&act)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, act.IsPrivate)
|
||||
}
|
||||
|
|
33
modules/repository/delete.go
Normal file
33
modules/repository/delete.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2022 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 repository
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// CanUserDelete returns true if user could delete the repository
|
||||
func CanUserDelete(repo *repo_model.Repository, user *user_model.User) (bool, error) {
|
||||
if user.IsAdmin || user.ID == repo.OwnerID {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err := repo.GetOwner(db.DefaultContext); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if repo.Owner.IsOrganization() {
|
||||
isOwner, err := organization.OrgFromUser(repo.Owner).IsOwnedBy(user.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return isOwner, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
31
modules/repository/fork.go
Normal file
31
modules/repository/fork.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2019 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 repository
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// CanUserForkRepo returns true if specified user can fork repository.
|
||||
func CanUserForkRepo(user *user_model.User, repo *repo_model.Repository) (bool, error) {
|
||||
if user == nil {
|
||||
return false, nil
|
||||
}
|
||||
if repo.OwnerID != user.ID && !repo_model.HasForkedRepo(user.ID, repo.ID) {
|
||||
return true, nil
|
||||
}
|
||||
ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(user.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, org := range ownedOrgs {
|
||||
if repo.OwnerID != org.ID && !repo_model.HasForkedRepo(org.ID, repo.ID) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
@ -20,6 +22,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/huandu/xstrings"
|
||||
)
|
||||
|
||||
|
@ -78,7 +81,38 @@ func generateExpansion(src string, templateRepo, generateRepo *repo_model.Reposi
|
|||
})
|
||||
}
|
||||
|
||||
func checkGiteaTemplate(tmpDir string) (*models.GiteaTemplate, error) {
|
||||
// GiteaTemplate holds information about a .gitea/template file
|
||||
type GiteaTemplate struct {
|
||||
Path string
|
||||
Content []byte
|
||||
|
||||
globs []glob.Glob
|
||||
}
|
||||
|
||||
// Globs parses the .gitea/template globs or returns them if they were already parsed
|
||||
func (gt GiteaTemplate) Globs() []glob.Glob {
|
||||
if gt.globs != nil {
|
||||
return gt.globs
|
||||
}
|
||||
|
||||
gt.globs = make([]glob.Glob, 0)
|
||||
scanner := bufio.NewScanner(bytes.NewReader(gt.Content))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
g, err := glob.Compile(line, '/')
|
||||
if err != nil {
|
||||
log.Info("Invalid glob expression '%s' (skipped): %v", line, err)
|
||||
continue
|
||||
}
|
||||
gt.globs = append(gt.globs, g)
|
||||
}
|
||||
return gt.globs
|
||||
}
|
||||
|
||||
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
|
||||
gtPath := filepath.Join(tmpDir, ".gitea", "template")
|
||||
if _, err := os.Stat(gtPath); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
|
@ -91,7 +125,7 @@ func checkGiteaTemplate(tmpDir string) (*models.GiteaTemplate, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
gt := &models.GiteaTemplate{
|
||||
gt := &GiteaTemplate{
|
||||
Path: gtPath,
|
||||
Content: content,
|
||||
}
|
||||
|
@ -227,7 +261,7 @@ func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *r
|
|||
if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
|
||||
return fmt.Errorf("setDefaultBranch: %v", err)
|
||||
}
|
||||
if err = models.UpdateRepositoryCtx(ctx, repo, false); err != nil {
|
||||
if err = UpdateRepository(ctx, repo, false); err != nil {
|
||||
return fmt.Errorf("updateRepository: %v", err)
|
||||
}
|
||||
|
||||
|
@ -240,7 +274,7 @@ func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_mo
|
|||
return err
|
||||
}
|
||||
|
||||
if err := models.UpdateRepoSize(ctx, generateRepo); err != nil {
|
||||
if err := UpdateRepoSize(ctx, generateRepo); err != nil {
|
||||
return fmt.Errorf("failed to update size for repository: %v", err)
|
||||
}
|
||||
|
||||
|
@ -250,8 +284,27 @@ func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_mo
|
|||
return nil
|
||||
}
|
||||
|
||||
// GenerateRepoOptions contains the template units to generate
|
||||
type GenerateRepoOptions struct {
|
||||
Name string
|
||||
DefaultBranch string
|
||||
Description string
|
||||
Private bool
|
||||
GitContent bool
|
||||
Topics bool
|
||||
GitHooks bool
|
||||
Webhooks bool
|
||||
Avatar bool
|
||||
IssueLabels bool
|
||||
}
|
||||
|
||||
// IsValid checks whether at least one option is chosen for generation
|
||||
func (gro GenerateRepoOptions) IsValid() bool {
|
||||
return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels // or other items as they are added
|
||||
}
|
||||
|
||||
// GenerateRepository generates a repository from a template
|
||||
func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts models.GenerateRepoOptions) (_ *repo_model.Repository, err error) {
|
||||
func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
|
||||
generateRepo := &repo_model.Repository{
|
||||
OwnerID: owner.ID,
|
||||
Owner: owner,
|
||||
|
@ -288,7 +341,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
|
|||
return generateRepo, err
|
||||
}
|
||||
|
||||
if err = models.CheckDaemonExportOK(ctx, generateRepo); err != nil {
|
||||
if err = CheckDaemonExportOK(ctx, generateRepo); err != nil {
|
||||
return generateRepo, fmt.Errorf("checkDaemonExportOK: %v", err)
|
||||
}
|
||||
|
||||
|
|
57
modules/repository/generate_test.go
Normal file
57
modules/repository/generate_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2019 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 repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var giteaTemplate = []byte(`
|
||||
# Header
|
||||
|
||||
# All .go files
|
||||
**.go
|
||||
|
||||
# All text files in /text/
|
||||
text/*.txt
|
||||
|
||||
# All files in modules folders
|
||||
**/modules/*
|
||||
`)
|
||||
|
||||
func TestGiteaTemplate(t *testing.T) {
|
||||
gt := GiteaTemplate{Content: giteaTemplate}
|
||||
assert.Len(t, gt.Globs(), 3)
|
||||
|
||||
tt := []struct {
|
||||
Path string
|
||||
Match bool
|
||||
}{
|
||||
{Path: "main.go", Match: true},
|
||||
{Path: "a/b/c/d/e.go", Match: true},
|
||||
{Path: "main.txt", Match: false},
|
||||
{Path: "a/b.txt", Match: false},
|
||||
{Path: "text/a.txt", Match: true},
|
||||
{Path: "text/b.txt", Match: true},
|
||||
{Path: "text/c.json", Match: false},
|
||||
{Path: "a/b/c/modules/README.md", Match: true},
|
||||
{Path: "a/b/c/modules/d/README.md", Match: false},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Path, func(t *testing.T) {
|
||||
match := false
|
||||
for _, g := range gt.Globs() {
|
||||
if g.Match(tc.Path) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tc.Match, match)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -444,7 +444,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re
|
|||
}
|
||||
}
|
||||
|
||||
if err = models.UpdateRepositoryCtx(ctx, repo, false); err != nil {
|
||||
if err = UpdateRepository(ctx, repo, false); err != nil {
|
||||
return fmt.Errorf("updateRepository: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
|||
repo.Owner = u
|
||||
}
|
||||
|
||||
if err = models.CheckDaemonExportOK(ctx, repo); err != nil {
|
||||
if err = CheckDaemonExportOK(ctx, repo); err != nil {
|
||||
return repo, fmt.Errorf("checkDaemonExportOK: %v", err)
|
||||
}
|
||||
|
||||
|
@ -168,9 +168,11 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
|||
}
|
||||
}
|
||||
|
||||
if err = models.UpdateRepoSize(ctx, repo); err != nil {
|
||||
log.Error("Failed to update size for repository: %v", err)
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if opts.Mirror {
|
||||
mirrorModel := repo_model.Mirror{
|
||||
|
@ -203,17 +205,24 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
|||
}
|
||||
}
|
||||
|
||||
if err = repo_model.InsertMirror(&mirrorModel); err != nil {
|
||||
if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil {
|
||||
return repo, fmt.Errorf("InsertOne: %v", err)
|
||||
}
|
||||
|
||||
repo.IsMirror = true
|
||||
err = models.UpdateRepository(repo, false)
|
||||
if err = UpdateRepository(ctx, repo, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
repo, err = CleanUpMigrateInfo(ctx, repo)
|
||||
if err = UpdateRepoSize(ctx, repo); err != nil {
|
||||
log.Error("Failed to update size for repository: %v", err)
|
||||
}
|
||||
if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return repo, err
|
||||
return repo, committer.Commit()
|
||||
}
|
||||
|
||||
// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
|
||||
|
@ -253,7 +262,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo
|
|||
}
|
||||
}
|
||||
|
||||
return repo, models.UpdateRepository(repo, false)
|
||||
return repo, UpdateRepository(ctx, repo, false)
|
||||
}
|
||||
|
||||
// SyncReleasesWithTags synchronizes release table with repository tags
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue