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 }