You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

repo.go 12KB

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