first-commit

This commit is contained in:
2025-08-25 15:46:12 +08:00
commit f4d95dfff4
5665 changed files with 705359 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
)
// SyncRepoBranches synchronizes branch table with repository branches
func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return 0, err
}
log.Debug("SyncRepoBranches: in Repo[%d:%s]", repo.ID, repo.FullName())
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
log.Error("OpenRepository[%s]: %w", repo.FullName(), err)
return 0, err
}
defer gitRepo.Close()
return SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doerID)
}
func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) {
objFmt, err := gitRepo.GetObjectFormat()
if err != nil {
return 0, fmt.Errorf("GetObjectFormat: %w", err)
}
if objFmt.Name() != repo.ObjectFormatName {
repo.ObjectFormatName = objFmt.Name()
if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "object_format_name"); err != nil {
return 0, fmt.Errorf("UpdateRepositoryColsWithAutoTime: %w", err)
}
}
allBranches := container.Set[string]{}
{
branches, _, err := gitRepo.GetBranchNames(0, 0)
if err != nil {
return 0, err
}
log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)
for _, branch := range branches {
allBranches.Add(branch)
}
}
dbBranches := make(map[string]*git_model.Branch)
{
branches, err := db.Find[git_model.Branch](ctx, git_model.FindBranchOptions{
ListOptions: db.ListOptionsAll,
RepoID: repo.ID,
})
if err != nil {
return 0, err
}
for _, branch := range branches {
dbBranches[branch.Name] = branch
}
}
var toAdd []*git_model.Branch
var toUpdate []*git_model.Branch
var toRemove []int64
for branch := range allBranches {
dbb := dbBranches[branch]
commit, err := gitRepo.GetBranchCommit(branch)
if err != nil {
return 0, err
}
if dbb == nil {
toAdd = append(toAdd, &git_model.Branch{
RepoID: repo.ID,
Name: branch,
CommitID: commit.ID.String(),
CommitMessage: commit.Summary(),
PusherID: doerID,
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
})
} else if commit.ID.String() != dbb.CommitID {
toUpdate = append(toUpdate, &git_model.Branch{
ID: dbb.ID,
RepoID: repo.ID,
Name: branch,
CommitID: commit.ID.String(),
CommitMessage: commit.Summary(),
PusherID: doerID,
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
})
}
}
for _, dbBranch := range dbBranches {
if !allBranches.Contains(dbBranch.Name) && !dbBranch.IsDeleted {
toRemove = append(toRemove, dbBranch.ID)
}
}
log.Trace("SyncRepoBranches[%s]: toAdd: %v, toUpdate: %v, toRemove: %v", repo.FullName(), toAdd, toUpdate, toRemove)
if len(toAdd) == 0 && len(toRemove) == 0 && len(toUpdate) == 0 {
return int64(len(allBranches)), nil
}
if err := db.WithTx(ctx, func(ctx context.Context) error {
if len(toAdd) > 0 {
if err := git_model.AddBranches(ctx, toAdd); err != nil {
return err
}
}
for _, b := range toUpdate {
if _, err := db.GetEngine(ctx).ID(b.ID).
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted").
Update(b); err != nil {
return err
}
}
if len(toRemove) > 0 {
if err := git_model.DeleteBranches(ctx, repo.ID, doerID, toRemove); err != nil {
return err
}
}
return nil
}); err != nil {
return 0, err
}
return int64(len(allBranches)), nil
}

View File

@@ -0,0 +1,31 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"testing"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestSyncRepoBranches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
_, err := db.GetEngine(db.DefaultContext).ID(1).Update(&repo_model.Repository{ObjectFormatName: "bad-fmt"})
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &git_model.Branch{}))
assert.NoError(t, err)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.Equal(t, "bad-fmt", repo.ObjectFormatName)
_, err = SyncRepoBranches(db.DefaultContext, 1, 0)
assert.NoError(t, err)
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.Equal(t, "sha1", repo.ObjectFormatName)
branch, err := git_model.GetBranch(db.DefaultContext, 1, "master")
assert.NoError(t, err)
assert.Equal(t, "master", branch.Name)
}

View File

