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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  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"
  14. "code.gitea.io/gitea/models/db"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/lfs"
  17. "code.gitea.io/gitea/modules/log"
  18. "code.gitea.io/gitea/modules/migration"
  19. "code.gitea.io/gitea/modules/setting"
  20. "code.gitea.io/gitea/modules/timeutil"
  21. "code.gitea.io/gitea/modules/util"
  22. "gopkg.in/ini.v1"
  23. )
  24. /*
  25. GitHub, GitLab, Gogs: *.wiki.git
  26. BitBucket: *.git/wiki
  27. */
  28. var commonWikiURLSuffixes = []string{".wiki.git", ".git/wiki"}
  29. // WikiRemoteURL returns accessible repository URL for wiki if exists.
  30. // Otherwise, it returns an empty string.
  31. func WikiRemoteURL(remote string) string {
  32. remote = strings.TrimSuffix(remote, ".git")
  33. for _, suffix := range commonWikiURLSuffixes {
  34. wikiURL := remote + suffix
  35. if git.IsRepoURLAccessible(wikiURL) {
  36. return wikiURL
  37. }
  38. }
  39. return ""
  40. }
  41. // MigrateRepositoryGitData starts migrating git related data after created migrating repository
  42. func MigrateRepositoryGitData(ctx context.Context, u *models.User,
  43. repo *models.Repository, opts migration.MigrateOptions,
  44. httpTransport *http.Transport,
  45. ) (*models.Repository, error) {
  46. repoPath := models.RepoPath(u.Name, opts.RepoName)
  47. if u.IsOrganization() {
  48. t, err := models.OrgFromUser(u).GetOwnerTeam()
  49. if err != nil {
  50. return nil, err
  51. }
  52. repo.NumWatches = t.NumMembers
  53. } else {
  54. repo.NumWatches = 1
  55. }
  56. migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
  57. var err error
  58. if err = util.RemoveAll(repoPath); err != nil {
  59. return repo, fmt.Errorf("Failed to remove %s: %v", repoPath, err)
  60. }
  61. if err = git.CloneWithContext(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
  62. Mirror: true,
  63. Quiet: true,
  64. Timeout: migrateTimeout,
  65. }); err != nil {
  66. return repo, fmt.Errorf("Clone: %v", err)
  67. }
  68. if opts.Wiki {
  69. wikiPath := models.WikiPath(u.Name, opts.RepoName)
  70. wikiRemotePath := WikiRemoteURL(opts.CloneAddr)
  71. if len(wikiRemotePath) > 0 {
  72. if err := util.RemoveAll(wikiPath); err != nil {
  73. return repo, fmt.Errorf("Failed to remove %s: %v", wikiPath, err)
  74. }
  75. if err = git.CloneWithContext(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
  76. Mirror: true,
  77. Quiet: true,
  78. Timeout: migrateTimeout,
  79. Branch: "master",
  80. }); err != nil {
  81. log.Warn("Clone wiki: %v", err)
  82. if err := util.RemoveAll(wikiPath); err != nil {
  83. return repo, fmt.Errorf("Failed to remove %s: %v", wikiPath, err)
  84. }
  85. }
  86. }
  87. }
  88. if repo.OwnerID == u.ID {
  89. repo.Owner = u
  90. }
  91. if err = repo.CheckDaemonExportOK(ctx); err != nil {
  92. return repo, fmt.Errorf("checkDaemonExportOK: %v", err)
  93. }
  94. if stdout, err := git.NewCommandContext(ctx, "update-server-info").
  95. SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)).
  96. RunInDir(repoPath); err != nil {
  97. log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
  98. return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %v", err)
  99. }
  100. gitRepo, err := git.OpenRepository(repoPath)
  101. if err != nil {
  102. return repo, fmt.Errorf("OpenRepository: %v", err)
  103. }
  104. defer gitRepo.Close()
  105. repo.IsEmpty, err = gitRepo.IsEmpty()
  106. if err != nil {
  107. return repo, fmt.Errorf("git.IsEmpty: %v", err)
  108. }
  109. if !repo.IsEmpty {
  110. if len(repo.DefaultBranch) == 0 {
  111. // Try to get HEAD branch and set it as default branch.
  112. headBranch, err := gitRepo.GetHEADBranch()
  113. if err != nil {
  114. return repo, fmt.Errorf("GetHEADBranch: %v", err)
  115. }
  116. if headBranch != nil {
  117. repo.DefaultBranch = headBranch.Name
  118. }
  119. }
  120. if !opts.Releases {
  121. if err = SyncReleasesWithTags(repo, gitRepo); err != nil {
  122. log.Error("Failed to synchronize tags to releases for repository: %v", err)
  123. }
  124. }
  125. if opts.LFS {
  126. endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint)
  127. lfsClient := lfs.NewClient(endpoint, httpTransport)
  128. if err = StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil {
  129. log.Error("Failed to store missing LFS objects for repository: %v", err)
  130. }
  131. }
  132. }
  133. if err = repo.UpdateSize(db.DefaultContext); err != nil {
  134. log.Error("Failed to update size for repository: %v", err)
  135. }
  136. if opts.Mirror {
  137. mirrorModel := models.Mirror{
  138. RepoID: repo.ID,
  139. Interval: setting.Mirror.DefaultInterval,
  140. EnablePrune: true,
  141. NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval),
  142. LFS: opts.LFS,
  143. }
  144. if opts.LFS {
  145. mirrorModel.LFSEndpoint = opts.LFSEndpoint
  146. }
  147. if opts.MirrorInterval != "" {
  148. parsedInterval, err := time.ParseDuration(opts.MirrorInterval)
  149. if err != nil {
  150. log.Error("Failed to set Interval: %v", err)
  151. return repo, err
  152. }
  153. if parsedInterval == 0 {
  154. mirrorModel.Interval = 0
  155. mirrorModel.NextUpdateUnix = 0
  156. } else if parsedInterval < setting.Mirror.MinInterval {
  157. err := fmt.Errorf("Interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval)
  158. log.Error("Interval: %s is too frequent", opts.MirrorInterval)
  159. return repo, err
  160. } else {
  161. mirrorModel.Interval = parsedInterval
  162. mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval)
  163. }
  164. }
  165. if err = models.InsertMirror(&mirrorModel); err != nil {
  166. return repo, fmt.Errorf("InsertOne: %v", err)
  167. }
  168. repo.IsMirror = true
  169. err = models.UpdateRepository(repo, false)
  170. } else {
  171. repo, err = CleanUpMigrateInfo(repo)
  172. }
  173. return repo, err
  174. }
  175. // cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
  176. // This also removes possible user credentials.
  177. func cleanUpMigrateGitConfig(configPath string) error {
  178. cfg, err := ini.Load(configPath)
  179. if err != nil {
  180. return fmt.Errorf("open config file: %v", err)
  181. }
  182. cfg.DeleteSection("remote \"origin\"")
  183. if err = cfg.SaveToIndent(configPath, "\t"); err != nil {
  184. return fmt.Errorf("save config file: %v", err)
  185. }
  186. return nil
  187. }
  188. // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
  189. func CleanUpMigrateInfo(repo *models.Repository) (*models.Repository, error) {
  190. repoPath := repo.RepoPath()
  191. if err := createDelegateHooks(repoPath); err != nil {
  192. return repo, fmt.Errorf("createDelegateHooks: %v", err)
  193. }
  194. if repo.HasWiki() {
  195. if err := createDelegateHooks(repo.WikiPath()); err != nil {
  196. return repo, fmt.Errorf("createDelegateHooks.(wiki): %v", err)
  197. }
  198. }
  199. _, err := git.NewCommand("remote", "rm", "origin").RunInDir(repoPath)
  200. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  201. return repo, fmt.Errorf("CleanUpMigrateInfo: %v", err)
  202. }
  203. if repo.HasWiki() {
  204. if err := cleanUpMigrateGitConfig(path.Join(repo.WikiPath(), "config")); err != nil {
  205. return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %v", err)
  206. }
  207. }
  208. return repo, models.UpdateRepository(repo, false)
  209. }
  210. // SyncReleasesWithTags synchronizes release table with repository tags
  211. func SyncReleasesWithTags(repo *models.Repository, gitRepo *git.Repository) error {
  212. existingRelTags := make(map[string]struct{})
  213. opts := models.FindReleasesOptions{
  214. IncludeDrafts: true,
  215. IncludeTags: true,
  216. ListOptions: db.ListOptions{PageSize: 50},
  217. }
  218. for page := 1; ; page++ {
  219. opts.Page = page
  220. rels, err := models.GetReleasesByRepoID(repo.ID, opts)
  221. if err != nil {
  222. return fmt.Errorf("GetReleasesByRepoID: %v", err)
  223. }
  224. if len(rels) == 0 {
  225. break
  226. }
  227. for _, rel := range rels {
  228. if rel.IsDraft {
  229. continue
  230. }
  231. commitID, err := gitRepo.GetTagCommitID(rel.TagName)
  232. if err != nil && !git.IsErrNotExist(err) {
  233. return fmt.Errorf("GetTagCommitID: %s: %v", rel.TagName, err)
  234. }
  235. if git.IsErrNotExist(err) || commitID != rel.Sha1 {
  236. if err := models.PushUpdateDeleteTag(repo, rel.TagName); err != nil {
  237. return fmt.Errorf("PushUpdateDeleteTag: %s: %v", rel.TagName, err)
  238. }
  239. } else {
  240. existingRelTags[strings.ToLower(rel.TagName)] = struct{}{}
  241. }
  242. }
  243. }
  244. tags, err := gitRepo.GetTags(0, 0)
  245. if err != nil {
  246. return fmt.Errorf("GetTags: %v", err)
  247. }
  248. for _, tagName := range tags {
  249. if _, ok := existingRelTags[strings.ToLower(tagName)]; !ok {
  250. if err := PushUpdateAddTag(repo, gitRepo, tagName); err != nil {
  251. return fmt.Errorf("pushUpdateAddTag: %v", err)
  252. }
  253. }
  254. }
  255. return nil
  256. }
  257. // PushUpdateAddTag must be called for any push actions to add tag
  258. func PushUpdateAddTag(repo *models.Repository, gitRepo *git.Repository, tagName string) error {
  259. tag, err := gitRepo.GetTag(tagName)
  260. if err != nil {
  261. return fmt.Errorf("GetTag: %v", err)
  262. }
  263. commit, err := tag.Commit()
  264. if err != nil {
  265. return fmt.Errorf("Commit: %v", err)
  266. }
  267. sig := tag.Tagger
  268. if sig == nil {
  269. sig = commit.Author
  270. }
  271. if sig == nil {
  272. sig = commit.Committer
  273. }
  274. var author *models.User
  275. var createdAt = time.Unix(1, 0)
  276. if sig != nil {
  277. author, err = models.GetUserByEmail(sig.Email)
  278. if err != nil && !models.IsErrUserNotExist(err) {
  279. return fmt.Errorf("GetUserByEmail: %v", err)
  280. }
  281. createdAt = sig.When
  282. }
  283. commitsCount, err := commit.CommitsCount()
  284. if err != nil {
  285. return fmt.Errorf("CommitsCount: %v", err)
  286. }
  287. var rel = models.Release{
  288. RepoID: repo.ID,
  289. TagName: tagName,
  290. LowerTagName: strings.ToLower(tagName),
  291. Sha1: commit.ID.String(),
  292. NumCommits: commitsCount,
  293. CreatedUnix: timeutil.TimeStamp(createdAt.Unix()),
  294. IsTag: true,
  295. }
  296. if author != nil {
  297. rel.PublisherID = author.ID
  298. }
  299. return models.SaveOrUpdateTag(repo, &rel)
  300. }
  301. // StoreMissingLfsObjectsInRepository downloads missing LFS objects
  302. func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *models.Repository, gitRepo *git.Repository, lfsClient lfs.Client) error {
  303. contentStore := lfs.NewContentStore()
  304. pointerChan := make(chan lfs.PointerBlob)
  305. errChan := make(chan error, 1)
  306. go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan)
  307. downloadObjects := func(pointers []lfs.Pointer) error {
  308. err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error {
  309. if objectError != nil {
  310. return objectError
  311. }
  312. defer content.Close()
  313. _, err := models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: p, RepositoryID: repo.ID})
  314. if err != nil {
  315. log.Error("Error creating LFS meta object %v: %v", p, err)
  316. return err
  317. }
  318. if err := contentStore.Put(p, content); err != nil {
  319. log.Error("Error storing content for LFS meta object %v: %v", p, err)
  320. if _, err2 := repo.RemoveLFSMetaObjectByOid(p.Oid); err2 != nil {
  321. log.Error("Error removing LFS meta object %v: %v", p, err2)
  322. }
  323. return err
  324. }
  325. return nil
  326. })
  327. if err != nil {
  328. select {
  329. case <-ctx.Done():
  330. return nil
  331. default:
  332. }
  333. }
  334. return err
  335. }
  336. var batch []lfs.Pointer
  337. for pointerBlob := range pointerChan {
  338. meta, err := repo.GetLFSMetaObjectByOid(pointerBlob.Oid)
  339. if err != nil && err != models.ErrLFSObjectNotExist {
  340. log.Error("Error querying LFS meta object %v: %v", pointerBlob.Pointer, err)
  341. return err
  342. }
  343. if meta != nil {
  344. log.Trace("Skipping unknown LFS meta object %v", pointerBlob.Pointer)
  345. continue
  346. }
  347. log.Trace("LFS object %v not present in repository %s", pointerBlob.Pointer, repo.FullName())
  348. exist, err := contentStore.Exists(pointerBlob.Pointer)
  349. if err != nil {
  350. log.Error("Error checking if LFS object %v exists: %v", pointerBlob.Pointer, err)
  351. return err
  352. }
  353. if exist {
  354. log.Trace("LFS object %v already present; creating meta object", pointerBlob.Pointer)
  355. _, err := models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: pointerBlob.Pointer, RepositoryID: repo.ID})
  356. if err != nil {
  357. log.Error("Error creating LFS meta object %v: %v", pointerBlob.Pointer, err)
  358. return err
  359. }
  360. } else {
  361. if setting.LFS.MaxFileSize > 0 && pointerBlob.Size > setting.LFS.MaxFileSize {
  362. log.Info("LFS object %v download denied because of LFS_MAX_FILE_SIZE=%d < size %d", pointerBlob.Pointer, setting.LFS.MaxFileSize, pointerBlob.Size)
  363. continue
  364. }
  365. batch = append(batch, pointerBlob.Pointer)
  366. if len(batch) >= lfsClient.BatchSize() {
  367. if err := downloadObjects(batch); err != nil {
  368. return err
  369. }
  370. batch = nil
  371. }
  372. }
  373. }
  374. if len(batch) > 0 {
  375. if err := downloadObjects(batch); err != nil {
  376. return err
  377. }
  378. }
  379. err, has := <-errChan
  380. if has {
  381. log.Error("Error enumerating LFS objects for repository: %v", err)
  382. return err
  383. }
  384. return nil
  385. }