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_pull.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  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. "fmt"
  8. "strings"
  9. "time"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/models/db"
  12. "code.gitea.io/gitea/modules/cache"
  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/notification"
  17. repo_module "code.gitea.io/gitea/modules/repository"
  18. "code.gitea.io/gitea/modules/setting"
  19. "code.gitea.io/gitea/modules/timeutil"
  20. "code.gitea.io/gitea/modules/util"
  21. )
  22. // gitShortEmptySha Git short empty SHA
  23. const gitShortEmptySha = "0000000"
  24. // UpdateAddress writes new address to Git repository and database
  25. func UpdateAddress(m *models.Mirror, addr string) error {
  26. remoteName := m.GetRemoteName()
  27. repoPath := m.Repo.RepoPath()
  28. // Remove old remote
  29. _, err := git.NewCommand("remote", "rm", remoteName).RunInDir(repoPath)
  30. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  31. return err
  32. }
  33. _, err = git.NewCommand("remote", "add", remoteName, "--mirror=fetch", addr).RunInDir(repoPath)
  34. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  35. return err
  36. }
  37. if m.Repo.HasWiki() {
  38. wikiPath := m.Repo.WikiPath()
  39. wikiRemotePath := repo_module.WikiRemoteURL(addr)
  40. // Remove old remote of wiki
  41. _, err := git.NewCommand("remote", "rm", remoteName).RunInDir(wikiPath)
  42. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  43. return err
  44. }
  45. _, err = git.NewCommand("remote", "add", remoteName, "--mirror=fetch", wikiRemotePath).RunInDir(wikiPath)
  46. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  47. return err
  48. }
  49. }
  50. m.Repo.OriginalURL = addr
  51. return models.UpdateRepositoryCols(m.Repo, "original_url")
  52. }
  53. // mirrorSyncResult contains information of a updated reference.
  54. // If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
  55. // If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty.
  56. type mirrorSyncResult struct {
  57. refName string
  58. oldCommitID string
  59. newCommitID string
  60. }
  61. // parseRemoteUpdateOutput detects create, update and delete operations of references from upstream.
  62. func parseRemoteUpdateOutput(output string) []*mirrorSyncResult {
  63. results := make([]*mirrorSyncResult, 0, 3)
  64. lines := strings.Split(output, "\n")
  65. for i := range lines {
  66. // Make sure reference name is presented before continue
  67. idx := strings.Index(lines[i], "-> ")
  68. if idx == -1 {
  69. continue
  70. }
  71. refName := lines[i][idx+3:]
  72. switch {
  73. case strings.HasPrefix(lines[i], " * "): // New reference
  74. if strings.HasPrefix(lines[i], " * [new tag]") {
  75. refName = git.TagPrefix + refName
  76. } else if strings.HasPrefix(lines[i], " * [new branch]") {
  77. refName = git.BranchPrefix + refName
  78. }
  79. results = append(results, &mirrorSyncResult{
  80. refName: refName,
  81. oldCommitID: gitShortEmptySha,
  82. })
  83. case strings.HasPrefix(lines[i], " - "): // Delete reference
  84. results = append(results, &mirrorSyncResult{
  85. refName: refName,
  86. newCommitID: gitShortEmptySha,
  87. })
  88. case strings.HasPrefix(lines[i], " + "): // Force update
  89. if idx := strings.Index(refName, " "); idx > -1 {
  90. refName = refName[:idx]
  91. }
  92. delimIdx := strings.Index(lines[i][3:], " ")
  93. if delimIdx == -1 {
  94. log.Error("SHA delimiter not found: %q", lines[i])
  95. continue
  96. }
  97. shas := strings.Split(lines[i][3:delimIdx+3], "...")
  98. if len(shas) != 2 {
  99. log.Error("Expect two SHAs but not what found: %q", lines[i])
  100. continue
  101. }
  102. results = append(results, &mirrorSyncResult{
  103. refName: refName,
  104. oldCommitID: shas[0],
  105. newCommitID: shas[1],
  106. })
  107. case strings.HasPrefix(lines[i], " "): // New commits of a reference
  108. delimIdx := strings.Index(lines[i][3:], " ")
  109. if delimIdx == -1 {
  110. log.Error("SHA delimiter not found: %q", lines[i])
  111. continue
  112. }
  113. shas := strings.Split(lines[i][3:delimIdx+3], "..")
  114. if len(shas) != 2 {
  115. log.Error("Expect two SHAs but not what found: %q", lines[i])
  116. continue
  117. }
  118. results = append(results, &mirrorSyncResult{
  119. refName: refName,
  120. oldCommitID: shas[0],
  121. newCommitID: shas[1],
  122. })
  123. default:
  124. log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i])
  125. }
  126. }
  127. return results
  128. }
  129. // runSync returns true if sync finished without error.
  130. func runSync(ctx context.Context, m *models.Mirror) ([]*mirrorSyncResult, bool) {
  131. repoPath := m.Repo.RepoPath()
  132. wikiPath := m.Repo.WikiPath()
  133. timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
  134. log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo)
  135. gitArgs := []string{"remote", "update"}
  136. if m.EnablePrune {
  137. gitArgs = append(gitArgs, "--prune")
  138. }
  139. gitArgs = append(gitArgs, m.GetRemoteName())
  140. remoteAddr, remoteErr := git.GetRemoteAddress(repoPath, m.GetRemoteName())
  141. if remoteErr != nil {
  142. log.Error("GetRemoteAddress Error %v", remoteErr)
  143. }
  144. stdoutBuilder := strings.Builder{}
  145. stderrBuilder := strings.Builder{}
  146. if err := git.NewCommand(gitArgs...).
  147. SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())).
  148. RunInDirTimeoutPipeline(timeout, repoPath, &stdoutBuilder, &stderrBuilder); err != nil {
  149. stdout := stdoutBuilder.String()
  150. stderr := stderrBuilder.String()
  151. // sanitize the output, since it may contain the remote address, which may
  152. // contain a password
  153. sanitizer := util.NewURLSanitizer(remoteAddr, true)
  154. stderrMessage := sanitizer.Replace(stderr)
  155. stdoutMessage := sanitizer.Replace(stdout)
  156. log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
  157. desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage)
  158. if err = models.CreateRepositoryNotice(desc); err != nil {
  159. log.Error("CreateRepositoryNotice: %v", err)
  160. }
  161. return nil, false
  162. }
  163. output := stderrBuilder.String()
  164. gitRepo, err := git.OpenRepository(repoPath)
  165. if err != nil {
  166. log.Error("OpenRepository: %v", err)
  167. return nil, false
  168. }
  169. log.Trace("SyncMirrors [repo: %-v]: syncing releases with tags...", m.Repo)
  170. if err = repo_module.SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
  171. log.Error("Failed to synchronize tags to releases for repository: %v", err)
  172. }
  173. if m.LFS && setting.LFS.StartServer {
  174. log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
  175. ep := lfs.DetermineEndpoint(remoteAddr.String(), m.LFSEndpoint)
  176. if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, ep, false); err != nil {
  177. log.Error("Failed to synchronize LFS objects for repository: %v", err)
  178. }
  179. }
  180. gitRepo.Close()
  181. log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
  182. if err := m.Repo.UpdateSize(db.DefaultContext); err != nil {
  183. log.Error("Failed to update size for mirror repository: %v", err)
  184. }
  185. if m.Repo.HasWiki() {
  186. log.Trace("SyncMirrors [repo: %-v Wiki]: running git remote update...", m.Repo)
  187. stderrBuilder.Reset()
  188. stdoutBuilder.Reset()
  189. if err := git.NewCommand("remote", "update", "--prune", m.GetRemoteName()).
  190. SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())).
  191. RunInDirTimeoutPipeline(timeout, wikiPath, &stdoutBuilder, &stderrBuilder); err != nil {
  192. stdout := stdoutBuilder.String()
  193. stderr := stderrBuilder.String()
  194. // sanitize the output, since it may contain the remote address, which may
  195. // contain a password
  196. remoteAddr, remoteErr := git.GetRemoteAddress(wikiPath, m.GetRemoteName())
  197. if remoteErr != nil {
  198. log.Error("GetRemoteAddress Error %v", remoteErr)
  199. }
  200. sanitizer := util.NewURLSanitizer(remoteAddr, true)
  201. stderrMessage := sanitizer.Replace(stderr)
  202. stdoutMessage := sanitizer.Replace(stdout)
  203. log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
  204. desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage)
  205. if err = models.CreateRepositoryNotice(desc); err != nil {
  206. log.Error("CreateRepositoryNotice: %v", err)
  207. }
  208. return nil, false
  209. }
  210. log.Trace("SyncMirrors [repo: %-v Wiki]: git remote update complete", m.Repo)
  211. }
  212. log.Trace("SyncMirrors [repo: %-v]: invalidating mirror branch caches...", m.Repo)
  213. branches, _, err := repo_module.GetBranches(m.Repo, 0, 0)
  214. if err != nil {
  215. log.Error("GetBranches: %v", err)
  216. return nil, false
  217. }
  218. for _, branch := range branches {
  219. cache.Remove(m.Repo.GetCommitsCountCacheKey(branch.Name, true))
  220. }
  221. m.UpdatedUnix = timeutil.TimeStampNow()
  222. return parseRemoteUpdateOutput(output), true
  223. }
  224. // SyncPullMirror starts the sync of the pull mirror and schedules the next run.
  225. func SyncPullMirror(ctx context.Context, repoID int64) bool {
  226. log.Trace("SyncMirrors [repo_id: %v]", repoID)
  227. defer func() {
  228. err := recover()
  229. if err == nil {
  230. return
  231. }
  232. // There was a panic whilst syncMirrors...
  233. log.Error("PANIC whilst syncMirrors[%d] Panic: %v\nStacktrace: %s", repoID, err, log.Stack(2))
  234. }()
  235. m, err := models.GetMirrorByRepoID(repoID)
  236. if err != nil {
  237. log.Error("GetMirrorByRepoID [%d]: %v", repoID, err)
  238. return false
  239. }
  240. log.Trace("SyncMirrors [repo: %-v]: Running Sync", m.Repo)
  241. results, ok := runSync(ctx, m)
  242. if !ok {
  243. return false
  244. }
  245. log.Trace("SyncMirrors [repo: %-v]: Scheduling next update", m.Repo)
  246. m.ScheduleNextUpdate()
  247. if err = models.UpdateMirror(m); err != nil {
  248. log.Error("UpdateMirror [%d]: %v", m.RepoID, err)
  249. return false
  250. }
  251. var gitRepo *git.Repository
  252. if len(results) == 0 {
  253. log.Trace("SyncMirrors [repo: %-v]: no branches updated", m.Repo)
  254. } else {
  255. log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
  256. gitRepo, err = git.OpenRepository(m.Repo.RepoPath())
  257. if err != nil {
  258. log.Error("OpenRepository [%d]: %v", m.RepoID, err)
  259. return false
  260. }
  261. defer gitRepo.Close()
  262. if ok := checkAndUpdateEmptyRepository(m, gitRepo, results); !ok {
  263. return false
  264. }
  265. }
  266. for _, result := range results {
  267. // Discard GitHub pull requests, i.e. refs/pull/*
  268. if strings.HasPrefix(result.refName, "refs/pull/") {
  269. continue
  270. }
  271. tp, _ := git.SplitRefName(result.refName)
  272. // Create reference
  273. if result.oldCommitID == gitShortEmptySha {
  274. if tp == git.TagPrefix {
  275. tp = "tag"
  276. } else if tp == git.BranchPrefix {
  277. tp = "branch"
  278. }
  279. commitID, err := gitRepo.GetRefCommitID(result.refName)
  280. if err != nil {
  281. log.Error("gitRepo.GetRefCommitID [repo_id: %d, ref_name: %s]: %v", m.RepoID, result.refName, err)
  282. continue
  283. }
  284. notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{
  285. RefFullName: result.refName,
  286. OldCommitID: git.EmptySHA,
  287. NewCommitID: commitID,
  288. }, repo_module.NewPushCommits())
  289. notification.NotifySyncCreateRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
  290. continue
  291. }
  292. // Delete reference
  293. if result.newCommitID == gitShortEmptySha {
  294. notification.NotifySyncDeleteRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
  295. continue
  296. }
  297. // Push commits
  298. oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID)
  299. if err != nil {
  300. log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
  301. continue
  302. }
  303. newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID)
  304. if err != nil {
  305. log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
  306. continue
  307. }
  308. commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
  309. if err != nil {
  310. log.Error("CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err)
  311. continue
  312. }
  313. theCommits := repo_module.GitToPushCommits(commits)
  314. if len(theCommits.Commits) > setting.UI.FeedMaxCommitNum {
  315. theCommits.Commits = theCommits.Commits[:setting.UI.FeedMaxCommitNum]
  316. }
  317. theCommits.CompareURL = m.Repo.ComposeCompareURL(oldCommitID, newCommitID)
  318. notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{
  319. RefFullName: result.refName,
  320. OldCommitID: oldCommitID,
  321. NewCommitID: newCommitID,
  322. }, theCommits)
  323. }
  324. log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo)
  325. // Get latest commit date and update to current repository updated time
  326. commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath())
  327. if err != nil {
  328. log.Error("GetLatestCommitDate [%d]: %v", m.RepoID, err)
  329. return false
  330. }
  331. if err = models.UpdateRepositoryUpdatedTime(m.RepoID, commitDate); err != nil {
  332. log.Error("Update repository 'updated_unix' [%d]: %v", m.RepoID, err)
  333. return false
  334. }
  335. log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo)
  336. return true
  337. }
  338. func checkAndUpdateEmptyRepository(m *models.Mirror, gitRepo *git.Repository, results []*mirrorSyncResult) bool {
  339. if !m.Repo.IsEmpty {
  340. return true
  341. }
  342. hasDefault := false
  343. hasMaster := false
  344. hasMain := false
  345. defaultBranchName := m.Repo.DefaultBranch
  346. if len(defaultBranchName) == 0 {
  347. defaultBranchName = setting.Repository.DefaultBranch
  348. }
  349. firstName := ""
  350. for _, result := range results {
  351. if strings.HasPrefix(result.refName, "refs/pull/") {
  352. continue
  353. }
  354. tp, name := git.SplitRefName(result.refName)
  355. if len(tp) > 0 && tp != git.BranchPrefix {
  356. continue
  357. }
  358. if len(firstName) == 0 {
  359. firstName = name
  360. }
  361. hasDefault = hasDefault || name == defaultBranchName
  362. hasMaster = hasMaster || name == "master"
  363. hasMain = hasMain || name == "main"
  364. }
  365. if len(firstName) > 0 {
  366. if hasDefault {
  367. m.Repo.DefaultBranch = defaultBranchName
  368. } else if hasMaster {
  369. m.Repo.DefaultBranch = "master"
  370. } else if hasMain {
  371. m.Repo.DefaultBranch = "main"
  372. } else {
  373. m.Repo.DefaultBranch = firstName
  374. }
  375. // Update the git repository default branch
  376. if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil {
  377. if !git.IsErrUnsupportedVersion(err) {
  378. log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err)
  379. desc := fmt.Sprintf("Failed to uupdate default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err)
  380. if err = models.CreateRepositoryNotice(desc); err != nil {
  381. log.Error("CreateRepositoryNotice: %v", err)
  382. }
  383. return false
  384. }
  385. }
  386. m.Repo.IsEmpty = false
  387. // Update the is empty and default_branch columns
  388. if err := models.UpdateRepositoryCols(m.Repo, "default_branch", "is_empty"); err != nil {
  389. log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err)
  390. desc := fmt.Sprintf("Failed to uupdate default branch of repository '%s': %v", m.Repo.RepoPath(), err)
  391. if err = models.CreateRepositoryNotice(desc); err != nil {
  392. log.Error("CreateRepositoryNotice: %v", err)
  393. }
  394. return false
  395. }
  396. }
  397. return true
  398. }