@@ -0,0 +1,175 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"fmt"
"net/url"
"time"
"code.gitea.io/gitea/models/avatars"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/cachegroup"
"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"
)
// PushCommit represents a commit in a push operation.
type PushCommit struct {
Sha1 string
Message string
AuthorEmail string
AuthorName string
CommitterEmail string
CommitterName string
Timestamp time.Time
}
// PushCommits represents list of commits in a push operation.
type PushCommits struct {
Commits []*PushCommit
HeadCommit *PushCommit
CompareURL string
Len int
}
// NewPushCommits creates a new PushCommits object.
func NewPushCommits() *PushCommits {
return &PushCommits{}
}
// ToAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object.
func ToAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repo *repo_model.Repository, commit *PushCommit) (*api.PayloadCommit, error) {
var err error
authorUsername := ""
author, ok := emailUsers[commit.AuthorEmail]
if !ok {
author, err = user_model.GetUserByEmail(ctx, commit.AuthorEmail)
if err == nil {
authorUsername = author.Name
emailUsers[commit.AuthorEmail] = author
}
} else {
authorUsername = author.Name
}
committerUsername := ""
committer, ok := emailUsers[commit.CommitterEmail]
if !ok {
committer, err = user_model.GetUserByEmail(ctx, commit.CommitterEmail)
if err == nil {
// TODO: check errors other than email not found.
committerUsername = committer.Name
emailUsers[commit.CommitterEmail] = committer
}
} else {
committerUsername = committer.Name
}
fileStatus, err := git.GetCommitFileStatus(ctx, repo.RepoPath(), commit.Sha1)
if err != nil {
return nil, fmt.Errorf("FileStatus [commit_sha1: %s]: %w", commit.Sha1, err)
}
return &api.PayloadCommit{
ID: commit.Sha1,
Message: commit.Message,
URL: fmt.Sprintf("%s/commit/%s", repo.HTMLURL(), url.PathEscape(commit.Sha1)),
Author: &api.PayloadUser{
Name: commit.AuthorName,
Email: commit.AuthorEmail,
UserName: authorUsername,
},
Committer: &api.PayloadUser{
Name: commit.CommitterName,
Email: commit.CommitterEmail,
UserName: committerUsername,
},
Added: fileStatus.Added,
Removed: fileStatus.Removed,
Modified: fileStatus.Modified,
Timestamp: commit.Timestamp,
}, nil
}
// ToAPIPayloadCommits converts a PushCommits object to api.PayloadCommit format.
// It returns all converted commits and, if provided, the head commit or an error otherwise.
func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repo *repo_model.Repository) ([]*api.PayloadCommit, *api.PayloadCommit, error) {
commits := make([]*api.PayloadCommit, len(pc.Commits))
var headCommit *api.PayloadCommit
emailUsers := make(map[string]*user_model.User)
for i, commit := range pc.Commits {
apiCommit, err := ToAPIPayloadCommit(ctx, emailUsers, repo, commit)
if err != nil {
return nil, nil, err
}
commits[i] = apiCommit
if pc.HeadCommit != nil && pc.HeadCommit.Sha1 == commits[i].ID {
headCommit = apiCommit
}
}
if pc.HeadCommit != nil && headCommit == nil {
var err error
headCommit, err = ToAPIPayloadCommit(ctx, emailUsers, repo, pc.HeadCommit)
if err != nil {
return nil, nil, err
}
}
return commits, headCommit, nil
}
// AvatarLink tries to match user in database with e-mail
// in order to show custom avatar, and falls back to general avatar link.
func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor
v, _ := cache.GetWithContextCache(ctx, cachegroup.EmailAvatarLink, email, func(ctx context.Context, email string) (string, error) {
u, err := user_model.GetUserByEmail(ctx, email)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return "", err
}
return avatars.GenerateEmailAvatarFastLink(ctx, email, size), nil
}
return u.AvatarLinkWithSize(ctx, size), nil
})
return v
}
// CommitToPushCommit transforms a git.Commit to PushCommit type.
func CommitToPushCommit(commit *git.Commit) *PushCommit {
return &PushCommit{
Sha1: commit.ID.String(),
Message: commit.Message(),
AuthorEmail: commit.Author.Email,
AuthorName: commit.Author.Name,
CommitterEmail: commit.Committer.Email,
CommitterName: commit.Committer.Name,
Timestamp: commit.Author.When,
}
}
// GitToPushCommits transforms a list of git.Commits to PushCommits type.
func GitToPushCommits(gitCommits []*git.Commit) *PushCommits {
commits := make([]*PushCommit, 0, len(gitCommits))
for _, commit := range gitCommits {
commits = append(commits, CommitToPushCommit(commit))
}
return &PushCommits{
Commits: commits,
HeadCommit: nil,
CompareURL: "",
Len: len(commits),
}
}

View File

