mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
2978 files. The fork is 2026-07-15 (rebrand: gitea.dev -> github.com/hanzoai/git), so 2026 is the year, and "Copyright 2026 Hanzo AI, Inc." was already the form used by the handful of files that had ours. Shape is now: // Copyright 2026 Hanzo AI, Inc. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT Ours on top, theirs retained verbatim. Retained because MIT requires it, and separately because they are true — those authors did write that code. Not renamed to "The Git Authors": Git is a different project under a different license, and putting its name here would attribute this code to people who have nothing to do with it. Adding our line is the fix; overwriting theirs would have been both a licence violation and a false statement. gofmt still reports import-order drift in some of these files. It predates this change (verified against a stash) and is left alone rather than burying a one-line-per-file legal change under 3000 files of unrelated reordering. Build clean. Co-authored-by: Hanzo Dev <[email protected]>
287 lines
8.6 KiB
Go
287 lines
8.6 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mirror
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/modules/git"
|
|
"github.com/hanzoai/git/modules/gitrepo"
|
|
"github.com/hanzoai/git/modules/lfs"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/process"
|
|
"github.com/hanzoai/git/modules/proxy"
|
|
"github.com/hanzoai/git/modules/repository"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/timeutil"
|
|
"github.com/hanzoai/git/modules/util"
|
|
"github.com/hanzoai/git/services/migrations"
|
|
repo_service "github.com/hanzoai/git/services/repository"
|
|
)
|
|
|
|
var stripExitStatus = regexp.MustCompile(`exit status \d+ - `)
|
|
|
|
// AddPushMirrorRemote registers the push mirror remote.
|
|
func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error {
|
|
addRemoteAndConfig := func(storageRepo gitrepo.Repository, addr string) error {
|
|
// RemoteOptionMirrorPush persists remote.<name>.mirror=true, which already
|
|
// means "push every ref". Adding explicit push refspecs on top is not just
|
|
// redundant — git refuses the combination outright ("--mirror can't be
|
|
// combined with refspecs"), so a mirror created this way could never sync
|
|
// and reported the failure only in the push-mirror record. Set the remote
|
|
// and stop; anything needing specific refspecs passes them per-push, which
|
|
// already overrides mirror mode for that invocation (see git.Push).
|
|
return gitrepo.GitRemoteAdd(ctx, storageRepo, m.RemoteName, addr, gitrepo.RemoteOptionMirrorPush)
|
|
}
|
|
|
|
if err := addRemoteAndConfig(m.Repo, addr); err != nil {
|
|
return err
|
|
}
|
|
|
|
if repo_service.HasWiki(ctx, m.Repo) {
|
|
wikiRemoteURL := repository.WikiRemoteURL(ctx, addr)
|
|
if len(wikiRemoteURL) > 0 {
|
|
if err := addRemoteAndConfig(m.Repo.WikiStorageRepo(), wikiRemoteURL); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemovePushMirrorRemote removes the push mirror remote.
|
|
func RemovePushMirrorRemote(ctx context.Context, m *repo_model.PushMirror) error {
|
|
_ = m.GetRepository(ctx)
|
|
if err := gitrepo.GitRemoteRemove(ctx, m.Repo, m.RemoteName); err != nil {
|
|
return err
|
|
}
|
|
|
|
if repo_service.HasWiki(ctx, m.Repo) {
|
|
if err := gitrepo.GitRemoteRemove(ctx, m.Repo.WikiStorageRepo(), m.RemoteName); 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))
|
|
}()
|
|
|
|
// TODO: Handle "!exist" better
|
|
m, exist, err := db.GetByID[repo_model.PushMirror](ctx, mirrorID)
|
|
if err != nil || !exist {
|
|
log.Error("GetPushMirrorByID [%d]: %v", mirrorID, err)
|
|
return false
|
|
}
|
|
|
|
_ = m.GetRepository(ctx)
|
|
|
|
m.LastError = ""
|
|
|
|
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing PushMirror %s/%s to %s", m.Repo.OwnerName, m.Repo.Name, m.RemoteName))
|
|
defer finished()
|
|
|
|
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 := repo_model.UpdatePushMirror(ctx, 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 *repo_model.PushMirror) error {
|
|
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
|
|
|
|
performPush := func(repo *repo_model.Repository, isWiki bool) error {
|
|
var storageRepo gitrepo.Repository = repo
|
|
if isWiki {
|
|
storageRepo = repo.WikiStorageRepo()
|
|
}
|
|
remoteURL, err := gitrepo.GitRemoteGetURL(ctx, storageRepo, m.RemoteName)
|
|
if err != nil {
|
|
log.Error("GetRemoteURL(%s) Error %v", storageRepo.RelativePath(), err)
|
|
return errors.New("Unexpected error")
|
|
}
|
|
|
|
if setting.LFS.StartServer {
|
|
log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
|
|
|
|
gitRepo, err := gitrepo.OpenRepository(ctx, storageRepo)
|
|
if err != nil {
|
|
log.Error("OpenRepository: %v", err)
|
|
return errors.New("Unexpected error")
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
lfsClient, err := lfs.NewClientFromEndpoint(remoteURL.String(), "", migrations.NewMigrationHTTPTransport())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil {
|
|
return util.SanitizeErrorCredentialURLs(err)
|
|
}
|
|
}
|
|
|
|
log.Trace("Pushing %s mirror[%d] remote %s", storageRepo.RelativePath(), m.ID, m.RemoteName)
|
|
|
|
envs := proxy.EnvWithProxy(remoteURL.URL)
|
|
// Carry branches and tags outward, additively. This deliberately does NOT
|
|
// use Force or Mirror: together those made the push authoritative over the
|
|
// remote, free to rewrite a diverged ref and to DELETE any ref the remote
|
|
// had and we did not. A mirror is a copy, so it must never be able to
|
|
// remove history from the thing it copies — and one already did: a push
|
|
// mirror of an IAM repo pruned the entire v1.0 through v1.31 tag range off
|
|
// the remote, a contiguous block belonging to the production line, and the
|
|
// objects were then garbage-collected there.
|
|
//
|
|
// A ref the remote already has resolves to a no-op, so nothing echoes back
|
|
// on repos we pull FROM. A ref that only exists here is added. A ref that
|
|
// diverged is REJECTED and reported rather than overwritten, which is the
|
|
// correct failure: divergence is a question for a person, not something a
|
|
// sync job should silently settle.
|
|
if err := gitrepo.PushToExternal(ctx, storageRepo, git.PushOptions{
|
|
Remote: m.RemoteName,
|
|
Refspecs: []string{
|
|
"refs/heads/*:refs/heads/*",
|
|
"refs/tags/*:refs/tags/*",
|
|
},
|
|
Timeout: timeout,
|
|
Env: envs,
|
|
}); err != nil {
|
|
log.Error("Error pushing %s mirror[%d] remote %s: %v", storageRepo.RelativePath(), m.ID, m.RemoteName, err)
|
|
|
|
return util.SanitizeErrorCredentialURLs(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
err := performPush(m.Repo, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if repo_service.HasWiki(ctx, m.Repo) {
|
|
if _, err := gitrepo.GitRemoteGetURL(ctx, m.Repo.WikiStorageRepo(), m.RemoteName); err == nil {
|
|
err := performPush(m.Repo, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if !errors.Is(err, util.ErrNotExist) {
|
|
log.Error("GetRemote of wiki failed: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func pushAllLFSObjects(ctx context.Context, gitRepo *git.Repository, lfsClient lfs.Client) error {
|
|
contentStore := lfs.NewContentStore()
|
|
|
|
pointerChan := make(chan lfs.PointerBlob)
|
|
errChan := make(chan error, 1)
|
|
go func() {
|
|
errChan <- lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan)
|
|
}()
|
|
|
|
uploadObjects := func(pointers []lfs.Pointer) error {
|
|
err := lfsClient.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) >= lfsClient.BatchSize() {
|
|
if err := uploadObjects(batch); err != nil {
|
|
return err
|
|
}
|
|
batch = nil
|
|
}
|
|
}
|
|
if len(batch) > 0 {
|
|
if err := uploadObjects(batch); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := <-errChan
|
|
if err != nil {
|
|
log.Error("Error enumerating LFS objects for repository: %v", err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func syncPushMirrorWithSyncOnCommit(ctx context.Context, repoID int64) {
|
|
pushMirrors, err := repo_model.GetPushMirrorsSyncedOnCommit(ctx, repoID)
|
|
if err != nil {
|
|
log.Error("repo_model.GetPushMirrorsSyncedOnCommit failed: %v", err)
|
|
return
|
|
}
|
|
|
|
for _, mirror := range pushMirrors {
|
|
AddPushMirrorToQueue(mirror.ID)
|
|
}
|
|
}
|