code.gitea.io/gitea@v1.22.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 "strings" 12 "time" 13 14 "code.gitea.io/gitea/models/db" 15 git_model "code.gitea.io/gitea/models/git" 16 repo_model "code.gitea.io/gitea/models/repo" 17 user_model "code.gitea.io/gitea/models/user" 18 "code.gitea.io/gitea/modules/container" 19 "code.gitea.io/gitea/modules/git" 20 "code.gitea.io/gitea/modules/gitrepo" 21 "code.gitea.io/gitea/modules/lfs" 22 "code.gitea.io/gitea/modules/log" 23 "code.gitea.io/gitea/modules/setting" 24 "code.gitea.io/gitea/modules/timeutil" 25 ) 26 27 /* 28 GitHub, GitLab, Gogs: *.wiki.git 29 BitBucket: *.git/wiki 30 */ 31 var commonWikiURLSuffixes = []string{".wiki.git", ".git/wiki"} 32 33 // WikiRemoteURL returns accessible repository URL for wiki if exists. 34 // Otherwise, it returns an empty string. 35 func WikiRemoteURL(ctx context.Context, remote string) string { 36 remote = strings.TrimSuffix(remote, ".git") 37 for _, suffix := range commonWikiURLSuffixes { 38 wikiURL := remote + suffix 39 if git.IsRepoURLAccessible(ctx, wikiURL) { 40 return wikiURL 41 } 42 } 43 return "" 44 } 45 46 // SyncRepoTags synchronizes releases table with repository tags 47 func SyncRepoTags(ctx context.Context, repoID int64) error { 48 repo, err := repo_model.GetRepositoryByID(ctx, repoID) 49 if err != nil { 50 return err 51 } 52 53 gitRepo, err := gitrepo.OpenRepository(ctx, repo) 54 if err != nil { 55 return err 56 } 57 defer gitRepo.Close() 58 59 return SyncReleasesWithTags(ctx, repo, gitRepo) 60 } 61 62 // SyncReleasesWithTags synchronizes release table with repository tags 63 func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error { 64 log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) 65 66 // optimized procedure for pull-mirrors which saves a lot of time (in 67 // particular for repos with many tags). 68 if repo.IsMirror { 69 return pullMirrorReleaseSync(ctx, repo, gitRepo) 70 } 71 72 existingRelTags := make(container.Set[string]) 73 opts := repo_model.FindReleasesOptions{ 74 IncludeDrafts: true, 75 IncludeTags: true, 76 ListOptions: db.ListOptions{PageSize: 50}, 77 RepoID: repo.ID, 78 } 79 for page := 1; ; page++ { 80 opts.Page = page 81 rels, err := db.Find[repo_model.Release](gitRepo.Ctx, opts) 82 if err != nil { 83 return fmt.Errorf("unable to GetReleasesByRepoID in Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) 84 } 85 if len(rels) == 0 { 86 break 87 } 88 for _, rel := range rels { 89 if rel.IsDraft { 90 continue 91 } 92 commitID, err := gitRepo.GetTagCommitID(rel.TagName) 93 if err != nil && !git.IsErrNotExist(err) { 94 return fmt.Errorf("unable to GetTagCommitID for %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err) 95 } 96 if git.IsErrNotExist(err) || commitID != rel.Sha1 { 97 if err := repo_model.PushUpdateDeleteTag(ctx, repo, rel.TagName); err != nil { 98 return fmt.Errorf("unable to PushUpdateDeleteTag: %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err) 99 } 100 } else { 101 existingRelTags.Add(strings.ToLower(rel.TagName)) 102 } 103 } 104 } 105 106 _, err := gitRepo.WalkReferences(git.ObjectTag, 0, 0, func(sha1, refname string) error { 107 tagName := strings.TrimPrefix(refname, git.TagPrefix) 108 if existingRelTags.Contains(strings.ToLower(tagName)) { 109 return nil 110 } 111 112 if err := PushUpdateAddTag(ctx, repo, gitRepo, tagName, sha1, refname); err != nil { 113 // sometimes, some tags will be sync failed. i.e. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tag/?h=v2.6.11 114 // this is a tree object, not a tag object which created before git 115 log.Error("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %v", tagName, repo.ID, repo.OwnerName, repo.Name, err) 116 } 117 118 return nil 119 }) 120 return err 121 } 122 123 // PushUpdateAddTag must be called for any push actions to add tag 124 func PushUpdateAddTag(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, tagName, sha1, refname string) error { 125 tag, err := gitRepo.GetTagWithID(sha1, tagName) 126 if err != nil { 127 return fmt.Errorf("unable to GetTag: %w", err) 128 } 129 commit, err := tag.Commit(gitRepo) 130 if err != nil { 131 return fmt.Errorf("unable to get tag Commit: %w", err) 132 } 133 134 sig := tag.Tagger 135 if sig == nil { 136 sig = commit.Author 137 } 138 if sig == nil { 139 sig = commit.Committer 140 } 141 142 var author *user_model.User 143 createdAt := time.Unix(1, 0) 144 145 if sig != nil { 146 author, err = user_model.GetUserByEmail(ctx, sig.Email) 147 if err != nil && !user_model.IsErrUserNotExist(err) { 148 return fmt.Errorf("unable to GetUserByEmail for %q: %w", sig.Email, err) 149 } 150 createdAt = sig.When 151 } 152 153 commitsCount, err := commit.CommitsCount() 154 if err != nil { 155 return fmt.Errorf("unable to get CommitsCount: %w", err) 156 } 157 158 rel := repo_model.Release{ 159 RepoID: repo.ID, 160 TagName: tagName, 161 LowerTagName: strings.ToLower(tagName), 162 Sha1: commit.ID.String(), 163 NumCommits: commitsCount, 164 CreatedUnix: timeutil.TimeStamp(createdAt.Unix()), 165 IsTag: true, 166 } 167 if author != nil { 168 rel.PublisherID = author.ID 169 } 170 171 return repo_model.SaveOrUpdateTag(ctx, repo, &rel) 172 } 173 174 // StoreMissingLfsObjectsInRepository downloads missing LFS objects 175 func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, lfsClient lfs.Client) error { 176 contentStore := lfs.NewContentStore() 177 178 pointerChan := make(chan lfs.PointerBlob) 179 errChan := make(chan error, 1) 180 go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan) 181 182 downloadObjects := func(pointers []lfs.Pointer) error { 183 err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error { 184 if objectError != nil { 185 if errors.Is(objectError, lfs.ErrObjectNotExist) { 186 log.Warn("Repo[%-v]: Ignore missing LFS object %-v: %v", repo, p, objectError) 187 return nil 188 } 189 return objectError 190 } 191 192 defer content.Close() 193 194 _, err := git_model.NewLFSMetaObject(ctx, repo.ID, p) 195 if err != nil { 196 log.Error("Repo[%-v]: Error creating LFS meta object %-v: %v", repo, p, err) 197 return err 198 } 199 200 if err := contentStore.Put(p, content); err != nil { 201 log.Error("Repo[%-v]: Error storing content for LFS meta object %-v: %v", repo, p, err) 202 if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, p.Oid); err2 != nil { 203 log.Error("Repo[%-v]: Error removing LFS meta object %-v: %v", repo, p, err2) 204 } 205 return err 206 } 207 return nil 208 }) 209 if err != nil { 210 select { 211 case <-ctx.Done(): 212 return nil 213 default: 214 } 215 } 216 return err 217 } 218 219 var batch []lfs.Pointer 220 for pointerBlob := range pointerChan { 221 meta, err := git_model.GetLFSMetaObjectByOid(ctx, repo.ID, pointerBlob.Oid) 222 if err != nil && err != git_model.ErrLFSObjectNotExist { 223 log.Error("Repo[%-v]: Error querying LFS meta object %-v: %v", repo, pointerBlob.Pointer, err) 224 return err 225 } 226 if meta != nil { 227 log.Trace("Repo[%-v]: Skipping unknown LFS meta object %-v", repo, pointerBlob.Pointer) 228 continue 229 } 230 231 log.Trace("Repo[%-v]: LFS object %-v not present in repository", repo, pointerBlob.Pointer) 232 233 exist, err := contentStore.Exists(pointerBlob.Pointer) 234 if err != nil { 235 log.Error("Repo[%-v]: Error checking if LFS object %-v exists: %v", repo, pointerBlob.Pointer, err) 236 return err 237 } 238 239 if exist { 240 log.Trace("Repo[%-v]: LFS object %-v already present; creating meta object", repo, pointerBlob.Pointer) 241 _, err := git_model.NewLFSMetaObject(ctx, repo.ID, pointerBlob.Pointer) 242 if err != nil { 243 log.Error("Repo[%-v]: Error creating LFS meta object %-v: %v", repo, pointerBlob.Pointer, err) 244 return err 245 } 246 } else { 247 if setting.LFS.MaxFileSize > 0 && pointerBlob.Size > setting.LFS.MaxFileSize { 248 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) 249 continue 250 } 251 252 batch = append(batch, pointerBlob.Pointer) 253 if len(batch) >= lfsClient.BatchSize() { 254 if err := downloadObjects(batch); err != nil { 255 return err 256 } 257 batch = nil 258 } 259 } 260 } 261 if len(batch) > 0 { 262 if err := downloadObjects(batch); err != nil { 263 return err 264 } 265 } 266 267 err, has := <-errChan 268 if has { 269 log.Error("Repo[%-v]: Error enumerating LFS objects for repository: %v", repo, err) 270 return err 271 } 272 273 return nil 274 } 275 276 // shortRelease to reduce load memory, this struct can replace repo_model.Release 277 type shortRelease struct { 278 ID int64 279 TagName string 280 Sha1 string 281 IsTag bool 282 } 283 284 func (shortRelease) TableName() string { 285 return "release" 286 } 287 288 // pullMirrorReleaseSync is a pull-mirror specific tag<->release table 289 // synchronization which overwrites all Releases from the repository tags. This 290 // can be relied on since a pull-mirror is always identical to its 291 // upstream. Hence, after each sync we want the pull-mirror release set to be 292 // identical to the upstream tag set. This is much more efficient for 293 // repositories like https://github.com/vim/vim (with over 13000 tags). 294 func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error { 295 log.Trace("pullMirrorReleaseSync: rebuilding releases for pull-mirror Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) 296 tags, numTags, err := gitRepo.GetTagInfos(0, 0) 297 if err != nil { 298 return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) 299 } 300 err = db.WithTx(ctx, func(ctx context.Context) error { 301 dbReleases, err := db.Find[shortRelease](ctx, repo_model.FindReleasesOptions{ 302 RepoID: repo.ID, 303 IncludeDrafts: true, 304 IncludeTags: true, 305 }) 306 if err != nil { 307 return fmt.Errorf("unable to FindReleases in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) 308 } 309 310 inserts, deletes, updates := calcSync(tags, dbReleases) 311 // 312 // make release set identical to upstream tags 313 // 314 for _, tag := range inserts { 315 release := repo_model.Release{ 316 RepoID: repo.ID, 317 TagName: tag.Name, 318 LowerTagName: strings.ToLower(tag.Name), 319 Sha1: tag.Object.String(), 320 // NOTE: ignored, since NumCommits are unused 321 // for pull-mirrors (only relevant when 322 // displaying releases, IsTag: false) 323 NumCommits: -1, 324 CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()), 325 IsTag: true, 326 } 327 if err := db.Insert(ctx, release); err != nil { 328 return fmt.Errorf("unable insert tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err) 329 } 330 } 331 332 // only delete tags releases 333 if len(deletes) > 0 { 334 if _, err := db.GetEngine(ctx).Where("repo_id=?", repo.ID). 335 In("id", deletes). 336 Delete(&repo_model.Release{}); err != nil { 337 return fmt.Errorf("unable to delete tags for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) 338 } 339 } 340 341 for _, tag := range updates { 342 if _, err := db.GetEngine(ctx).Where("repo_id = ? AND lower_tag_name = ?", repo.ID, strings.ToLower(tag.Name)). 343 Cols("sha1"). 344 Update(&repo_model.Release{ 345 Sha1: tag.Object.String(), 346 }); err != nil { 347 return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err) 348 } 349 } 350 return nil 351 }) 352 if err != nil { 353 return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) 354 } 355 356 log.Trace("pullMirrorReleaseSync: done rebuilding %d releases", numTags) 357 return nil 358 } 359 360 func calcSync(destTags []*git.Tag, dbTags []*shortRelease) ([]*git.Tag, []int64, []*git.Tag) { 361 destTagMap := make(map[string]*git.Tag) 362 for _, tag := range destTags { 363 destTagMap[tag.Name] = tag 364 } 365 dbTagMap := make(map[string]*shortRelease) 366 for _, rel := range dbTags { 367 dbTagMap[rel.TagName] = rel 368 } 369 370 inserted := make([]*git.Tag, 0, 10) 371 updated := make([]*git.Tag, 0, 10) 372 for _, tag := range destTags { 373 rel := dbTagMap[tag.Name] 374 if rel == nil { 375 inserted = append(inserted, tag) 376 } else if rel.Sha1 != tag.Object.String() { 377 updated = append(updated, tag) 378 } 379 } 380 deleted := make([]int64, 0, 10) 381 for _, tag := range dbTags { 382 if destTagMap[tag.TagName] == nil && tag.IsTag { 383 deleted = append(deleted, tag.ID) 384 } 385 } 386 return inserted, deleted, updated 387 }