@@ -0,0 +1,202 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"strconv"
"testing"
"time"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestPushCommits_ToAPIPayloadCommits(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pushCommits := NewPushCommits()
pushCommits.Commits = []*PushCommit{
{
Sha1: "69554a6",
CommitterEmail: "user2@example.com",
CommitterName: "User2",
AuthorEmail: "user2@example.com",
AuthorName: "User2",
Message: "not signed commit",
},
{
Sha1: "27566bd",
CommitterEmail: "user2@example.com",
CommitterName: "User2",
AuthorEmail: "user2@example.com",
AuthorName: "User2",
Message: "good signed commit (with not yet validated email)",
},
{
Sha1: "5099b81",
CommitterEmail: "user2@example.com",
CommitterName: "User2",
AuthorEmail: "user2@example.com",
AuthorName: "User2",
Message: "good signed commit",
},
}
pushCommits.HeadCommit = &PushCommit{Sha1: "69554a6"}
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16})
payloadCommits, headCommit, err := pushCommits.ToAPIPayloadCommits(git.DefaultContext, repo)
assert.NoError(t, err)
assert.Len(t, payloadCommits, 3)
assert.NotNil(t, headCommit)
assert.Equal(t, "69554a6", payloadCommits[0].ID)
assert.Equal(t, "not signed commit", payloadCommits[0].Message)
assert.Equal(t, "https://try.gitea.io/user2/repo16/commit/69554a6", payloadCommits[0].URL)
assert.Equal(t, "User2", payloadCommits[0].Committer.Name)
assert.Equal(t, "user2", payloadCommits[0].Committer.UserName)
assert.Equal(t, "User2", payloadCommits[0].Author.Name)
assert.Equal(t, "user2", payloadCommits[0].Author.UserName)
assert.Equal(t, []string{}, payloadCommits[0].Added)
assert.Equal(t, []string{}, payloadCommits[0].Removed)
assert.Equal(t, []string{"readme.md"}, payloadCommits[0].Modified)
assert.Equal(t, "27566bd", payloadCommits[1].ID)
assert.Equal(t, "good signed commit (with not yet validated email)", payloadCommits[1].Message)
assert.Equal(t, "https://try.gitea.io/user2/repo16/commit/27566bd", payloadCommits[1].URL)
assert.Equal(t, "User2", payloadCommits[1].Committer.Name)
assert.Equal(t, "user2", payloadCommits[1].Committer.UserName)
assert.Equal(t, "User2", payloadCommits[1].Author.Name)
assert.Equal(t, "user2", payloadCommits[1].Author.UserName)
assert.Equal(t, []string{}, payloadCommits[1].Added)
assert.Equal(t, []string{}, payloadCommits[1].Removed)
assert.Equal(t, []string{"readme.md"}, payloadCommits[1].Modified)
assert.Equal(t, "5099b81", payloadCommits[2].ID)
assert.Equal(t, "good signed commit", payloadCommits[2].Message)
assert.Equal(t, "https://try.gitea.io/user2/repo16/commit/5099b81", payloadCommits[2].URL)
assert.Equal(t, "User2", payloadCommits[2].Committer.Name)
assert.Equal(t, "user2", payloadCommits[2].Committer.UserName)
assert.Equal(t, "User2", payloadCommits[2].Author.Name)
assert.Equal(t, "user2", payloadCommits[2].Author.UserName)
assert.Equal(t, []string{"readme.md"}, payloadCommits[2].Added)
assert.Equal(t, []string{}, payloadCommits[2].Removed)
assert.Equal(t, []string{}, payloadCommits[2].Modified)
assert.Equal(t, "69554a6", headCommit.ID)
assert.Equal(t, "not signed commit", headCommit.Message)
assert.Equal(t, "https://try.gitea.io/user2/repo16/commit/69554a6", headCommit.URL)
assert.Equal(t, "User2", headCommit.Committer.Name)
assert.Equal(t, "user2", headCommit.Committer.UserName)
assert.Equal(t, "User2", headCommit.Author.Name)
assert.Equal(t, "user2", headCommit.Author.UserName)
assert.Equal(t, []string{}, headCommit.Added)
assert.Equal(t, []string{}, headCommit.Removed)
assert.Equal(t, []string{"readme.md"}, headCommit.Modified)
}
func TestPushCommits_AvatarLink(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pushCommits := NewPushCommits()
pushCommits.Commits = []*PushCommit{
{
Sha1: "abcdef1",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user4@example.com",
AuthorName: "User Four",
Message: "message1",
},
{
Sha1: "abcdef2",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "message2",
},
}
assert.Equal(t,
"/avatars/ab53a2911ddf9b4817ac01ddcd3d975f?size="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor),
pushCommits.AvatarLink(db.DefaultContext, "user2@example.com"))
assert.Equal(t,
"/assets/img/avatar_default.png",
pushCommits.AvatarLink(db.DefaultContext, "nonexistent@example.com"))
}
func TestCommitToPushCommit(t *testing.T) {
now := time.Now()
sig := &git.Signature{
Email: "example@example.com",
Name: "John Doe",
When: now,
}
const hexString = "0123456789abcdef0123456789abcdef01234567"
sha1, err := git.NewIDFromString(hexString)
assert.NoError(t, err)
pushCommit := CommitToPushCommit(&git.Commit{
ID: sha1,
Author: sig,
Committer: sig,
CommitMessage: "Commit Message",
})
assert.Equal(t, hexString, pushCommit.Sha1)
assert.Equal(t, "Commit Message", pushCommit.Message)
assert.Equal(t, "example@example.com", pushCommit.AuthorEmail)
assert.Equal(t, "John Doe", pushCommit.AuthorName)
assert.Equal(t, "example@example.com", pushCommit.CommitterEmail)
assert.Equal(t, "John Doe", pushCommit.CommitterName)
assert.Equal(t, now, pushCommit.Timestamp)
}
func TestListToPushCommits(t *testing.T) {
now := time.Now()
sig := &git.Signature{
Email: "example@example.com",
Name: "John Doe",
When: now,
}
const hexString1 = "0123456789abcdef0123456789abcdef01234567"
hash1, err := git.NewIDFromString(hexString1)
assert.NoError(t, err)
const hexString2 = "fedcba9876543210fedcba9876543210fedcba98"
hash2, err := git.NewIDFromString(hexString2)
assert.NoError(t, err)
l := []*git.Commit{
{
ID: hash1,
Author: sig,
Committer: sig,
CommitMessage: "Message1",
},
{
ID: hash2,
Author: sig,
Committer: sig,
CommitMessage: "Message2",
},
}
pushCommits := GitToPushCommits(l)
if assert.Len(t, pushCommits.Commits, 2) {
assert.Equal(t, "Message1", pushCommits.Commits[0].Message)
assert.Equal(t, hexString1, pushCommits.Commits[0].Sha1)
assert.Equal(t, "example@example.com", pushCommits.Commits[0].AuthorEmail)
assert.Equal(t, now, pushCommits.Commits[0].Timestamp)
assert.Equal(t, "Message2", pushCommits.Commits[1].Message)
assert.Equal(t, hexString2, pushCommits.Commits[1].Sha1)
assert.Equal(t, "example@example.com", pushCommits.Commits[1].AuthorEmail)
assert.Equal(t, now, pushCommits.Commits[1].Timestamp)
}
}

View File

@@ -0,0 +1,57 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"fmt"
"os"
"path/filepath"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
)
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)
}

View File

@@ -0,0 +1,24 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetDirectorySize(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 1)
assert.NoError(t, err)
size, err := getDirectorySize(repo.RepoPath())
assert.NoError(t, err)
repo.Size = 8165 // real size on the disk
assert.Equal(t, repo.Size, size)
}

View File

