code.gitea.io/gitea@v1.19.3/modules/repository/repo.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repository
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"path"
    13  	"strings"
    14  	"time"
    15  
    16  	"code.gitea.io/gitea/models/db"
    17  	git_model "code.gitea.io/gitea/models/git"
    18  	"code.gitea.io/gitea/models/organization"
    19  	repo_model "code.gitea.io/gitea/models/repo"
    20  	user_model "code.gitea.io/gitea/models/user"
    21  	"code.gitea.io/gitea/modules/container"
    22  	"code.gitea.io/gitea/modules/git"
    23  	"code.gitea.io/gitea/modules/lfs"
    24  	"code.gitea.io/gitea/modules/log"
    25  	"code.gitea.io/gitea/modules/migration"
    26  	"code.gitea.io/gitea/modules/setting"
    27  	"code.gitea.io/gitea/modules/timeutil"
    28  	"code.gitea.io/gitea/modules/util"
    29  
    30  	"gopkg.in/ini.v1"
    31  )
    32  
    33  /*
    34  GitHub, GitLab, Gogs: *.wiki.git
    35  BitBucket: *.git/wiki
    36  */
    37  var commonWikiURLSuffixes = []string{".wiki.git", ".git/wiki"}
    38  
    39  // WikiRemoteURL returns accessible repository URL for wiki if exists.
    40  // Otherwise, it returns an empty string.
    41  func WikiRemoteURL(ctx context.Context, remote string) string {
    42  	remote = strings.TrimSuffix(remote, ".git")
    43  	for _, suffix := range commonWikiURLSuffixes {
    44  		wikiURL := remote + suffix
    45  		if git.IsRepoURLAccessible(ctx, wikiURL) {
    46  			return wikiURL
    47  		}
    48  	}
    49  	return ""
    50  }
    51  
    52  // MigrateRepositoryGitData starts migrating git related data after created migrating repository
    53  func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
    54  	repo *repo_model.Repository, opts migration.MigrateOptions,
    55  	httpTransport *http.Transport,
    56  ) (*repo_model.Repository, error) {
    57  	repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
    58  
    59  	if u.IsOrganization() {
    60  		t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  		repo.NumWatches = t.NumMembers
    65  	} else {
    66  		repo.NumWatches = 1
    67  	}
    68  
    69  	migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
    70  
    71  	var err error
    72  	if err = util.RemoveAll(repoPath); err != nil {
    73  		return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err)
    74  	}
    75  
    76  	if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
    77  		Mirror:        true,
    78  		Quiet:         true,
    79  		Timeout:       migrateTimeout,
    80  		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
    81  	}); err != nil {
    82  		if errors.Is(err, context.DeadlineExceeded) {
    83  			return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err)
    84  		}
    85  		return repo, fmt.Errorf("Clone: %w", err)
    86  	}
    87  
    88  	if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
    89  		return repo, err
    90  	}
    91  
    92  	if opts.Wiki {
    93  		wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
    94  		wikiRemotePath := WikiRemoteURL(ctx, opts.CloneAddr)
    95  		if len(wikiRemotePath) > 0 {
    96  			if err := util.RemoveAll(wikiPath); err != nil {
    97  				return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
    98  			}
    99  
   100  			if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
   101  				Mirror:        true,
   102  				Quiet:         true,
   103  				Timeout:       migrateTimeout,
   104  				Branch:        "master",
   105  				SkipTLSVerify: setting.Migrations.SkipTLSVerify,
   106  			}); err != nil {
   107  				log.Warn("Clone wiki: %v", err)
   108  				if err := util.RemoveAll(wikiPath); err != nil {
   109  					return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
   110  				}
   111  			} else {
   112  				if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
   113  					return repo, err
   114  				}
   115  			}
   116  		}
   117  	}
   118  
   119  	if repo.OwnerID == u.ID {
   120  		repo.Owner = u
   121  	}
   122  
   123  	if err = CheckDaemonExportOK(ctx, repo); err != nil {
   124  		return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
   125  	}
   126  
   127  	if stdout, _, err := git.NewCommand(ctx, "update-server-info").
   128  		SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)).
   129  		RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
   130  		log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
   131  		return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err)
   132  	}
   133  
   134  	gitRepo, err := git.OpenRepository(ctx, repoPath)
   135  	if err != nil {
   136  		return repo, fmt.Errorf("OpenRepository: %w", err)
   137  	}
   138  	defer gitRepo.Close()
   139  
   140  	repo.IsEmpty, err = gitRepo.IsEmpty()
   141  	if err != nil {
   142  		return repo, fmt.Errorf("git.IsEmpty: %w", err)
   143  	}
   144  
   145  	if !repo.IsEmpty {
   146  		if len(repo.DefaultBranch) == 0 {
   147  			// Try to get HEAD branch and set it as default branch.
   148  			headBranch, err := gitRepo.GetHEADBranch()
   149  			if err != nil {
   150  				return repo, fmt.Errorf("GetHEADBranch: %w", err)
   151  			}
   152  			if headBranch != nil {
   153  				repo.DefaultBranch = headBranch.Name
   154  			}
   155  		}
   156  
   157  		if !opts.Releases {
   158  			// note: this will greatly improve release (tag) sync
   159  			// for pull-mirrors with many tags
   160  			repo.IsMirror = opts.Mirror
   161  			if err = SyncReleasesWithTags(repo, gitRepo); err != nil {
   162  				log.Error("Failed to synchronize tags to releases for repository: %v", err)
   163  			}
   164  		}
   165  
   166  		if opts.LFS {
   167  			endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint)
   168  			lfsClient := lfs.NewClient(endpoint, httpTransport)
   169  			if err = StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil {
   170  				log.Error("Failed to store missing LFS objects for repository: %v", err)
   171  			}
   172  		}
   173  	}
   174  
   175  	ctx, committer, err := db.TxContext(db.DefaultContext)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	defer committer.Close()
   180  
   181  	if opts.Mirror {
   182  		mirrorModel := repo_model.Mirror{
   183  			RepoID:         repo.ID,
   184  			Interval:       setting.Mirror.DefaultInterval,
   185  			EnablePrune:    true,
   186  			NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval),
   187  			LFS:            opts.LFS,
   188  		}
   189  		if opts.LFS {
   190  			mirrorModel.LFSEndpoint = opts.LFSEndpoint
   191  		}
   192  
   193  		if opts.MirrorInterval != "" {
   194  			parsedInterval, err := time.ParseDuration(opts.MirrorInterval)
   195  			if err != nil {
   196  				log.Error("Failed to set Interval: %v", err)
   197  				return repo, err
   198  			}
   199  			if parsedInterval == 0 {
   200  				mirrorModel.Interval = 0
   201  				mirrorModel.NextUpdateUnix = 0
   202  			} else if parsedInterval < setting.Mirror.MinInterval {
   203  				err := fmt.Errorf("Interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval)
   204  				log.Error("Interval: %s is too frequent", opts.MirrorInterval)
   205  				return repo, err
   206  			} else {
   207  				mirrorModel.Interval = parsedInterval
   208  				mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval)
   209  			}
   210  		}
   211  
   212  		if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil {
   213  			return repo, fmt.Errorf("InsertOne: %w", err)
   214  		}
   215  
   216  		repo.IsMirror = true
   217  		if err = UpdateRepository(ctx, repo, false); err != nil {
   218  			return nil, err
   219  		}
   220  	} else {
   221  		if err = UpdateRepoSize(ctx, repo); err != nil {
   222  			log.Error("Failed to update size for repository: %v", err)
   223  		}
   224  		if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil {
   225  			return nil, err
   226  		}
   227  	}
   228  
   229  	return repo, committer.Commit()
   230  }
   231  
   232  // cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
   233  // This also removes possible user credentials.
   234  func cleanUpMigrateGitConfig(configPath string) error {
   235  	cfg, err := ini.Load(configPath)
   236  	if err != nil {
   237  		return fmt.Errorf("open config file: %w", err)
   238  	}
   239  	cfg.DeleteSection("remote \"origin\"")
   240  	if err = cfg.SaveToIndent(configPath, "\t"); err != nil {
   241  		return fmt.Errorf("save config file: %w", err)
   242  	}
   243  	return nil
   244  }
   245  
   246  // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
   247  func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) {
   248  	repoPath := repo.RepoPath()
   249  	if err := createDelegateHooks(repoPath); err != nil {
   250  		return repo, fmt.Errorf("createDelegateHooks: %w", err)
   251  	}
   252  	if repo.HasWiki() {
   253  		if err := createDelegateHooks(repo.WikiPath()); err != nil {
   254  			return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
   255  		}
   256  	}
   257  
   258  	_, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
   259  	if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
   260  		return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
   261  	}
   262  
   263  	if repo.HasWiki() {
   264  		if err := cleanUpMigrateGitConfig(path.Join(repo.WikiPath(), "config")); err != nil {
   265  			return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err)
   266  		}
   267  	}
   268  
   269  	return repo, UpdateRepository(ctx, repo, false)
   270  }
   271  
   272  // SyncReleasesWithTags synchronizes release table with repository tags
   273  func SyncReleasesWithTags(repo *repo_model.Repository, gitRepo *git.Repository) error {
   274  	log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
   275  
   276  	// optimized procedure for pull-mirrors which saves a lot of time (in
   277  	// particular for repos with many tags).
   278  	if repo.IsMirror {
   279  		return pullMirrorReleaseSync(repo, gitRepo)
   280  	}
   281  
   282  	existingRelTags := make(container.Set[string])
   283  	opts := repo_model.FindReleasesOptions{
   284  		IncludeDrafts: true,
   285  		IncludeTags:   true,
   286  		ListOptions:   db.ListOptions{PageSize: 50},
   287  	}
   288  	for page := 1; ; page++ {
   289  		opts.Page = page
   290  		rels, err := repo_model.GetReleasesByRepoID(gitRepo.Ctx, repo.ID, opts)
   291  		if err != nil {
   292  			return fmt.Errorf("unable to GetReleasesByRepoID in Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
   293  		}
   294  		if len(rels) == 0 {
   295  			break
   296  		}
   297  		for _, rel := range rels {
   298  			if rel.IsDraft {
   299  				continue
   300  			}
   301  			commitID, err := gitRepo.GetTagCommitID(rel.TagName)
   302  			if err != nil && !git.IsErrNotExist(err) {
   303  				return fmt.Errorf("unable to GetTagCommitID for %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err)
   304  			}
   305  			if git.IsErrNotExist(err) || commitID != rel.Sha1 {
   306  				if err := repo_model.PushUpdateDeleteTag(repo, rel.TagName); err != nil {
   307  					return fmt.Errorf("unable to PushUpdateDeleteTag: %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err)
   308  				}
   309  			} else {
   310  				existingRelTags.Add(strings.ToLower(rel.TagName))
   311  			}
   312  		}
   313  	}
   314  
   315  	_, err := gitRepo.WalkReferences(git.ObjectTag, 0, 0, func(sha1, refname string) error {
   316  		tagName := strings.TrimPrefix(refname, git.TagPrefix)
   317  		if existingRelTags.Contains(strings.ToLower(tagName)) {
   318  			return nil
   319  		}
   320  
   321  		if err := PushUpdateAddTag(db.DefaultContext, repo, gitRepo, tagName, sha1, refname); err != nil {
   322  			return fmt.Errorf("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %w", tagName, repo.ID, repo.OwnerName, repo.Name, err)
   323  		}
   324  
   325  		return nil
   326  	})
   327  	return err
   328  }
   329  
   330  // PushUpdateAddTag must be called for any push actions to add tag
   331  func PushUpdateAddTag(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, tagName, sha1, refname string) error {
   332  	tag, err := gitRepo.GetTagWithID(sha1, tagName)
   333  	if err != nil {
   334  		return fmt.Errorf("unable to GetTag: %w", err)
   335  	}
   336  	commit, err := tag.Commit(gitRepo)
   337  	if err != nil {
   338  		return fmt.Errorf("unable to get tag Commit: %w", err)
   339  	}
   340  
   341  	sig := tag.Tagger
   342  	if sig == nil {
   343  		sig = commit.Author
   344  	}
   345  	if sig == nil {
   346  		sig = commit.Committer
   347  	}
   348  
   349  	var author *user_model.User
   350  	createdAt := time.Unix(1, 0)
   351  
   352  	if sig != nil {
   353  		author, err = user_model.GetUserByEmail(ctx, sig.Email)
   354  		if err != nil && !user_model.IsErrUserNotExist(err) {
   355  			return fmt.Errorf("unable to GetUserByEmail for %q: %w", sig.Email, err)
   356  		}
   357  		createdAt = sig.When
   358  	}
   359  
   360  	commitsCount, err := commit.CommitsCount()
   361  	if err != nil {
   362  		return fmt.Errorf("unable to get CommitsCount: %w", err)
   363  	}
   364  
   365  	rel := repo_model.Release{
   366  		RepoID:       repo.ID,
   367  		TagName:      tagName,
   368  		LowerTagName: strings.ToLower(tagName),
   369  		Sha1:         commit.ID.String(),
   370  		NumCommits:   commitsCount,
   371  		CreatedUnix:  timeutil.TimeStamp(createdAt.Unix()),
   372  		IsTag:        true,
   373  	}
   374  	if author != nil {
   375  		rel.PublisherID = author.ID
   376  	}
   377  
   378  	return repo_model.SaveOrUpdateTag(repo, &rel)
   379  }
   380  
   381  // StoreMissingLfsObjectsInRepository downloads missing LFS objects
   382  func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, lfsClient lfs.Client) error {
   383  	contentStore := lfs.NewContentStore()
   384  
   385  	pointerChan := make(chan lfs.PointerBlob)
   386  	errChan := make(chan error, 1)
   387  	go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan)
   388  
   389  	downloadObjects := func(pointers []lfs.Pointer) error {
   390  		err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error {
   391  			if objectError != nil {
   392  				return objectError
   393  			}
   394  
   395  			defer content.Close()
   396  
   397  			_, err := git_model.NewLFSMetaObject(ctx, &git_model.LFSMetaObject{Pointer: p, RepositoryID: repo.ID})
   398  			if err != nil {
   399  				log.Error("Repo[%-v]: Error creating LFS meta object %-v: %v", repo, p, err)
   400  				return err
   401  			}
   402  
   403  			if err := contentStore.Put(p, content); err != nil {
   404  				log.Error("Repo[%-v]: Error storing content for LFS meta object %-v: %v", repo, p, err)
   405  				if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, p.Oid); err2 != nil {
   406  					log.Error("Repo[%-v]: Error removing LFS meta object %-v: %v", repo, p, err2)
   407  				}
   408  				return err
   409  			}
   410  			return nil
   411  		})
   412  		if err != nil {
   413  			select {
   414  			case <-ctx.Done():
   415  				return nil
   416  			default:
   417  			}
   418  		}
   419  		return err
   420  	}
   421  
   422  	var batch []lfs.Pointer
   423  	for pointerBlob := range pointerChan {
   424  		meta, err := git_model.GetLFSMetaObjectByOid(ctx, repo.ID, pointerBlob.Oid)
   425  		if err != nil && err != git_model.ErrLFSObjectNotExist {
   426  			log.Error("Repo[%-v]: Error querying LFS meta object %-v: %v", repo, pointerBlob.Pointer, err)
   427  			return err
   428  		}
   429  		if meta != nil {
   430  			log.Trace("Repo[%-v]: Skipping unknown LFS meta object %-v", repo, pointerBlob.Pointer)
   431  			continue
   432  		}
   433  
   434  		log.Trace("Repo[%-v]: LFS object %-v not present in repository", repo, pointerBlob.Pointer)
   435  
   436  		exist, err := contentStore.Exists(pointerBlob.Pointer)
   437  		if err != nil {
   438  			log.Error("Repo[%-v]: Error checking if LFS object %-v exists: %v", repo, pointerBlob.Pointer, err)
   439  			return err
   440  		}
   441  
   442  		if exist {
   443  			log.Trace("Repo[%-v]: LFS object %-v already present; creating meta object", repo, pointerBlob.Pointer)
   444  			_, err := git_model.NewLFSMetaObject(ctx, &git_model.LFSMetaObject{Pointer: pointerBlob.Pointer, RepositoryID: repo.ID})
   445  			if err != nil {
   446  				log.Error("Repo[%-v]: Error creating LFS meta object %-v: %v", repo, pointerBlob.Pointer, err)
   447  				return err
   448  			}
   449  		} else {
   450  			if setting.LFS.MaxFileSize > 0 && pointerBlob.Size > setting.LFS.MaxFileSize {
   451  				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)
   452  				continue
   453  			}
   454  
   455  			batch = append(batch, pointerBlob.Pointer)
   456  			if len(batch) >= lfsClient.BatchSize() {
   457  				if err := downloadObjects(batch); err != nil {
   458  					return err
   459  				}
   460  				batch = nil
   461  			}
   462  		}
   463  	}
   464  	if len(batch) > 0 {
   465  		if err := downloadObjects(batch); err != nil {
   466  			return err
   467  		}
   468  	}
   469  
   470  	err, has := <-errChan
   471  	if has {
   472  		log.Error("Repo[%-v]: Error enumerating LFS objects for repository: %v", repo, err)
   473  		return err
   474  	}
   475  
   476  	return nil
   477  }
   478  
   479  // pullMirrorReleaseSync is a pull-mirror specific tag<->release table
   480  // synchronization which overwrites all Releases from the repository tags. This
   481  // can be relied on since a pull-mirror is always identical to its
   482  // upstream. Hence, after each sync we want the pull-mirror release set to be
   483  // identical to the upstream tag set. This is much more efficient for
   484  // repositories like https://github.com/vim/vim (with over 13000 tags).
   485  func pullMirrorReleaseSync(repo *repo_model.Repository, gitRepo *git.Repository) error {
   486  	log.Trace("pullMirrorReleaseSync: rebuilding releases for pull-mirror Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
   487  	tags, numTags, err := gitRepo.GetTagInfos(0, 0)
   488  	if err != nil {
   489  		return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
   490  	}
   491  	err = db.WithTx(db.DefaultContext, func(ctx context.Context) error {
   492  		//
   493  		// clear out existing releases
   494  		//
   495  		if _, err := db.DeleteByBean(ctx, &repo_model.Release{RepoID: repo.ID}); err != nil {
   496  			return fmt.Errorf("unable to clear releases for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
   497  		}
   498  		//
   499  		// make release set identical to upstream tags
   500  		//
   501  		for _, tag := range tags {
   502  			release := repo_model.Release{
   503  				RepoID:       repo.ID,
   504  				TagName:      tag.Name,
   505  				LowerTagName: strings.ToLower(tag.Name),
   506  				Sha1:         tag.Object.String(),
   507  				// NOTE: ignored, since NumCommits are unused
   508  				// for pull-mirrors (only relevant when
   509  				// displaying releases, IsTag: false)
   510  				NumCommits:  -1,
   511  				CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()),
   512  				IsTag:       true,
   513  			}
   514  			if err := db.Insert(ctx, release); err != nil {
   515  				return fmt.Errorf("unable insert tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err)
   516  			}
   517  		}
   518  		return nil
   519  	})
   520  	if err != nil {
   521  		return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
   522  	}
   523  
   524  	log.Trace("pullMirrorReleaseSync: done rebuilding %d releases", numTags)
   525  	return nil
   526  }