code.gitea.io/gitea@v1.22.3/services/mirror/mirror_push.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package mirror
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"regexp"
    12  	"strings"
    13  	"time"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	repo_model "code.gitea.io/gitea/models/repo"
    17  	"code.gitea.io/gitea/modules/git"
    18  	"code.gitea.io/gitea/modules/gitrepo"
    19  	"code.gitea.io/gitea/modules/lfs"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/process"
    22  	"code.gitea.io/gitea/modules/repository"
    23  	"code.gitea.io/gitea/modules/setting"
    24  	"code.gitea.io/gitea/modules/timeutil"
    25  	"code.gitea.io/gitea/modules/util"
    26  )
    27  
    28  var stripExitStatus = regexp.MustCompile(`exit status \d+ - `)
    29  
    30  // AddPushMirrorRemote registers the push mirror remote.
    31  func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error {
    32  	addRemoteAndConfig := func(addr, path string) error {
    33  		cmd := git.NewCommand(ctx, "remote", "add", "--mirror=push").AddDynamicArguments(m.RemoteName, addr)
    34  		if strings.Contains(addr, "://") && strings.Contains(addr, "@") {
    35  			cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path))
    36  		} else {
    37  			cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, addr, path))
    38  		}
    39  		if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil {
    40  			return err
    41  		}
    42  		if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
    43  			return err
    44  		}
    45  		if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
    46  			return err
    47  		}
    48  		return nil
    49  	}
    50  
    51  	if err := addRemoteAndConfig(addr, m.Repo.RepoPath()); err != nil {
    52  		return err
    53  	}
    54  
    55  	if m.Repo.HasWiki() {
    56  		wikiRemoteURL := repository.WikiRemoteURL(ctx, addr)
    57  		if len(wikiRemoteURL) > 0 {
    58  			if err := addRemoteAndConfig(wikiRemoteURL, m.Repo.WikiPath()); err != nil {
    59  				return err
    60  			}
    61  		}
    62  	}
    63  
    64  	return nil
    65  }
    66  
    67  // RemovePushMirrorRemote removes the push mirror remote.
    68  func RemovePushMirrorRemote(ctx context.Context, m *repo_model.PushMirror) error {
    69  	cmd := git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(m.RemoteName)
    70  	_ = m.GetRepository(ctx)
    71  
    72  	if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: m.Repo.RepoPath()}); err != nil {
    73  		return err
    74  	}
    75  
    76  	if m.Repo.HasWiki() {
    77  		if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: m.Repo.WikiPath()}); err != nil {
    78  			// The wiki remote may not exist
    79  			log.Warn("Wiki Remote[%d] could not be removed: %v", m.ID, err)
    80  		}
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  // SyncPushMirror starts the sync of the push mirror and schedules the next run.
    87  func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
    88  	log.Trace("SyncPushMirror [mirror: %d]", mirrorID)
    89  	defer func() {
    90  		err := recover()
    91  		if err == nil {
    92  			return
    93  		}
    94  		// There was a panic whilst syncPushMirror...
    95  		log.Error("PANIC whilst syncPushMirror[%d] Panic: %v\nStacktrace: %s", mirrorID, err, log.Stack(2))
    96  	}()
    97  
    98  	// TODO: Handle "!exist" better
    99  	m, exist, err := db.GetByID[repo_model.PushMirror](ctx, mirrorID)
   100  	if err != nil || !exist {
   101  		log.Error("GetPushMirrorByID [%d]: %v", mirrorID, err)
   102  		return false
   103  	}
   104  
   105  	_ = m.GetRepository(ctx)
   106  
   107  	m.LastError = ""
   108  
   109  	ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing PushMirror %s/%s to %s", m.Repo.OwnerName, m.Repo.Name, m.RemoteName))
   110  	defer finished()
   111  
   112  	log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Running Sync", m.ID, m.Repo)
   113  	err = runPushSync(ctx, m)
   114  	if err != nil {
   115  		log.Error("SyncPushMirror [mirror: %d][repo: %-v]: %v", m.ID, m.Repo, err)
   116  		m.LastError = stripExitStatus.ReplaceAllLiteralString(err.Error(), "")
   117  	}
   118  
   119  	m.LastUpdateUnix = timeutil.TimeStampNow()
   120  
   121  	if err := repo_model.UpdatePushMirror(ctx, m); err != nil {
   122  		log.Error("UpdatePushMirror [%d]: %v", m.ID, err)
   123  
   124  		return false
   125  	}
   126  
   127  	log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Finished", m.ID, m.Repo)
   128  
   129  	return err == nil
   130  }
   131  
   132  func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
   133  	timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
   134  
   135  	performPush := func(repo *repo_model.Repository, isWiki bool) error {
   136  		path := repo.RepoPath()
   137  		if isWiki {
   138  			path = repo.WikiPath()
   139  		}
   140  		remoteURL, err := git.GetRemoteURL(ctx, path, m.RemoteName)
   141  		if err != nil {
   142  			log.Error("GetRemoteAddress(%s) Error %v", path, err)
   143  			return errors.New("Unexpected error")
   144  		}
   145  
   146  		if setting.LFS.StartServer {
   147  			log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
   148  
   149  			var gitRepo *git.Repository
   150  			if isWiki {
   151  				gitRepo, err = gitrepo.OpenWikiRepository(ctx, repo)
   152  			} else {
   153  				gitRepo, err = gitrepo.OpenRepository(ctx, repo)
   154  			}
   155  			if err != nil {
   156  				log.Error("OpenRepository: %v", err)
   157  				return errors.New("Unexpected error")
   158  			}
   159  			defer gitRepo.Close()
   160  
   161  			endpoint := lfs.DetermineEndpoint(remoteURL.String(), "")
   162  			lfsClient := lfs.NewClient(endpoint, nil)
   163  			if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil {
   164  				return util.SanitizeErrorCredentialURLs(err)
   165  			}
   166  		}
   167  
   168  		log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName)
   169  
   170  		if err := git.Push(ctx, path, git.PushOptions{
   171  			Remote:  m.RemoteName,
   172  			Force:   true,
   173  			Mirror:  true,
   174  			Timeout: timeout,
   175  		}); err != nil {
   176  			log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err)
   177  
   178  			return util.SanitizeErrorCredentialURLs(err)
   179  		}
   180  
   181  		return nil
   182  	}
   183  
   184  	err := performPush(m.Repo, false)
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	if m.Repo.HasWiki() {
   190  		_, err := git.GetRemoteAddress(ctx, m.Repo.WikiPath(), m.RemoteName)
   191  		if err == nil {
   192  			err := performPush(m.Repo, true)
   193  			if err != nil {
   194  				return err
   195  			}
   196  		} else {
   197  			log.Trace("Skipping wiki: No remote configured")
   198  		}
   199  	}
   200  
   201  	return nil
   202  }
   203  
   204  func pushAllLFSObjects(ctx context.Context, gitRepo *git.Repository, lfsClient lfs.Client) error {
   205  	contentStore := lfs.NewContentStore()
   206  
   207  	pointerChan := make(chan lfs.PointerBlob)
   208  	errChan := make(chan error, 1)
   209  	go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan)
   210  
   211  	uploadObjects := func(pointers []lfs.Pointer) error {
   212  		err := lfsClient.Upload(ctx, pointers, func(p lfs.Pointer, objectError error) (io.ReadCloser, error) {
   213  			if objectError != nil {
   214  				return nil, objectError
   215  			}
   216  
   217  			content, err := contentStore.Get(p)
   218  			if err != nil {
   219  				log.Error("Error reading LFS object %v: %v", p, err)
   220  			}
   221  			return content, err
   222  		})
   223  		if err != nil {
   224  			select {
   225  			case <-ctx.Done():
   226  				return nil
   227  			default:
   228  			}
   229  		}
   230  		return err
   231  	}
   232  
   233  	var batch []lfs.Pointer
   234  	for pointerBlob := range pointerChan {
   235  		exists, err := contentStore.Exists(pointerBlob.Pointer)
   236  		if err != nil {
   237  			log.Error("Error checking if LFS object %v exists: %v", pointerBlob.Pointer, err)
   238  			return err
   239  		}
   240  		if !exists {
   241  			log.Trace("Skipping missing LFS object %v", pointerBlob.Pointer)
   242  			continue
   243  		}
   244  
   245  		batch = append(batch, pointerBlob.Pointer)
   246  		if len(batch) >= lfsClient.BatchSize() {
   247  			if err := uploadObjects(batch); err != nil {
   248  				return err
   249  			}
   250  			batch = nil
   251  		}
   252  	}
   253  	if len(batch) > 0 {
   254  		if err := uploadObjects(batch); err != nil {
   255  			return err
   256  		}
   257  	}
   258  
   259  	err, has := <-errChan
   260  	if has {
   261  		log.Error("Error enumerating LFS objects for repository: %v", err)
   262  		return err
   263  	}
   264  
   265  	return nil
   266  }
   267  
   268  func syncPushMirrorWithSyncOnCommit(ctx context.Context, repoID int64) {
   269  	pushMirrors, err := repo_model.GetPushMirrorsSyncedOnCommit(ctx, repoID)
   270  	if err != nil {
   271  		log.Error("repo_model.GetPushMirrorsSyncedOnCommit failed: %v", err)
   272  		return
   273  	}
   274  
   275  	for _, mirror := range pushMirrors {
   276  		AddPushMirrorToQueue(mirror.ID)
   277  	}
   278  }