@@ -0,0 +1,33 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"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(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) {
if user.IsAdmin || user.ID == repo.OwnerID {
return true, nil
}
if err := repo.LoadOwner(ctx); err != nil {
return false, err
}
if repo.Owner.IsOrganization() {
isAdmin, err := organization.OrgFromUser(repo.Owner).IsOrgAdmin(ctx, user.ID)
if err != nil {
return false, err
}
return isAdmin, nil
}
return false, nil
}

87
modules/repository/env.go Normal file
View File

@@ -0,0 +1,87 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"os"
"strconv"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
)
// env keys for git hooks need
const (
EnvRepoName = "GITEA_REPO_NAME"
EnvRepoUsername = "GITEA_REPO_USER_NAME"
EnvRepoID = "GITEA_REPO_ID"
EnvRepoIsWiki = "GITEA_REPO_IS_WIKI"
EnvPusherName = "GITEA_PUSHER_NAME"
EnvPusherEmail = "GITEA_PUSHER_EMAIL"
EnvPusherID = "GITEA_PUSHER_ID"
EnvKeyID = "GITEA_KEY_ID" // public key ID
EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID"
EnvPRID = "GITEA_PR_ID"
EnvPushTrigger = "GITEA_PUSH_TRIGGER"
EnvIsInternal = "GITEA_INTERNAL_PUSH"
EnvAppURL = "GITEA_ROOT_URL"
EnvActionPerm = "GITEA_ACTION_PERM"
)
type PushTrigger string
const (
PushTriggerPRMergeToBase PushTrigger = "pr-merge-to-base"
PushTriggerPRUpdateWithBase PushTrigger = "pr-update-with-base"
)
// InternalPushingEnvironment returns an os environment to switch off hooks on push
// It is recommended to avoid using this unless you are pushing within a transaction
// or if you absolutely are sure that post-receive and pre-receive will do nothing
// We provide the full pushing-environment for other hook providers
func InternalPushingEnvironment(doer *user_model.User, repo *repo_model.Repository) []string {
return append(PushingEnvironment(doer, repo),
EnvIsInternal+"=true",
)
}
// PushingEnvironment returns an os environment to allow hooks to work on push
func PushingEnvironment(doer *user_model.User, repo *repo_model.Repository) []string {
return FullPushingEnvironment(doer, doer, repo, repo.Name, 0)
}
// FullPushingEnvironment returns an os environment to allow hooks to work on push
func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID int64) []string {
isWiki := "false"
if strings.HasSuffix(repoName, ".wiki") {
isWiki = "true"
}
authorSig := author.NewGitSig()
committerSig := committer.NewGitSig()
environ := append(os.Environ(),
"GIT_AUTHOR_NAME="+authorSig.Name,
"GIT_AUTHOR_EMAIL="+authorSig.Email,
"GIT_COMMITTER_NAME="+committerSig.Name,
"GIT_COMMITTER_EMAIL="+committerSig.Email,
EnvRepoName+"="+repoName,
EnvRepoUsername+"="+repo.OwnerName,
EnvRepoIsWiki+"="+isWiki,
EnvPusherName+"="+committer.Name,
EnvPusherID+"="+strconv.FormatInt(committer.ID, 10),
EnvRepoID+"="+strconv.FormatInt(repo.ID, 10),
EnvPRID+"="+strconv.FormatInt(prID, 10),
EnvAppURL+"="+setting.AppURL,
"SSH_ORIGINAL_COMMAND=gitea-internal",
)
if !committer.KeepEmailPrivate {
environ = append(environ, EnvPusherEmail+"="+committer.Email)
}
return environ
}

View File

@@ -0,0 +1,43 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
)
// CanUserForkBetweenOwners returns true if user can fork between owners.
// By default, a user can fork a repository from another owner, but not from themselves.
// Many users really like to fork their own repositories, so add an experimental setting to allow this.
func CanUserForkBetweenOwners(id1, id2 int64) bool {
if id1 != id2 {
return true
}
return setting.Repository.AllowForkIntoSameOwner
}
// CanUserForkRepo returns true if specified user can fork repository.
func CanUserForkRepo(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (bool, error) {
if user == nil {
return false, nil
}
if CanUserForkBetweenOwners(repo.OwnerID, user.ID) && !repo_model.HasForkedRepo(ctx, user.ID, repo.ID) {
return true, nil
}
ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, user.ID)
if err != nil {
return false, err
}
for _, org := range ownedOrgs {
if repo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, repo.ID) {
return true, nil
}
}
return false, nil
}

View File

@@ -0,0 +1,25 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestCanUserForkBetweenOwners(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner)
setting.Repository.AllowForkIntoSameOwner = true
assert.True(t, CanUserForkBetweenOwners(1, 1))
assert.True(t, CanUserForkBetweenOwners(1, 2))
setting.Repository.AllowForkIntoSameOwner = false
assert.False(t, CanUserForkBetweenOwners(1, 1))
assert.True(t, CanUserForkBetweenOwners(1, 2))
}

156
modules/repository/init.go Normal file
View File

