aboutsummaryrefslogtreecommitdiffstats
path: root/services/mirror
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2021-06-14 19:20:43 +0200
committerGitHub <noreply@github.com>2021-06-14 19:20:43 +0200
commit440039c0cce18622b12da5677bf6585caed6070a (patch)
tree8f8532a2d40983b35b3fdb5460b47218b26bbd89 /services/mirror
parent5d113bdd1905c73fb8071f420ae2d248202971f9 (diff)
downloadgitea-440039c0cce18622b12da5677bf6585caed6070a.tar.gz
gitea-440039c0cce18622b12da5677bf6585caed6070a.zip
Add push to remote mirror repository (#15157)
* Added push mirror model. * Integrated push mirror into queue. * Moved methods into own file. * Added basic implementation. * Mirror wiki too. * Removed duplicated method. * Get url for different remotes. * Added migration. * Unified remote url access. * Add/Remove push mirror remotes. * Prevent hangs with missing credentials. * Moved code between files. * Changed sanitizer interface. * Added push mirror backend methods. * Only update the mirror remote. * Limit refs on push. * Added UI part. * Added missing table. * Delete mirror if repository gets removed. * Changed signature. Handle object errors. * Added upload method. * Added "upload" unit tests. * Added transfer adapter unit tests. * Send correct headers. * Added pushing of LFS objects. * Added more logging. * Simpler body handling. * Process files in batches to reduce HTTP calls. * Added created timestamp. * Fixed invalid column name. * Changed name to prevent xorm auto setting. * Remove table header im empty. * Strip exit code from error message. * Added docs page about mirroring. * Fixed date. * Fixed merge errors. * Moved test to integrations. * Added push mirror test. * Added test.
Diffstat (limited to 'services/mirror')
-rw-r--r--services/mirror/mirror.go578
-rw-r--r--services/mirror/mirror_pull.go452
-rw-r--r--services/mirror/mirror_push.go242
-rw-r--r--services/mirror/mirror_test.go96
4 files changed, 739 insertions, 629 deletions
diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go
index 839d692f97..1e30c919e6 100644
--- a/services/mirror/mirror.go
+++ b/services/mirror/mirror.go
@@ -7,585 +7,97 @@ package mirror
import (
"context"
"fmt"
- "net/url"
"strconv"
"strings"
- "time"
"code.gitea.io/gitea/models"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/notification"
- repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sync"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
)
// mirrorQueue holds an UniqueQueue object of the mirror
var mirrorQueue = sync.NewUniqueQueue(setting.Repository.MirrorQueueLength)
-func readAddress(m *models.Mirror) {
- if len(m.Address) > 0 {
- return
- }
- var err error
- m.Address, err = remoteAddress(m.Repo.RepoPath())
- if err != nil {
- log.Error("remoteAddress: %v", err)
- }
-}
-
-func remoteAddress(repoPath string) (string, error) {
- var cmd *git.Command
- err := git.LoadGitVersion()
- if err != nil {
- return "", err
- }
- if git.CheckGitVersionAtLeast("2.7") == nil {
- cmd = git.NewCommand("remote", "get-url", "origin")
- } else {
- cmd = git.NewCommand("config", "--get", "remote.origin.url")
- }
-
- result, err := cmd.RunInDir(repoPath)
- if err != nil {
- if strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
- return "", nil
- }
- return "", err
- }
- if len(result) > 0 {
- return result[:len(result)-1], nil
- }
- return "", nil
-}
-
-// sanitizeOutput sanitizes output of a command, replacing occurrences of the
-// repository's remote address with a sanitized version.
-func sanitizeOutput(output, repoPath string) (string, error) {
- remoteAddr, err := remoteAddress(repoPath)
- if err != nil {
- // if we're unable to load the remote address, then we're unable to
- // sanitize.
- return "", err
- }
- return util.SanitizeMessage(output, remoteAddr), nil
-}
-
-// AddressNoCredentials returns mirror address from Git repository config without credentials.
-func AddressNoCredentials(m *models.Mirror) string {
- readAddress(m)
- u, err := url.Parse(m.Address)
- if err != nil {
- // this shouldn't happen but just return it unsanitised
- return m.Address
- }
- u.User = nil
- return u.String()
-}
-
-// UpdateAddress writes new address to Git repository and database
-func UpdateAddress(m *models.Mirror, addr string) error {
- repoPath := m.Repo.RepoPath()
- // Remove old origin
- _, err := git.NewCommand("remote", "rm", "origin").RunInDir(repoPath)
- if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
- return err
- }
-
- _, err = git.NewCommand("remote", "add", "origin", "--mirror=fetch", addr).RunInDir(repoPath)
- if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
- return err
- }
-
- if m.Repo.HasWiki() {
- wikiPath := m.Repo.WikiPath()
- wikiRemotePath := repo_module.WikiRemoteURL(addr)
- // Remove old origin of wiki
- _, err := git.NewCommand("remote", "rm", "origin").RunInDir(wikiPath)
- if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
- return err
- }
-
- _, err = git.NewCommand("remote", "add", "origin", "--mirror=fetch", wikiRemotePath).RunInDir(wikiPath)
- if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
- return err
- }
- }
-
- m.Repo.OriginalURL = addr
- return models.UpdateRepositoryCols(m.Repo, "original_url")
-}
-
-// gitShortEmptySha Git short empty SHA
-const gitShortEmptySha = "0000000"
-
-// mirrorSyncResult contains information of a updated reference.
-// If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
-// If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty.
-type mirrorSyncResult struct {
- refName string
- oldCommitID string
- newCommitID string
-}
-
-// parseRemoteUpdateOutput detects create, update and delete operations of references from upstream.
-func parseRemoteUpdateOutput(output string) []*mirrorSyncResult {
- results := make([]*mirrorSyncResult, 0, 3)
- lines := strings.Split(output, "\n")
- for i := range lines {
- // Make sure reference name is presented before continue
- idx := strings.Index(lines[i], "-> ")
- if idx == -1 {
- continue
- }
-
- refName := lines[i][idx+3:]
-
- switch {
- case strings.HasPrefix(lines[i], " * "): // New reference
- if strings.HasPrefix(lines[i], " * [new tag]") {
- refName = git.TagPrefix + refName
- } else if strings.HasPrefix(lines[i], " * [new branch]") {
- refName = git.BranchPrefix + refName
- }
- results = append(results, &mirrorSyncResult{
- refName: refName,
- oldCommitID: gitShortEmptySha,
- })
- case strings.HasPrefix(lines[i], " - "): // Delete reference
- results = append(results, &mirrorSyncResult{
- refName: refName,
- newCommitID: gitShortEmptySha,
- })
- case strings.HasPrefix(lines[i], " + "): // Force update
- if idx := strings.Index(refName, " "); idx > -1 {
- refName = refName[:idx]
- }
- delimIdx := strings.Index(lines[i][3:], " ")
- if delimIdx == -1 {
- log.Error("SHA delimiter not found: %q", lines[i])
- continue
- }
- shas := strings.Split(lines[i][3:delimIdx+3], "...")
- if len(shas) != 2 {
- log.Error("Expect two SHAs but not what found: %q", lines[i])
- continue
- }
- results = append(results, &mirrorSyncResult{
- refName: refName,
- oldCommitID: shas[0],
- newCommitID: shas[1],
- })
- case strings.HasPrefix(lines[i], " "): // New commits of a reference
- delimIdx := strings.Index(lines[i][3:], " ")
- if delimIdx == -1 {
- log.Error("SHA delimiter not found: %q", lines[i])
- continue
- }
- shas := strings.Split(lines[i][3:delimIdx+3], "..")
- if len(shas) != 2 {
- log.Error("Expect two SHAs but not what found: %q", lines[i])
- continue
- }
- results = append(results, &mirrorSyncResult{
- refName: refName,
- oldCommitID: shas[0],
- newCommitID: shas[1],
- })
-
- default:
- log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i])
- }
- }
- return results
-}
-
-// runSync returns true if sync finished without error.
-func runSync(ctx context.Context, m *models.Mirror) ([]*mirrorSyncResult, bool) {
- repoPath := m.Repo.RepoPath()
- wikiPath := m.Repo.WikiPath()
- timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
-
- log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo)
- gitArgs := []string{"remote", "update"}
- if m.EnablePrune {
- gitArgs = append(gitArgs, "--prune")
- }
-
- stdoutBuilder := strings.Builder{}
- stderrBuilder := strings.Builder{}
- if err := git.NewCommand(gitArgs...).
- SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())).
- RunInDirTimeoutPipeline(timeout, repoPath, &stdoutBuilder, &stderrBuilder); err != nil {
- stdout := stdoutBuilder.String()
- stderr := stderrBuilder.String()
- // sanitize the output, since it may contain the remote address, which may
- // contain a password
- stderrMessage, sanitizeErr := sanitizeOutput(stderr, repoPath)
- if sanitizeErr != nil {
- log.Error("sanitizeOutput failed on stderr: %v", sanitizeErr)
- }
- stdoutMessage, sanitizeErr := sanitizeOutput(stdout, repoPath)
- if sanitizeErr != nil {
- log.Error("sanitizeOutput failed: %v", sanitizeErr)
- }
-
- log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
- desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", m.Repo.FullName(), stderrMessage)
- if err = models.CreateRepositoryNotice(desc); err != nil {
- log.Error("CreateRepositoryNotice: %v", err)
- }
- return nil, false
- }
- output := stderrBuilder.String()
-
- gitRepo, err := git.OpenRepository(repoPath)
- if err != nil {
- log.Error("OpenRepository: %v", err)
- return nil, false
- }
- defer gitRepo.Close()
-
- log.Trace("SyncMirrors [repo: %-v]: syncing releases with tags...", m.Repo)
- if err = repo_module.SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
- log.Error("Failed to synchronize tags to releases for repository: %v", err)
- }
-
- if m.LFS && setting.LFS.StartServer {
- log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
- readAddress(m)
- ep := lfs.DetermineEndpoint(m.Address, m.LFSEndpoint)
- if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, ep); err != nil {
- log.Error("Failed to synchronize LFS objects for repository: %v", err)
- }
- }
-
- log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
- if err := m.Repo.UpdateSize(models.DefaultDBContext()); err != nil {
- log.Error("Failed to update size for mirror repository: %v", err)
- }
-
- if m.Repo.HasWiki() {
- log.Trace("SyncMirrors [repo: %-v Wiki]: running git remote update...", m.Repo)
- stderrBuilder.Reset()
- stdoutBuilder.Reset()
- if err := git.NewCommand("remote", "update", "--prune").
- SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())).
- RunInDirTimeoutPipeline(timeout, wikiPath, &stdoutBuilder, &stderrBuilder); err != nil {
- stdout := stdoutBuilder.String()
- stderr := stderrBuilder.String()
- // sanitize the output, since it may contain the remote address, which may
- // contain a password
- stderrMessage, sanitizeErr := sanitizeOutput(stderr, wikiPath)
- if sanitizeErr != nil {
- log.Error("sanitizeOutput failed on stderr: %v", sanitizeErr)
- }
- stdoutMessage, sanitizeErr := sanitizeOutput(stdout, wikiPath)
- if sanitizeErr != nil {
- log.Error("sanitizeOutput failed: %v", sanitizeErr)
- }
-
- log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
- desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", m.Repo.FullName(), stderrMessage)
- if err = models.CreateRepositoryNotice(desc); err != nil {
- log.Error("CreateRepositoryNotice: %v", err)
- }
- return nil, false
- }
- log.Trace("SyncMirrors [repo: %-v Wiki]: git remote update complete", m.Repo)
- }
-
- log.Trace("SyncMirrors [repo: %-v]: invalidating mirror branch caches...", m.Repo)
- branches, _, err := repo_module.GetBranches(m.Repo, 0, 0)
- if err != nil {
- log.Error("GetBranches: %v", err)
- return nil, false
- }
-
- for _, branch := range branches {
- cache.Remove(m.Repo.GetCommitsCountCacheKey(branch.Name, true))
- }
-
- m.UpdatedUnix = timeutil.TimeStampNow()
- return parseRemoteUpdateOutput(output), true
-}
-
-// Address returns mirror address from Git repository config without credentials.
-func Address(m *models.Mirror) string {
- readAddress(m)
- return util.SanitizeURLCredentials(m.Address, false)
-}
-
-// Username returns the mirror address username
-func Username(m *models.Mirror) string {
- readAddress(m)
- u, err := url.Parse(m.Address)
- if err != nil {
- // this shouldn't happen but if it does return ""
- return ""
- }
- return u.User.Username()
-}
-
-// Password returns the mirror address password
-func Password(m *models.Mirror) string {
- readAddress(m)
- u, err := url.Parse(m.Address)
- if err != nil {
- // this shouldn't happen but if it does return ""
- return ""
- }
- password, _ := u.User.Password()
- return password
-}
-
// Update checks and updates mirror repositories.
func Update(ctx context.Context) error {
log.Trace("Doing: Update")
- if err := models.MirrorsIterate(func(idx int, bean interface{}) error {
- m := bean.(*models.Mirror)
- if m.Repo == nil {
- log.Error("Disconnected mirror repository found: %d", m.ID)
+
+ handler := func(idx int, bean interface{}) error {
+ var item string
+ if m, ok := bean.(*models.Mirror); ok {
+ if m.Repo == nil {
+ log.Error("Disconnected mirror found: %d", m.ID)
+ return nil
+ }
+ item = fmt.Sprintf("pull %d", m.RepoID)
+ } else if m, ok := bean.(*models.PushMirror); ok {
+ if m.Repo == nil {
+ log.Error("Disconnected push-mirror found: %d", m.ID)
+ return nil
+ }
+ item = fmt.Sprintf("push %d", m.ID)
+ } else {
+ log.Error("Unknown bean: %v", bean)
return nil
}
+
select {
case <-ctx.Done():
return fmt.Errorf("Aborted")
default:
- mirrorQueue.Add(m.RepoID)
+ mirrorQueue.Add(item)
return nil
}
- }); err != nil {
- log.Trace("Update: %v", err)
+ }
+
+ if err := models.MirrorsIterate(handler); err != nil {
+ log.Error("MirrorsIterate: %v", err)
+ return err
+ }
+ if err := models.PushMirrorsIterate(handler); err != nil {
+ log.Error("PushMirrorsIterate: %v", err)
return err
}
log.Trace("Finished: Update")
return nil
}
-// SyncMirrors checks and syncs mirrors.
+// syncMirrors checks and syncs mirrors.
// FIXME: graceful: this should be a persistable queue
-func SyncMirrors(ctx context.Context) {
+func syncMirrors(ctx context.Context) {
// Start listening on new sync requests.
for {
select {
case <-ctx.Done():
mirrorQueue.Close()
return
- case repoID := <-mirrorQueue.Queue():
- syncMirror(ctx, repoID)
- }
- }
-}
-
-func syncMirror(ctx context.Context, repoID string) {
- log.Trace("SyncMirrors [repo_id: %v]", repoID)
- defer func() {
- err := recover()
- if err == nil {
- return
- }
- // There was a panic whilst syncMirrors...
- log.Error("PANIC whilst syncMirrors[%s] Panic: %v\nStacktrace: %s", repoID, err, log.Stack(2))
- }()
- mirrorQueue.Remove(repoID)
-
- id, _ := strconv.ParseInt(repoID, 10, 64)
- m, err := models.GetMirrorByRepoID(id)
- if err != nil {
- log.Error("GetMirrorByRepoID [%s]: %v", repoID, err)
- return
- }
-
- log.Trace("SyncMirrors [repo: %-v]: Running Sync", m.Repo)
- results, ok := runSync(ctx, m)
- if !ok {
- return
- }
-
- log.Trace("SyncMirrors [repo: %-v]: Scheduling next update", m.Repo)
- m.ScheduleNextUpdate()
- if err = models.UpdateMirror(m); err != nil {
- log.Error("UpdateMirror [%s]: %v", repoID, err)
- return
- }
-
- var gitRepo *git.Repository
- if len(results) == 0 {
- log.Trace("SyncMirrors [repo: %-v]: no branches updated", m.Repo)
- } else {
- log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
- gitRepo, err = git.OpenRepository(m.Repo.RepoPath())
- if err != nil {
- log.Error("OpenRepository [%d]: %v", m.RepoID, err)
- return
- }
- defer gitRepo.Close()
-
- if ok := checkAndUpdateEmptyRepository(m, gitRepo, results); !ok {
- return
- }
- }
-
- for _, result := range results {
- // Discard GitHub pull requests, i.e. refs/pull/*
- if strings.HasPrefix(result.refName, "refs/pull/") {
- continue
- }
-
- tp, _ := git.SplitRefName(result.refName)
-
- // Create reference
- if result.oldCommitID == gitShortEmptySha {
- if tp == git.TagPrefix {
- tp = "tag"
- } else if tp == git.BranchPrefix {
- tp = "branch"
+ case item := <-mirrorQueue.Queue():
+ id, _ := strconv.ParseInt(item[5:], 10, 64)
+ if strings.HasPrefix(item, "pull") {
+ _ = SyncPullMirror(ctx, id)
+ } else if strings.HasPrefix(item, "push") {
+ _ = SyncPushMirror(ctx, id)
+ } else {
+ log.Error("Unknown item in queue: %v", item)
}
- commitID, err := gitRepo.GetRefCommitID(result.refName)
- if err != nil {
- log.Error("gitRepo.GetRefCommitID [repo_id: %s, ref_name: %s]: %v", m.RepoID, result.refName, err)
- continue
- }
- notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{
- RefFullName: result.refName,
- OldCommitID: git.EmptySHA,
- NewCommitID: commitID,
- }, repo_module.NewPushCommits())
- notification.NotifySyncCreateRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
- continue
- }
-
- // Delete reference
- if result.newCommitID == gitShortEmptySha {
- notification.NotifySyncDeleteRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
- continue
- }
-
- // Push commits
- oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID)
- if err != nil {
- log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
- continue
- }
- newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID)
- if err != nil {
- log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
- continue
- }
- commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
- if err != nil {
- log.Error("CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err)
- continue
+ mirrorQueue.Remove(item)
}
-
- theCommits := repo_module.ListToPushCommits(commits)
- if len(theCommits.Commits) > setting.UI.FeedMaxCommitNum {
- theCommits.Commits = theCommits.Commits[:setting.UI.FeedMaxCommitNum]
- }
-
- theCommits.CompareURL = m.Repo.ComposeCompareURL(oldCommitID, newCommitID)
-
- notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{
- RefFullName: result.refName,
- OldCommitID: oldCommitID,
- NewCommitID: newCommitID,
- }, theCommits)
- }
- log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo)
-
- // Get latest commit date and update to current repository updated time
- commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath())
- if err != nil {
- log.Error("GetLatestCommitDate [%d]: %v", m.RepoID, err)
- return
- }
-
- if err = models.UpdateRepositoryUpdatedTime(m.RepoID, commitDate); err != nil {
- log.Error("Update repository 'updated_unix' [%d]: %v", m.RepoID, err)
- return
}
-
- log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo)
-}
-
-func checkAndUpdateEmptyRepository(m *models.Mirror, gitRepo *git.Repository, results []*mirrorSyncResult) bool {
- if !m.Repo.IsEmpty {
- return true
- }
-
- hasDefault := false
- hasMaster := false
- hasMain := false
- defaultBranchName := m.Repo.DefaultBranch
- if len(defaultBranchName) == 0 {
- defaultBranchName = setting.Repository.DefaultBranch
- }
- firstName := ""
- for _, result := range results {
- if strings.HasPrefix(result.refName, "refs/pull/") {
- continue
- }
- tp, name := git.SplitRefName(result.refName)
- if len(tp) > 0 && tp != git.BranchPrefix {
- continue
- }
- if len(firstName) == 0 {
- firstName = name
- }
-
- hasDefault = hasDefault || name == defaultBranchName
- hasMaster = hasMaster || name == "master"
- hasMain = hasMain || name == "main"
- }
-
- if len(firstName) > 0 {
- if hasDefault {
- m.Repo.DefaultBranch = defaultBranchName
- } else if hasMaster {
- m.Repo.DefaultBranch = "master"
- } else if hasMain {
- m.Repo.DefaultBranch = "main"
- } else {
- m.Repo.DefaultBranch = firstName
- }
- // Update the git repository default branch
- if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil {
- if !git.IsErrUnsupportedVersion(err) {
- log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err)
- desc := fmt.Sprintf("Failed to uupdate default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err)
- if err = models.CreateRepositoryNotice(desc); err != nil {
- log.Error("CreateRepositoryNotice: %v", err)
- }
- return false
- }
- }
- m.Repo.IsEmpty = false
- // Update the is empty and default_branch columns
- if err := models.UpdateRepositoryCols(m.Repo, "default_branch", "is_empty"); err != nil {
- log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err)
- desc := fmt.Sprintf("Failed to uupdate default branch of repository '%s': %v", m.Repo.RepoPath(), err)
- if err = models.CreateRepositoryNotice(desc); err != nil {
- log.Error("CreateRepositoryNotice: %v", err)
- }
- return false
- }
- }
- return true
}
// InitSyncMirrors initializes a go routine to sync the mirrors
func InitSyncMirrors() {
- go graceful.GetManager().RunWithShutdownContext(SyncMirrors)
+ go graceful.GetManager().RunWithShutdownContext(syncMirrors)
}
// StartToMirror adds repoID to mirror queue
func StartToMirror(repoID int64) {
- go mirrorQueue.Add(repoID)
+ go mirrorQueue.Add(fmt.Sprintf("pull %d", repoID))
+}
+
+// AddPushMirrorToQueue adds the push mirror to the queue
+func AddPushMirrorToQueue(mirrorID int64) {
+ go mirrorQueue.Add(fmt.Sprintf("push %d", mirrorID))
}
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
new file mode 100644
index 0000000000..a16724b36f
--- /dev/null
+++ b/services/mirror/mirror_pull.go
@@ -0,0 +1,452 @@
+// Copyright 2021 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 mirror
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/cache"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/notification"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// gitShortEmptySha Git short empty SHA
+const gitShortEmptySha = "0000000"
+
+// UpdateAddress writes new address to Git repository and database
+func UpdateAddress(m *models.Mirror, addr string) error {
+ remoteName := m.GetRemoteName()
+ repoPath := m.Repo.RepoPath()
+ // Remove old remote
+ _, err := git.NewCommand("remote", "rm", remoteName).RunInDir(repoPath)
+ if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+ return err
+ }
+
+ _, err = git.NewCommand("remote", "add", remoteName, "--mirror=fetch", addr).RunInDir(repoPath)
+ if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+ return err
+ }
+
+ if m.Repo.HasWiki() {
+ wikiPath := m.Repo.WikiPath()
+ wikiRemotePath := repo_module.WikiRemoteURL(addr)
+ // Remove old remote of wiki
+ _, err := git.NewCommand("remote", "rm", remoteName).RunInDir(wikiPath)
+ if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+ return err
+ }
+
+ _, err = git.NewCommand("remote", "add", remoteName, "--mirror=fetch", wikiRemotePath).RunInDir(wikiPath)
+ if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+ return err
+ }
+ }
+
+ m.Repo.OriginalURL = addr
+ return models.UpdateRepositoryCols(m.Repo, "original_url")
+}
+
+// mirrorSyncResult contains information of a updated reference.
+// If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
+// If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty.
+type mirrorSyncResult struct {
+ refName string
+ oldCommitID string
+ newCommitID string
+}
+
+// parseRemoteUpdateOutput detects create, update and delete operations of references from upstream.
+func parseRemoteUpdateOutput(output string) []*mirrorSyncResult {
+ results := make([]*mirrorSyncResult, 0, 3)
+ lines := strings.Split(output, "\n")
+ for i := range lines {
+ // Make sure reference name is presented before continue
+ idx := strings.Index(lines[i], "-> ")
+ if idx == -1 {
+ continue
+ }
+
+ refName := lines[i][idx+3:]
+
+ switch {
+ case strings.HasPrefix(lines[i], " * "): // New reference
+ if strings.HasPrefix(lines[i], " * [new tag]") {
+ refName = git.TagPrefix + refName
+ } else if strings.HasPrefix(lines[i], " * [new branch]") {
+ refName = git.BranchPrefix + refName
+ }
+ results = append(results, &mirrorSyncResult{
+ refName: refName,
+ oldCommitID: gitShortEmptySha,
+ })
+ case strings.HasPrefix(lines[i], " - "): // Delete reference
+ results = append(results, &mirrorSyncResult{
+ refName: refName,
+ newCommitID: gitShortEmptySha,
+ })
+ case strings.HasPrefix(lines[i], " + "): // Force update
+ if idx := strings.Index(refName, " "); idx > -1 {
+ refName = refName[:idx]
+ }
+ delimIdx := strings.Index(lines[i][3:], " ")
+ if delimIdx == -1 {
+ log.Error("SHA delimiter not found: %q", lines[i])
+ continue
+ }
+ shas := strings.Split(lines[i][3:delimIdx+3], "...")
+ if len(shas) != 2 {
+ log.Error("Expect two SHAs but not what found: %q", lines[i])
+ continue
+ }
+ results = append(results, &mirrorSyncResult{
+ refName: refName,
+ oldCommitID: shas[0],
+ newCommitID: shas[1],
+ })
+ case strings.HasPrefix(lines[i], " "): // New commits of a reference
+ delimIdx := strings.Index(lines[i][3:], " ")
+ if delimIdx == -1 {
+ log.Error("SHA delimiter not found: %q", lines[i])
+ continue
+ }
+ shas := strings.Split(lines[i][3:delimIdx+3], "..")
+ if len(shas) != 2 {
+ log.Error("Expect two SHAs but not what found: %q", lines[i])
+ continue
+ }
+ results = append(results, &mirrorSyncResult{
+ refName: refName,
+ oldCommitID: shas[0],
+ newCommitID: shas[1],
+ })
+
+ default:
+ log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i])
+ }
+ }
+ return results
+}
+
+// runSync returns true if sync finished without error.
+func runSync(ctx context.Context, m *models.Mirror) ([]*mirrorSyncResult, bool) {
+ repoPath := m.Repo.RepoPath()
+ wikiPath := m.Repo.WikiPath()
+ timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
+
+ log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo)
+ gitArgs := []string{"remote", "update"}
+ if m.EnablePrune {
+ gitArgs = append(gitArgs, "--prune")
+ }
+ gitArgs = append(gitArgs, m.GetRemoteName())
+
+ remoteAddr, remoteErr := git.GetRemoteAddress(repoPath, m.GetRemoteName())
+ if remoteErr != nil {
+ log.Error("GetRemoteAddress Error %v", remoteErr)
+ }
+
+ stdoutBuilder := strings.Builder{}
+ stderrBuilder := strings.Builder{}
+ if err := git.NewCommand(gitArgs...).
+ SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())).
+ RunInDirTimeoutPipeline(timeout, repoPath, &stdoutBuilder, &stderrBuilder); err != nil {
+ stdout := stdoutBuilder.String()
+ stderr := stderrBuilder.String()
+
+ // sanitize the output, since it may contain the remote address, which may
+ // contain a password
+
+ sanitizer := util.NewURLSanitizer(remoteAddr, true)
+ stderrMessage := sanitizer.Replace(stderr)
+ stdoutMessage := sanitizer.Replace(stdout)
+
+ log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
+ desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage)
+ if err = models.CreateRepositoryNotice(desc); err != nil {
+ log.Error("CreateRepositoryNotice: %v", err)
+ }
+ return nil, false
+ }
+ output := stderrBuilder.String()
+
+ gitRepo, err := git.OpenRepository(repoPath)
+ if err != nil {
+ log.Error("OpenRepository: %v", err)
+ return nil, false
+ }
+
+ log.Trace("SyncMirrors [repo: %-v]: syncing releases with tags...", m.Repo)
+ if err = repo_module.SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
+ log.Error("Failed to synchronize tags to releases for repository: %v", err)
+ }
+
+ if m.LFS && setting.LFS.StartServer {
+ log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
+ ep := lfs.DetermineEndpoint(remoteAddr.String(), m.LFSEndpoint)
+ if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, ep); err != nil {
+ log.Error("Failed to synchronize LFS objects for repository: %v", err)
+ }
+ }
+ gitRepo.Close()
+
+ log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
+ if err := m.Repo.UpdateSize(models.DefaultDBContext()); err != nil {
+ log.Error("Failed to update size for mirror repository: %v", err)
+ }
+
+ if m.Repo.HasWiki() {
+ log.Trace("SyncMirrors [repo: %-v Wiki]: running git remote update...", m.Repo)
+ stderrBuilder.Reset()
+ stdoutBuilder.Reset()
+ if err := git.NewCommand("remote", "update", "--prune", m.GetRemoteName()).
+ SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())).
+ RunInDirTimeoutPipeline(timeout, wikiPath, &stdoutBuilder, &stderrBuilder); err != nil {
+ stdout := stdoutBuilder.String()
+ stderr := stderrBuilder.String()
+
+ // sanitize the output, since it may contain the remote address, which may
+ // contain a password
+
+ remoteAddr, remoteErr := git.GetRemoteAddress(wikiPath, m.GetRemoteName())
+ if remoteErr != nil {
+ log.Error("GetRemoteAddress Error %v", remoteErr)
+ }
+
+ sanitizer := util.NewURLSanitizer(remoteAddr, true)
+ stderrMessage := sanitizer.Replace(stderr)
+ stdoutMessage := sanitizer.Replace(stdout)
+
+ log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
+ desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage)
+ if err = models.CreateRepositoryNotice(desc); err != nil {
+ log.Error("CreateRepositoryNotice: %v", err)
+ }
+ return nil, false
+ }
+ log.Trace("SyncMirrors [repo: %-v Wiki]: git remote update complete", m.Repo)
+ }
+
+ log.Trace("SyncMirrors [repo: %-v]: invalidating mirror branch caches...", m.Repo)
+ branches, _, err := repo_module.GetBranches(m.Repo, 0, 0)
+ if err != nil {
+ log.Error("GetBranches: %v", err)
+ return nil, false
+ }
+
+ for _, branch := range branches {
+ cache.Remove(m.Repo.GetCommitsCountCacheKey(branch.Name, true))
+ }
+
+ m.UpdatedUnix = timeutil.TimeStampNow()
+ return parseRemoteUpdateOutput(output), true
+}
+
+// SyncPullMirror starts the sync of the pull mirror and schedules the next run.
+func SyncPullMirror(ctx context.Context, repoID int64) bool {
+ log.Trace("SyncMirrors [repo_id: %v]", repoID)
+ defer func() {
+ err := recover()
+ if err == nil {
+ return
+ }
+ // There was a panic whilst syncMirrors...
+ log.Error("PANIC whilst syncMirrors[%d] Panic: %v\nStacktrace: %s", repoID, err, log.Stack(2))
+ }()
+
+ m, err := models.GetMirrorByRepoID(repoID)
+ if err != nil {
+ log.Error("GetMirrorByRepoID [%d]: %v", repoID, err)
+ return false
+ }
+
+ log.Trace("SyncMirrors [repo: %-v]: Running Sync", m.Repo)
+ results, ok := runSync(ctx, m)
+ if !ok {
+ return false
+ }
+
+ log.Trace("SyncMirrors [repo: %-v]: Scheduling next update", m.Repo)
+ m.ScheduleNextUpdate()
+ if err = models.UpdateMirror(m); err != nil {
+ log.Error("UpdateMirror [%d]: %v", m.RepoID, err)
+ return false
+ }
+
+ var gitRepo *git.Repository
+ if len(results) == 0 {
+ log.Trace("SyncMirrors [repo: %-v]: no branches updated", m.Repo)
+ } else {
+ log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
+ gitRepo, err = git.OpenRepository(m.Repo.RepoPath())
+ if err != nil {
+ log.Error("OpenRepository [%d]: %v", m.RepoID, err)
+ return false
+ }
+ defer gitRepo.Close()
+
+ if ok := checkAndUpdateEmptyRepository(m, gitRepo, results); !ok {
+ return false
+ }
+ }
+
+ for _, result := range results {
+ // Discard GitHub pull requests, i.e. refs/pull/*
+ if strings.HasPrefix(result.refName, "refs/pull/") {
+ continue
+ }
+
+ tp, _ := git.SplitRefName(result.refName)
+
+ // Create reference
+ if result.oldCommitID == gitShortEmptySha {
+ if tp == git.TagPrefix {
+ tp = "tag"
+ } else if tp == git.BranchPrefix {
+ tp = "branch"
+ }
+ commitID, err := gitRepo.GetRefCommitID(result.refName)
+ if err != nil {
+ log.Error("gitRepo.GetRefCommitID [repo_id: %d, ref_name: %s]: %v", m.RepoID, result.refName, err)
+ continue
+ }
+ notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{
+ RefFullName: result.refName,
+ OldCommitID: git.EmptySHA,
+ NewCommitID: commitID,
+ }, repo_module.NewPushCommits())
+ notification.NotifySyncCreateRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
+ continue
+ }
+
+ // Delete reference
+ if result.newCommitID == gitShortEmptySha {
+ notification.NotifySyncDeleteRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
+ continue
+ }
+
+ // Push commits
+ oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID)
+ if err != nil {
+ log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
+ continue
+ }
+ newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID)
+ if err != nil {
+ log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
+ continue
+ }
+ commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
+ if err != nil {
+ log.Error("CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err)
+ continue
+ }
+
+ theCommits := repo_module.ListToPushCommits(commits)
+ if len(theCommits.Commits) > setting.UI.FeedMaxCommitNum {
+ theCommits.Commits = theCommits.Commits[:setting.UI.FeedMaxCommitNum]
+ }
+
+ theCommits.CompareURL = m.Repo.ComposeCompareURL(oldCommitID, newCommitID)
+
+ notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{
+ RefFullName: result.refName,
+ OldCommitID: oldCommitID,
+ NewCommitID: newCommitID,
+ }, theCommits)
+ }
+ log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo)
+
+ // Get latest commit date and update to current repository updated time
+ commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath())
+ if err != nil {
+ log.Error("GetLatestCommitDate [%d]: %v", m.RepoID, err)
+ return false
+ }
+
+ if err = models.UpdateRepositoryUpdatedTime(m.RepoID, commitDate); err != nil {
+ log.Error("Update repository 'updated_unix' [%d]: %v", m.RepoID, err)
+ return false
+ }
+
+ log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo)
+
+ return true
+}
+
+func checkAndUpdateEmptyRepository(m *models.Mirror, gitRepo *git.Repository, results []*mirrorSyncResult) bool {
+ if !m.Repo.IsEmpty {
+ return true
+ }
+
+ hasDefault := false
+ hasMaster := false
+ hasMain := false
+ defaultBranchName := m.Repo.DefaultBranch
+ if len(defaultBranchName) == 0 {
+ defaultBranchName = setting.Repository.DefaultBranch
+ }
+ firstName := ""
+ for _, result := range results {
+ if strings.HasPrefix(result.refName, "refs/pull/") {
+ continue
+ }
+ tp, name := git.SplitRefName(result.refName)
+ if len(tp) > 0 && tp != git.BranchPrefix {
+ continue
+ }
+ if len(firstName) == 0 {
+ firstName = name
+ }
+
+ hasDefault = hasDefault || name == defaultBranchName
+ hasMaster = hasMaster || name == "master"
+ hasMain = hasMain || name == "main"
+ }
+
+ if len(firstName) > 0 {
+ if hasDefault {
+ m.Repo.DefaultBranch = defaultBranchName
+ } else if hasMaster {
+ m.Repo.DefaultBranch = "master"
+ } else if hasMain {
+ m.Repo.DefaultBranch = "main"
+ } else {
+ m.Repo.DefaultBranch = firstName
+ }
+ // Update the git repository default branch
+ if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil {
+ if !git.IsErrUnsupportedVersion(err) {
+ log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err)
+ desc := fmt.Sprintf("Failed to uupdate default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err)
+ if err = models.CreateRepositoryNotice(desc); err != nil {
+ log.Error("CreateRepositoryNotice: %v", err)
+ }
+ return false
+ }
+ }
+ m.Repo.IsEmpty = false
+ // Update the is empty and default_branch columns
+ if err := models.UpdateRepositoryCols(m.Repo, "default_branch", "is_empty"); err != nil {
+ log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err)
+ desc := fmt.Sprintf("Failed to uupdate default branch of repository '%s': %v", m.Repo.RepoPath(), err)
+ if err = models.CreateRepositoryNotice(desc); err != nil {
+ log.Error("CreateRepositoryNotice: %v", err)
+ }
+ return false
+ }
+ }
+ return true
+}
diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go
new file mode 100644
index 0000000000..de81303689
--- /dev/null
+++ b/services/mirror/mirror_push.go
@@ -0,0 +1,242 @@
+// Copyright 2021 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 mirror
+
+import (
+ "context"
+ "errors"
+ "io"
+ "net/url"
+ "regexp"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+)
+
+var stripExitStatus = regexp.MustCompile(`exit status \d+ - `)
+
+// AddPushMirrorRemote registers the push mirror remote.
+func AddPushMirrorRemote(m *models.PushMirror, addr string) error {
+ addRemoteAndConfig := func(addr, path string) error {
+ if _, err := git.NewCommand("remote", "add", "--mirror=push", m.RemoteName, addr).RunInDir(path); err != nil {
+ return err
+ }
+ if _, err := git.NewCommand("config", "--add", "remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunInDir(path); err != nil {
+ return err
+ }
+ if _, err := git.NewCommand("config", "--add", "remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunInDir(path); err != nil {
+ return err
+ }
+ return nil
+ }
+
+ if err := addRemoteAndConfig(addr, m.Repo.RepoPath()); err != nil {
+ return err
+ }
+
+ if m.Repo.HasWiki() {
+ wikiRemoteURL := repository.WikiRemoteURL(addr)
+ if len(wikiRemoteURL) > 0 {
+ if err := addRemoteAndConfig(wikiRemoteURL, m.Repo.WikiPath()); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// RemovePushMirrorRemote removes the push mirror remote.
+func RemovePushMirrorRemote(m *models.PushMirror) error {
+ cmd := git.NewCommand("remote", "rm", m.RemoteName)
+
+ if _, err := cmd.RunInDir(m.Repo.RepoPath()); err != nil {
+ return err
+ }
+
+ if m.Repo.HasWiki() {
+ if _, err := cmd.RunInDir(m.Repo.WikiPath()); err != nil {
+ // The wiki remote may not exist
+ log.Warn("Wiki Remote[%d] could not be removed: %v", m.ID, err)
+ }
+ }
+
+ return nil
+}
+
+// SyncPushMirror starts the sync of the push mirror and schedules the next run.
+func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
+ log.Trace("SyncPushMirror [mirror: %d]", mirrorID)
+ defer func() {
+ err := recover()
+ if err == nil {
+ return
+ }
+ // There was a panic whilst syncPushMirror...
+ log.Error("PANIC whilst syncPushMirror[%d] Panic: %v\nStacktrace: %s", mirrorID, err, log.Stack(2))
+ }()
+
+ m, err := models.GetPushMirrorByID(mirrorID)
+ if err != nil {
+ log.Error("GetPushMirrorByID [%d]: %v", mirrorID, err)
+ return false
+ }
+
+ m.LastError = ""
+
+ log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Running Sync", m.ID, m.Repo)
+ err = runPushSync(ctx, m)
+ if err != nil {
+ log.Error("SyncPushMirror [mirror: %d][repo: %-v]: %v", m.ID, m.Repo, err)
+ m.LastError = stripExitStatus.ReplaceAllLiteralString(err.Error(), "")
+ }
+
+ m.LastUpdateUnix = timeutil.TimeStampNow()
+
+ if err := models.UpdatePushMirror(m); err != nil {
+ log.Error("UpdatePushMirror [%d]: %v", m.ID, err)
+
+ return false
+ }
+
+ log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Finished", m.ID, m.Repo)
+
+ return err == nil
+}
+
+func runPushSync(ctx context.Context, m *models.PushMirror) error {
+ timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
+
+ performPush := func(path string) error {
+ remoteAddr, err := git.GetRemoteAddress(path, m.RemoteName)
+ if err != nil {
+ log.Error("GetRemoteAddress(%s) Error %v", path, err)
+ return errors.New("Unexpected error")
+ }
+
+ if setting.LFS.StartServer {
+ log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
+
+ gitRepo, err := git.OpenRepository(path)
+ if err != nil {
+ log.Error("OpenRepository: %v", err)
+ return errors.New("Unexpected error")
+ }
+ defer gitRepo.Close()
+
+ ep := lfs.DetermineEndpoint(remoteAddr.String(), "")
+ if err := pushAllLFSObjects(ctx, gitRepo, ep); err != nil {
+ return util.NewURLSanitizedError(err, remoteAddr, true)
+ }
+ }
+
+ log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName)
+
+ if err := git.Push(path, git.PushOptions{
+ Remote: m.RemoteName,
+ Force: true,
+ Mirror: true,
+ Timeout: timeout,
+ }); err != nil {
+ log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err)
+
+ return util.NewURLSanitizedError(err, remoteAddr, true)
+ }
+
+ return nil
+ }
+
+ err := performPush(m.Repo.RepoPath())
+ if err != nil {
+ return err
+ }
+
+ if m.Repo.HasWiki() {
+ wikiPath := m.Repo.WikiPath()
+ _, err := git.GetRemoteAddress(wikiPath, m.RemoteName)
+ if err == nil {
+ err := performPush(wikiPath)
+ if err != nil {
+ return err
+ }
+ } else {
+ log.Trace("Skipping wiki: No remote configured")
+ }
+ }
+
+ return nil
+}
+
+func pushAllLFSObjects(ctx context.Context, gitRepo *git.Repository, endpoint *url.URL) error {
+ client := lfs.NewClient(endpoint)
+ contentStore := lfs.NewContentStore()
+
+ pointerChan := make(chan lfs.PointerBlob)
+ errChan := make(chan error, 1)
+ go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan)
+
+ uploadObjects := func(pointers []lfs.Pointer) error {
+ err := client.Upload(ctx, pointers, func(p lfs.Pointer, objectError error) (io.ReadCloser, error) {
+ if objectError != nil {
+ return nil, objectError
+ }
+
+ content, err := contentStore.Get(p)
+ if err != nil {
+ log.Error("Error reading LFS object %v: %v", p, err)
+ }
+ return content, err
+ })
+ if err != nil {
+ select {
+ case <-ctx.Done():
+ return nil
+ default:
+ }
+ }
+ return err
+ }
+
+ var batch []lfs.Pointer
+ for pointerBlob := range pointerChan {
+ exists, err := contentStore.Exists(pointerBlob.Pointer)
+ if err != nil {
+ log.Error("Error checking if LFS object %v exists: %v", pointerBlob.Pointer, err)
+ return err
+ }
+ if !exists {
+ log.Trace("Skipping missing LFS object %v", pointerBlob.Pointer)
+ continue
+ }
+
+ batch = append(batch, pointerBlob.Pointer)
+ if len(batch) >= client.BatchSize() {
+ if err := uploadObjects(batch); err != nil {
+ return err
+ }
+ batch = nil
+ }
+ }
+ if len(batch) > 0 {
+ if err := uploadObjects(batch); err != nil {
+ return err
+ }
+ }
+
+ err, has := <-errChan
+ if has {
+ log.Error("Error enumerating LFS objects for repository: %v", err)
+ return err
+ }
+
+ return nil
+}
diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go
deleted file mode 100644
index 20492c784b..0000000000
--- a/services/mirror/mirror_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// 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 mirror
-
-import (
- "context"
- "path/filepath"
- "testing"
-
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/modules/git"
- migration "code.gitea.io/gitea/modules/migrations/base"
- "code.gitea.io/gitea/modules/repository"
- release_service "code.gitea.io/gitea/services/release"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestMain(m *testing.M) {
- models.MainTest(m, filepath.Join("..", ".."))
-}
-
-func TestRelease_MirrorDelete(t *testing.T) {
- assert.NoError(t, models.PrepareTestDatabase())
-
- user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
- repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
- repoPath := models.RepoPath(user.Name, repo.Name)
-
- opts := migration.MigrateOptions{
- RepoName: "test_mirror",
- Description: "Test mirror",
- Private: false,
- Mirror: true,
- CloneAddr: repoPath,
- Wiki: true,
- Releases: false,
- }
-
- mirrorRepo, err := repository.CreateRepository(user, user, models.CreateRepoOptions{
- Name: opts.RepoName,
- Description: opts.Description,
- IsPrivate: opts.Private,
- IsMirror: opts.Mirror,
- Status: models.RepositoryBeingMigrated,
- })
- assert.NoError(t, err)
-
- ctx := context.Background()
-
- mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts)
- assert.NoError(t, err)
-
- gitRepo, err := git.OpenRepository(repoPath)
- assert.NoError(t, err)
- defer gitRepo.Close()
-
- findOptions := models.FindReleasesOptions{IncludeDrafts: true, IncludeTags: true}
- initCount, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions)
- assert.NoError(t, err)
-
- assert.NoError(t, release_service.CreateRelease(gitRepo, &models.Release{
- RepoID: repo.ID,
- PublisherID: user.ID,
- TagName: "v0.2",
- Target: "master",
- Title: "v0.2 is released",
- Note: "v0.2 is released",
- IsDraft: false,
- IsPrerelease: false,
- IsTag: true,
- }, nil, ""))
-
- err = mirror.GetMirror()
- assert.NoError(t, err)
-
- _, ok := runSync(ctx, mirror.Mirror)
- assert.True(t, ok)
-
- count, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions)
- assert.NoError(t, err)
- assert.EqualValues(t, initCount+1, count)
-
- release, err := models.GetRelease(repo.ID, "v0.2")
- assert.NoError(t, err)
- assert.NoError(t, release_service.DeleteReleaseByID(release.ID, user, true))
-
- _, ok = runSync(ctx, mirror.Mirror)
- assert.True(t, ok)
-
- count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions)
- assert.NoError(t, err)
- assert.EqualValues(t, initCount, count)
-}