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.

mirror_push.go 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. // Copyright 2021 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 mirror
  5. import (
  6. "context"
  7. "errors"
  8. "io"
  9. "net/url"
  10. "regexp"
  11. "time"
  12. "code.gitea.io/gitea/models"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/lfs"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/repository"
  17. "code.gitea.io/gitea/modules/setting"
  18. "code.gitea.io/gitea/modules/timeutil"
  19. "code.gitea.io/gitea/modules/util"
  20. )
  21. var stripExitStatus = regexp.MustCompile(`exit status \d+ - `)
  22. // AddPushMirrorRemote registers the push mirror remote.
  23. func AddPushMirrorRemote(m *models.PushMirror, addr string) error {
  24. addRemoteAndConfig := func(addr, path string) error {
  25. if _, err := git.NewCommand("remote", "add", "--mirror=push", m.RemoteName, addr).RunInDir(path); err != nil {
  26. return err
  27. }
  28. if _, err := git.NewCommand("config", "--add", "remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunInDir(path); err != nil {
  29. return err
  30. }
  31. if _, err := git.NewCommand("config", "--add", "remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunInDir(path); err != nil {
  32. return err
  33. }
  34. return nil
  35. }
  36. if err := addRemoteAndConfig(addr, m.Repo.RepoPath()); err != nil {
  37. return err
  38. }
  39. if m.Repo.HasWiki() {
  40. wikiRemoteURL := repository.WikiRemoteURL(addr)
  41. if len(wikiRemoteURL) > 0 {
  42. if err := addRemoteAndConfig(wikiRemoteURL, m.Repo.WikiPath()); err != nil {
  43. return err
  44. }
  45. }
  46. }
  47. return nil
  48. }
  49. // RemovePushMirrorRemote removes the push mirror remote.
  50. func RemovePushMirrorRemote(m *models.PushMirror) error {
  51. cmd := git.NewCommand("remote", "rm", m.RemoteName)
  52. if _, err := cmd.RunInDir(m.Repo.RepoPath()); err != nil {
  53. return err
  54. }
  55. if m.Repo.HasWiki() {
  56. if _, err := cmd.RunInDir(m.Repo.WikiPath()); err != nil {
  57. // The wiki remote may not exist
  58. log.Warn("Wiki Remote[%d] could not be removed: %v", m.ID, err)
  59. }
  60. }
  61. return nil
  62. }
  63. // SyncPushMirror starts the sync of the push mirror and schedules the next run.
  64. func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
  65. log.Trace("SyncPushMirror [mirror: %d]", mirrorID)
  66. defer func() {
  67. err := recover()
  68. if err == nil {
  69. return
  70. }
  71. // There was a panic whilst syncPushMirror...
  72. log.Error("PANIC whilst syncPushMirror[%d] Panic: %v\nStacktrace: %s", mirrorID, err, log.Stack(2))
  73. }()
  74. m, err := models.GetPushMirrorByID(mirrorID)
  75. if err != nil {
  76. log.Error("GetPushMirrorByID [%d]: %v", mirrorID, err)
  77. return false
  78. }
  79. m.LastError = ""
  80. log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Running Sync", m.ID, m.Repo)
  81. err = runPushSync(ctx, m)
  82. if err != nil {
  83. log.Error("SyncPushMirror [mirror: %d][repo: %-v]: %v", m.ID, m.Repo, err)
  84. m.LastError = stripExitStatus.ReplaceAllLiteralString(err.Error(), "")
  85. }
  86. m.LastUpdateUnix = timeutil.TimeStampNow()
  87. if err := models.UpdatePushMirror(m); err != nil {
  88. log.Error("UpdatePushMirror [%d]: %v", m.ID, err)
  89. return false
  90. }
  91. log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Finished", m.ID, m.Repo)
  92. return err == nil
  93. }
  94. func runPushSync(ctx context.Context, m *models.PushMirror) error {
  95. timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
  96. performPush := func(path string) error {
  97. remoteAddr, err := git.GetRemoteAddress(path, m.RemoteName)
  98. if err != nil {
  99. log.Error("GetRemoteAddress(%s) Error %v", path, err)
  100. return errors.New("Unexpected error")
  101. }
  102. if setting.LFS.StartServer {
  103. log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
  104. gitRepo, err := git.OpenRepository(path)
  105. if err != nil {
  106. log.Error("OpenRepository: %v", err)
  107. return errors.New("Unexpected error")
  108. }
  109. defer gitRepo.Close()
  110. ep := lfs.DetermineEndpoint(remoteAddr.String(), "")
  111. if err := pushAllLFSObjects(ctx, gitRepo, ep, false); err != nil {
  112. return util.NewURLSanitizedError(err, remoteAddr, true)
  113. }
  114. }
  115. log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName)
  116. if err := git.Push(path, git.PushOptions{
  117. Remote: m.RemoteName,
  118. Force: true,
  119. Mirror: true,
  120. Timeout: timeout,
  121. }); err != nil {
  122. log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err)
  123. return util.NewURLSanitizedError(err, remoteAddr, true)
  124. }
  125. return nil
  126. }
  127. err := performPush(m.Repo.RepoPath())
  128. if err != nil {
  129. return err
  130. }
  131. if m.Repo.HasWiki() {
  132. wikiPath := m.Repo.WikiPath()
  133. _, err := git.GetRemoteAddress(wikiPath, m.RemoteName)
  134. if err == nil {
  135. err := performPush(wikiPath)
  136. if err != nil {
  137. return err
  138. }
  139. } else {
  140. log.Trace("Skipping wiki: No remote configured")
  141. }
  142. }
  143. return nil
  144. }
  145. func pushAllLFSObjects(ctx context.Context, gitRepo *git.Repository, endpoint *url.URL, skipTLSVerify bool) error {
  146. client := lfs.NewClient(endpoint, skipTLSVerify)
  147. contentStore := lfs.NewContentStore()
  148. pointerChan := make(chan lfs.PointerBlob)
  149. errChan := make(chan error, 1)
  150. go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan)
  151. uploadObjects := func(pointers []lfs.Pointer) error {
  152. err := client.Upload(ctx, pointers, func(p lfs.Pointer, objectError error) (io.ReadCloser, error) {
  153. if objectError != nil {
  154. return nil, objectError
  155. }
  156. content, err := contentStore.Get(p)
  157. if err != nil {
  158. log.Error("Error reading LFS object %v: %v", p, err)
  159. }
  160. return content, err
  161. })
  162. if err != nil {
  163. select {
  164. case <-ctx.Done():
  165. return nil
  166. default:
  167. }
  168. }
  169. return err
  170. }
  171. var batch []lfs.Pointer
  172. for pointerBlob := range pointerChan {
  173. exists, err := contentStore.Exists(pointerBlob.Pointer)
  174. if err != nil {
  175. log.Error("Error checking if LFS object %v exists: %v", pointerBlob.Pointer, err)
  176. return err
  177. }
  178. if !exists {
  179. log.Trace("Skipping missing LFS object %v", pointerBlob.Pointer)
  180. continue
  181. }
  182. batch = append(batch, pointerBlob.Pointer)
  183. if len(batch) >= client.BatchSize() {
  184. if err := uploadObjects(batch); err != nil {
  185. return err
  186. }
  187. batch = nil
  188. }
  189. }
  190. if len(batch) > 0 {
  191. if err := uploadObjects(batch); err != nil {
  192. return err
  193. }
  194. }
  195. err, has := <-errChan
  196. if has {
  197. log.Error("Error enumerating LFS objects for repository: %v", err)
  198. return err
  199. }
  200. return nil
  201. }