@@ -0,0 +1,156 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
type OptionFile struct {
DisplayName string
Description string
}
var (
// Gitignores contains the gitiginore files
Gitignores []string
// Licenses contains the license files
Licenses []string
// Readmes contains the readme files
Readmes []string
// LabelTemplateFiles contains the label template files, each item has its DisplayName and Description
LabelTemplateFiles []OptionFile
labelTemplateFileMap = map[string]string{} // DisplayName => FileName mapping
)
type optionFileList struct {
all []string // all files provided by bindata & custom-path. Sorted.
custom []string // custom files provided by custom-path. Non-sorted, internal use only.
}
// mergeCustomLabelFiles merges the custom label files. Always use the file's main name (DisplayName) as the key to de-duplicate.
func mergeCustomLabelFiles(fl optionFileList) []string {
exts := map[string]int{"": 0, ".yml": 1, ".yaml": 2} // "yaml" file has the highest priority to be used.
m := map[string]string{}
merge := func(list []string) {
sort.Slice(list, func(i, j int) bool { return exts[filepath.Ext(list[i])] < exts[filepath.Ext(list[j])] })
for _, f := range list {
m[strings.TrimSuffix(f, filepath.Ext(f))] = f
}
}
merge(fl.all)
merge(fl.custom)
files := make([]string, 0, len(m))
for _, f := range m {
files = append(files, f)
}
sort.Strings(files)
return files
}
// LoadRepoConfig loads the repository config
func LoadRepoConfig() error {
types := []string{"gitignore", "license", "readme", "label"} // option file directories
typeFiles := make([]optionFileList, len(types))
for i, t := range types {
var err error
if typeFiles[i].all, err = options.AssetFS().ListFiles(t, true); err != nil {
return fmt.Errorf("failed to list %s files: %w", t, err)
}
sort.Strings(typeFiles[i].all)
customPath := filepath.Join(setting.CustomPath, "options", t)
if isDir, err := util.IsDir(customPath); err != nil {
return fmt.Errorf("failed to check custom %s dir: %w", t, err)
} else if isDir {
if typeFiles[i].custom, err = util.ListDirRecursively(customPath, &util.ListDirOptions{SkipCommonHiddenNames: true}); err != nil {
return fmt.Errorf("failed to list custom %s files: %w", t, err)
}
}
}
Gitignores = typeFiles[0].all
Licenses = typeFiles[1].all
Readmes = typeFiles[2].all
// Load label templates
LabelTemplateFiles = nil
labelTemplateFileMap = map[string]string{}
for _, file := range mergeCustomLabelFiles(typeFiles[3]) {
description, err := label.LoadTemplateDescription(file)
if err != nil {
return fmt.Errorf("failed to load labels: %w", err)
}
displayName := strings.TrimSuffix(file, filepath.Ext(file))
labelTemplateFileMap[displayName] = file
LabelTemplateFiles = append(LabelTemplateFiles, OptionFile{DisplayName: displayName, Description: description})
}
// Filter out invalid names and promote preferred licenses.
sortedLicenses := make([]string, 0, len(Licenses))
for _, name := range setting.Repository.PreferredLicenses {
if util.SliceContainsString(Licenses, name, true) {
sortedLicenses = append(sortedLicenses, name)
}
}
for _, name := range Licenses {
if !util.SliceContainsString(setting.Repository.PreferredLicenses, name, true) {
sortedLicenses = append(sortedLicenses, name)
}
}
Licenses = sortedLicenses
return nil
}
// InitializeLabels adds a label set to a repository using a template
func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error {
list, err := LoadTemplateLabelsByDisplayName(labelTemplate)
if err != nil {
return err
}
labels := make([]*issues_model.Label, len(list))
for i := range list {
labels[i] = &issues_model.Label{
Name: list[i].Name,
Exclusive: list[i].Exclusive,
ExclusiveOrder: list[i].ExclusiveOrder,
Description: list[i].Description,
Color: list[i].Color,
}
if isOrg {
labels[i].OrgID = id
} else {
labels[i].RepoID = id
}
}
for _, label := range labels {
if err = issues_model.NewLabel(ctx, label); err != nil {
return err
}
}
return nil
}
// LoadTemplateLabelsByDisplayName loads a label template by its display name
func LoadTemplateLabelsByDisplayName(displayName string) ([]*label.Label, error) {
if fileName, ok := labelTemplateFileMap[displayName]; ok {
return label.LoadTemplateFile(fileName)
}
return nil, label.ErrTemplateLoad{TemplateFile: displayName, OriginalError: fmt.Errorf("label template %q not found", displayName)}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMergeCustomLabels(t *testing.T) {
files := mergeCustomLabelFiles(optionFileList{
all: []string{"a", "a.yaml", "a.yml"},
custom: nil,
})
assert.Equal(t, []string{"a.yaml"}, files, "yaml file should win")
files = mergeCustomLabelFiles(optionFileList{
all: []string{"a", "a.yaml"},
custom: []string{"a"},
})
assert.Equal(t, []string{"a"}, files, "custom file should win")
files = mergeCustomLabelFiles(optionFileList{
all: []string{"a", "a.yml", "a.yaml"},
custom: []string{"a", "a.yml"},
})
assert.Equal(t, []string{"a.yml"}, files, "custom yml file should win if no yaml")
}

View File

@@ -0,0 +1,113 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"bufio"
"bytes"
"fmt"
"regexp"
"strings"
"code.gitea.io/gitea/modules/options"
)
type LicenseValues struct {
Owner string
Email string
Repo string
Year string
}
func GetLicense(name string, values *LicenseValues) ([]byte, error) {
data, err := options.License(name)
if err != nil {
return nil, fmt.Errorf("GetLicense[%s]: %w", name, err)
}
return fillLicensePlaceholder(name, values, data), nil
}
func fillLicensePlaceholder(name string, values *LicenseValues, origin []byte) []byte {
placeholder := getLicensePlaceholder(name)
scanner := bufio.NewScanner(bytes.NewReader(origin))
output := bytes.NewBuffer(nil)
for scanner.Scan() {
line := scanner.Text()
if placeholder.MatchLine == nil || placeholder.MatchLine.MatchString(line) {
for _, v := range placeholder.Owner {
line = strings.ReplaceAll(line, v, values.Owner)
}
for _, v := range placeholder.Email {
line = strings.ReplaceAll(line, v, values.Email)
}
for _, v := range placeholder.Repo {
line = strings.ReplaceAll(line, v, values.Repo)
}
for _, v := range placeholder.Year {
line = strings.ReplaceAll(line, v, values.Year)
}
}
output.WriteString(line + "\n")
}
return output.Bytes()
}
type licensePlaceholder struct {
Owner []string
Email []string
Repo []string
Year []string
MatchLine *regexp.Regexp
}
func getLicensePlaceholder(name string) *licensePlaceholder {
// Some universal placeholders.
// If you want to add a new one, make sure you have check it by `grep -r 'NEW_WORD' options/license` and all of them are placeholders.
ret := &licensePlaceholder{
Owner: []string{
"<name of author>",
"<owner>",
"[NAME]",
"[name of copyright owner]",
"[name of copyright holder]",
"<COPYRIGHT HOLDERS>",
"<copyright holders>",
"<AUTHOR>",
"<author's name or designee>",
"[one or more legally recognised persons or entities offering the Work under the terms and conditions of this Licence]",
},
Email: []string{
"[EMAIL]",
},
Repo: []string{
"<program>",
"<one line to give the program's name and a brief idea of what it does.>",
},
Year: []string{
"<year>",
"[YEAR]",
"{YEAR}",
"[yyyy]",
"[Year]",
"[year]",
},
}
// Some special placeholders for specific licenses.
// It's unsafe to apply them to all licenses.
switch name {
case "0BSD":
return &licensePlaceholder{
Owner: []string{"AUTHOR"},
Email: []string{"EMAIL"},
Year: []string{"YEAR"},
MatchLine: regexp.MustCompile(`Copyright \(C\) YEAR by AUTHOR EMAIL`), // there is another AUTHOR in the file, but it's not a placeholder
}
// Other special placeholders can be added here.
}
return ret
}

