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 16KB

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