View File

@@ -0,0 +1,176 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getLicense(t *testing.T) {
type args struct {
name string
values *LicenseValues
}
tests := []struct {
name string
args args
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "regular",
args: args{
name: "MIT",
values: &LicenseValues{Owner: "Gitea", Year: "2023"},
},
want: `MIT License
Copyright (c) 2023 Gitea
Permission is hereby granted`,
wantErr: assert.NoError,
},
{
name: "license not found",
args: args{
name: "notfound",
},
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetLicense(tt.args.name, tt.args.values)
if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) {
return
}
assert.Contains(t, string(got), tt.want, "GetLicense(%v, %v)", tt.args.name, tt.args.values)
})
}
}
func Test_fillLicensePlaceholder(t *testing.T) {
type args struct {
name string
values *LicenseValues
origin string
}
tests := []struct {
name string
args args
want string
}{
{
name: "owner",
args: args{
name: "regular",
values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: `
<name of author>
<owner>
[NAME]
[name of copyright owner]
[name of copyright holder]
<COPYRIGHT HOLDERS>
<copyright holders>
<AUTHOR>
<author's name or designee>
[one or more legally recognised persons or entities offering the Work under the terms and conditions of this Licence]
`,
},
want: `
Gitea
Gitea
Gitea
Gitea
Gitea
Gitea
Gitea
Gitea
Gitea
Gitea
`,
},
{
name: "email",
args: args{
name: "regular",
values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: `
[EMAIL]
`,
},
want: `
teabot@gitea.io
`,
},
{
name: "repo",
args: args{
name: "regular",
values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: `
<program>
<one line to give the program's name and a brief idea of what it does.>
`,
},
want: `
gitea
gitea
`,
},
{
name: "year",
args: args{
name: "regular",
values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: `
<year>
[YEAR]
{YEAR}
[yyyy]
[Year]
[year]
`,
},
want: `
2023
2023
2023
2023
2023
2023
`,
},
{
name: "0BSD",
args: args{
name: "0BSD",
values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: `
Copyright (C) YEAR by AUTHOR EMAIL
...
... THE AUTHOR BE LIABLE FOR ...
`,
},
want: `
Copyright (C) 2023 by Gitea teabot@gitea.io
...
... THE AUTHOR BE LIABLE FOR ...
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, string(fillLicensePlaceholder(tt.args.name, tt.args.values, []byte(tt.args.origin))), "fillLicensePlaceholder(%v, %v, %v)", tt.args.name, tt.args.values, tt.args.origin)
})
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"testing"
"code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}

View File

@@ -0,0 +1,71 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"code.gitea.io/gitea/modules/git"
)
// PushUpdateOptions defines the push update options
type PushUpdateOptions struct {
PusherID int64
PusherName string
RepoUserName string
RepoName string
RefFullName git.RefName // branch, tag or other name to push
OldCommitID string
NewCommitID string
}
// IsNewRef return true if it's a first-time push to a branch, tag or etc.
func (opts *PushUpdateOptions) IsNewRef() bool {
commitID, err := git.NewIDFromString(opts.OldCommitID)
return err == nil && commitID.IsZero()
}
// IsDelRef return true if it's a deletion to a branch or tag
func (opts *PushUpdateOptions) IsDelRef() bool {
commitID, err := git.NewIDFromString(opts.NewCommitID)
return err == nil && commitID.IsZero()
}
// IsUpdateRef return true if it's an update operation
func (opts *PushUpdateOptions) IsUpdateRef() bool {
return !opts.IsNewRef() && !opts.IsDelRef()
}
// IsNewTag return true if it's a creation to a tag
func (opts *PushUpdateOptions) IsNewTag() bool {
return opts.RefFullName.IsTag() && opts.IsNewRef()
}
// IsDelTag return true if it's a deletion to a tag
func (opts *PushUpdateOptions) IsDelTag() bool {
return opts.RefFullName.IsTag() && opts.IsDelRef()
}
// IsNewBranch return true if it's the first-time push to a branch
func (opts *PushUpdateOptions) IsNewBranch() bool {
return opts.RefFullName.IsBranch() && opts.IsNewRef()
}
// IsUpdateBranch return true if it's not the first push to a branch
func (opts *PushUpdateOptions) IsUpdateBranch() bool {
return opts.RefFullName.IsBranch() && opts.IsUpdateRef()
}
// IsDelBranch return true if it's a deletion to a branch
func (opts *PushUpdateOptions) IsDelBranch() bool {
return opts.RefFullName.IsBranch() && opts.IsDelRef()
}
// RefName returns simple name for ref
func (opts *PushUpdateOptions) RefName() string {
return opts.RefFullName.ShortName()
}
// RepoFullName returns repo full name
func (opts *PushUpdateOptions) RepoFullName() string {
return opts.RepoUserName + "/" + opts.RepoName
}

274
modules/repository/repo.go Normal file
View File

@@ -0,0 +1,274 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"errors"
"fmt"
"io"
"strings"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
)
/*
GitHub, GitLab, Gogs: *.wiki.git
BitBucket: *.git/wiki
*/
var commonWikiURLSuffixes = []string{".wiki.git", ".git/wiki"}
// WikiRemoteURL returns accessible repository URL for wiki if exists.
// Otherwise, it returns an empty string.
func WikiRemoteURL(ctx context.Context, remote string) string {
remote = strings.TrimSuffix(remote, ".git")
for _, suffix := range commonWikiURLSuffixes {
wikiURL := remote + suffix
if git.IsRepoURLAccessible(ctx, wikiURL) {
return wikiURL
}
}
return ""
}
// SyncRepoTags synchronizes releases table with repository tags
func SyncRepoTags(ctx context.Context, repoID int64) error {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return err
}
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return err
}
defer gitRepo.Close()
return SyncReleasesWithTags(ctx, repo, gitRepo)
}
// StoreMissingLfsObjectsInRepository downloads missing LFS objects
func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, lfsClient lfs.Client) error {
contentStore := lfs.NewContentStore()
pointerChan := make(chan lfs.PointerBlob)
errChan := make(chan error, 1)
go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan)
downloadObjects := func(pointers []lfs.Pointer) error {
err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error {
if errors.Is(objectError, lfs.ErrObjectNotExist) {
log.Warn("Ignoring missing upstream LFS object %-v: %v", p, objectError)
return nil
}
if objectError != nil {
return objectError
}
defer content.Close()
_, err := git_model.NewLFSMetaObject(ctx, repo.ID, p)
if err != nil {
log.Error("Repo[%-v]: Error creating LFS meta object %-v: %v", repo, p, err)
return err
}
if err := contentStore.Put(p, content); err != nil {
log.Error("Repo[%-v]: Error storing content for LFS meta object %-v: %v", repo, p, err)
if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, p.Oid); err2 != nil {
log.Error("Repo[%-v]: Error removing LFS meta object %-v: %v", repo, p, err2)
}
return err
}
return nil
})
if err != nil {
select {
case <-ctx.Done():
return nil
default:
}
}
return err
}
var batch []lfs.Pointer
for pointerBlob := range pointerChan {
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repo.ID, pointerBlob.Oid)
if err != nil && err != git_model.ErrLFSObjectNotExist {
log.Error("Repo[%-v]: Error querying LFS meta object %-v: %v", repo, pointerBlob.Pointer, err)
return err
}
if meta != nil {
log.Trace("Repo[%-v]: Skipping unknown LFS meta object %-v", repo, pointerBlob.Pointer)
continue
}
log.Trace("Repo[%-v]: LFS object %-v not present in repository", repo, pointerBlob.Pointer)
exist, err := contentStore.Exists(pointerBlob.Pointer)
if err != nil {
log.Error("Repo[%-v]: Error checking if LFS object %-v exists: %v", repo, pointerBlob.Pointer, err)
return err
}
if exist {
log.Trace("Repo[%-v]: LFS object %-v already present; creating meta object", repo, pointerBlob.Pointer)
_, err := git_model.NewLFSMetaObject(ctx, repo.ID, pointerBlob.Pointer)
if err != nil {
log.Error("Repo[%-v]: Error creating LFS meta object %-v: %v", repo, pointerBlob.Pointer, err)
return err
}
} else {
if setting.LFS.MaxFileSize > 0 && pointerBlob.Size > setting.LFS.MaxFileSize {
log.Info("Repo[%-v]: LFS object %-v download denied because of LFS_MAX_FILE_SIZE=%d < size %d", repo, pointerBlob.Pointer, setting.LFS.MaxFileSize, pointerBlob.Size)
continue
}
batch = append(batch, pointerBlob.Pointer)
if len(batch) >= lfsClient.BatchSize() {
if err := downloadObjects(batch); err != nil {
return err
}
batch = nil
}
}
}
if len(batch) > 0 {
if err := downloadObjects(batch); err != nil {
return err
}
}
err, has := <-errChan
if has {
log.Error("Repo[%-v]: Error enumerating LFS objects for repository: %v", repo, err)
return err
}
return nil
}
// shortRelease to reduce load memory, this struct can replace repo_model.Release
type shortRelease struct {
ID int64
TagName string
Sha1 string
IsTag bool
}
func (shortRelease) TableName() string {
return "release"
}
// SyncReleasesWithTags is a tag<->release table
// synchronization which overwrites all Releases from the repository tags. This
// can be relied on since a pull-mirror is always identical to its
// upstream. Hence, after each sync we want the release set to be
// identical to the upstream tag set. This is much more efficient for
// repositories like https://github.com/vim/vim (with over 13000 tags).
func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error {
log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
tags, _, err := gitRepo.GetTagInfos(0, 0)
if err != nil {
return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
}
var added, deleted, updated int
err = db.WithTx(ctx, func(ctx context.Context) error {
dbReleases, err := db.Find[shortRelease](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
IncludeDrafts: true,
IncludeTags: true,
})
if err != nil {
return fmt.Errorf("unable to FindReleases in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
}
inserts, deletes, updates := calcSync(tags, dbReleases)
//
// make release set identical to upstream tags
//
for _, tag := range inserts {
release := repo_model.Release{
RepoID: repo.ID,
TagName: tag.Name,
LowerTagName: strings.ToLower(tag.Name),
Sha1: tag.Object.String(),
// NOTE: ignored, The NumCommits value is calculated and cached on demand when the UI requires it.
NumCommits: -1,
CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()),
IsTag: true,
}
if err := db.Insert(ctx, release); err != nil {
return fmt.Errorf("unable insert tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err)
}
}
// only delete tags releases
if len(deletes) > 0 {
if _, err := db.GetEngine(ctx).Where("repo_id=?", repo.ID).
In("id", deletes).
Delete(&repo_model.Release{}); err != nil {
return fmt.Errorf("unable to delete tags for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
}
}
for _, tag := range updates {
if _, err := db.GetEngine(ctx).Where("repo_id = ? AND lower_tag_name = ?", repo.ID, strings.ToLower(tag.Name)).
Cols("sha1", "created_unix").
Update(&repo_model.Release{
Sha1: tag.Object.String(),
CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()),
}); err != nil {
return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err)
}
}
added, deleted, updated = len(deletes), len(updates), len(inserts)
return nil
})
if err != nil {
return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
}
log.Trace("SyncReleasesWithTags: %d tags added, %d tags deleted, %d tags updated", added, deleted, updated)
return nil
}
func calcSync(destTags []*git.Tag, dbTags []*shortRelease) ([]*git.Tag, []int64, []*git.Tag) {
destTagMap := make(map[string]*git.Tag)
for _, tag := range destTags {
destTagMap[tag.Name] = tag
}
dbTagMap := make(map[string]*shortRelease)
for _, rel := range dbTags {
dbTagMap[rel.TagName] = rel
}
inserted := make([]*git.Tag, 0, 10)
updated := make([]*git.Tag, 0, 10)
for _, tag := range destTags {
rel := dbTagMap[tag.Name]
if rel == nil {
inserted = append(inserted, tag)
} else if rel.Sha1 != tag.Object.String() {
updated = append(updated, tag)
}
}
deleted := make([]int64, 0, 10)
for _, tag := range dbTags {
if destTagMap[tag.TagName] == nil && tag.IsTag {
deleted = append(deleted, tag.ID)
}
}
return inserted, deleted, updated
}

View File

@@ -0,0 +1,76 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"testing"
"code.gitea.io/gitea/modules/git"
"github.com/stretchr/testify/assert"
)
func Test_calcSync(t *testing.T) {
gitTags := []*git.Tag{
/*{
Name: "v0.1.0-beta", //deleted tag
Object: git.MustIDFromString(""),
},
{
Name: "v0.1.1-beta", //deleted tag but release should not be deleted because it's a release
Object: git.MustIDFromString(""),
},
*/
{
Name: "v1.0.0", // keep as before
Object: git.MustIDFromString("1006e6e13c73ad3d9e2d5682ad266b5016523485"),
},
{
Name: "v1.1.0", // retagged with new commit id
Object: git.MustIDFromString("bbdb7df30248e7d4a26a909c8d2598a152e13868"),
},
{
Name: "v1.2.0", // new tag
Object: git.MustIDFromString("a5147145e2f24d89fd6d2a87826384cc1d253267"),
},
}
dbReleases := []*shortRelease{
{
ID: 1,
TagName: "v0.1.0-beta",
Sha1: "244758d7da8dd1d9e0727e8cb7704ed4ba9a17c3",
IsTag: true,
},
{
ID: 2,
TagName: "v0.1.1-beta",
Sha1: "244758d7da8dd1d9e0727e8cb7704ed4ba9a17c3",
IsTag: false,
},
{
ID: 3,
TagName: "v1.0.0",
Sha1: "1006e6e13c73ad3d9e2d5682ad266b5016523485",
},
{
ID: 4,
TagName: "v1.1.0",
Sha1: "53ab18dcecf4152b58328d1f47429510eb414d50",
},
}
inserts, deletes, updates := calcSync(gitTags, dbReleases)
if assert.Len(t, inserts, 1, "inserts") {
assert.Equal(t, *gitTags[2], *inserts[0], "inserts equal")
}
if assert.Len(t, deletes, 1, "deletes") {
assert.EqualValues(t, 1, deletes[0], "deletes equal")
}
if assert.Len(t, updates, 1, "updates") {
assert.Equal(t, *gitTags[1], *updates[0], "updates equal")
}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"fmt"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// CreateTemporaryPath creates a temporary path
func CreateTemporaryPath(prefix string) (string, context.CancelFunc, error) {
basePath, cleanup, err := setting.AppDataTempDir("local-repo").MkdirTempRandom(prefix + ".git")
if err != nil {
log.Error("Unable to create temporary directory: %s-*.git (%v)", prefix, err)
return "", nil, fmt.Errorf("failed to create dir %s-*.git: %w", prefix, err)
}
return basePath, cleanup, nil
}