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.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  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 mirror
  5. import (
  6. "context"
  7. "fmt"
  8. "net/url"
  9. "strings"
  10. "time"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/cache"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/graceful"
  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/sync"
  20. "code.gitea.io/gitea/modules/timeutil"
  21. "code.gitea.io/gitea/modules/util"
  22. "github.com/unknwon/com"
  23. )
  24. // mirrorQueue holds an UniqueQueue object of the mirror
  25. var mirrorQueue = sync.NewUniqueQueue(setting.Repository.MirrorQueueLength)
  26. func readAddress(m *models.Mirror) {
  27. if len(m.Address) > 0 {
  28. return
  29. }
  30. var err error
  31. m.Address, err = remoteAddress(m.Repo.RepoPath())
  32. if err != nil {
  33. log.Error("remoteAddress: %v", err)
  34. }
  35. }
  36. func remoteAddress(repoPath string) (string, error) {
  37. var cmd *git.Command
  38. err := git.LoadGitVersion()
  39. if err != nil {
  40. return "", err
  41. }
  42. if git.CheckGitVersionAtLeast("2.7") == nil {
  43. cmd = git.NewCommand("remote", "get-url", "origin")
  44. } else {
  45. cmd = git.NewCommand("config", "--get", "remote.origin.url")
  46. }
  47. result, err := cmd.RunInDir(repoPath)
  48. if err != nil {
  49. if strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  50. return "", nil
  51. }
  52. return "", err
  53. }
  54. if len(result) > 0 {
  55. return result[:len(result)-1], nil
  56. }
  57. return "", nil
  58. }
  59. // sanitizeOutput sanitizes output of a command, replacing occurrences of the
  60. // repository's remote address with a sanitized version.
  61. func sanitizeOutput(output, repoPath string) (string, error) {
  62. remoteAddr, err := remoteAddress(repoPath)
  63. if err != nil {
  64. // if we're unable to load the remote address, then we're unable to
  65. // sanitize.
  66. return "", err
  67. }
  68. return util.SanitizeMessage(output, remoteAddr), nil
  69. }
  70. // AddressNoCredentials returns mirror address from Git repository config without credentials.
  71. func AddressNoCredentials(m *models.Mirror) string {
  72. readAddress(m)
  73. u, err := url.Parse(m.Address)
  74. if err != nil {
  75. // this shouldn't happen but just return it unsanitised
  76. return m.Address
  77. }
  78. u.User = nil
  79. return u.String()
  80. }
  81. // UpdateAddress writes new address to Git repository and database
  82. func UpdateAddress(m *models.Mirror, addr string) error {
  83. repoPath := m.Repo.RepoPath()
  84. // Remove old origin
  85. _, err := git.NewCommand("remote", "rm", "origin").RunInDir(repoPath)
  86. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  87. return err
  88. }
  89. _, err = git.NewCommand("remote", "add", "origin", "--mirror=fetch", addr).RunInDir(repoPath)
  90. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  91. return err
  92. }
  93. if m.Repo.HasWiki() {
  94. wikiPath := m.Repo.WikiPath()
  95. wikiRemotePath := repo_module.WikiRemoteURL(addr)
  96. // Remove old origin of wiki
  97. _, err := git.NewCommand("remote", "rm", "origin").RunInDir(wikiPath)
  98. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  99. return err
  100. }
  101. _, err = git.NewCommand("remote", "add", "origin", "--mirror=fetch", wikiRemotePath).RunInDir(wikiPath)
  102. if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
  103. return err
  104. }
  105. }
  106. m.Repo.OriginalURL = addr
  107. return models.UpdateRepositoryCols(m.Repo, "original_url")
  108. }
  109. // gitShortEmptySha Git short empty SHA
  110. const gitShortEmptySha = "0000000"
  111. // mirrorSyncResult contains information of a updated reference.
  112. // If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
  113. // If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty.
  114. type mirrorSyncResult struct {
  115. refName string
  116. oldCommitID string
  117. newCommitID string
  118. }
  119. // parseRemoteUpdateOutput detects create, update and delete operations of references from upstream.
  120. func parseRemoteUpdateOutput(output string) []*mirrorSyncResult {
  121. results := make([]*mirrorSyncResult, 0, 3)
  122. lines := strings.Split(output, "\n")
  123. for i := range lines {
  124. // Make sure reference name is presented before continue
  125. idx := strings.Index(lines[i], "-> ")
  126. if idx == -1 {
  127. continue
  128. }
  129. refName := lines[i][idx+3:]
  130. switch {
  131. case strings.HasPrefix(lines[i], " * "): // New reference
  132. results = append(results, &mirrorSyncResult{
  133. refName: refName,
  134. oldCommitID: gitShortEmptySha,
  135. })
  136. case strings.HasPrefix(lines[i], " - "): // Delete reference
  137. results = append(results, &mirrorSyncResult{
  138. refName: refName,
  139. newCommitID: gitShortEmptySha,
  140. })
  141. case strings.HasPrefix(lines[i], " + "): // Force update
  142. if idx := strings.Index(refName, " "); idx > -1 {
  143. refName = refName[:idx]
  144. }
  145. delimIdx := strings.Index(lines[i][3:], " ")
  146. if delimIdx == -1 {
  147. log.Error("SHA delimiter not found: %q", lines[i])
  148. continue
  149. }
  150. shas := strings.Split(lines[i][3:delimIdx+3], "...")
  151. if len(shas) != 2 {
  152. log.Error("Expect two SHAs but not what found: %q", lines[i])
  153. continue
  154. }
  155. results = append(results, &mirrorSyncResult{
  156. refName: refName,
  157. oldCommitID: shas[0],
  158. newCommitID: shas[1],
  159. })
  160. case strings.HasPrefix(lines[i], " "): // New commits of a reference
  161. delimIdx := strings.Index(lines[i][3:], " ")
  162. if delimIdx == -1 {
  163. log.Error("SHA delimiter not found: %q", lines[i])
  164. continue
  165. }
  166. shas := strings.Split(lines[i][3:delimIdx+3], "..")
  167. if len(shas) != 2 {
  168. log.Error("Expect two SHAs but not what found: %q", lines[i])
  169. continue
  170. }
  171. results = append(results, &mirrorSyncResult{
  172. refName: refName,
  173. oldCommitID: shas[0],
  174. newCommitID: shas[1],
  175. })
  176. default:
  177. log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i])
  178. }
  179. }
  180. return results
  181. }
  182. // runSync returns true if sync finished without error.
  183. func runSync(m *models.Mirror) ([]*mirrorSyncResult, bool) {
  184. repoPath := m.Repo.RepoPath()
  185. wikiPath := m.Repo.WikiPath()
  186. timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
  187. log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo)
  188. gitArgs := []string{"remote", "update"}
  189. if m.EnablePrune {
  190. gitArgs = append(gitArgs, "--prune")
  191. }
  192. stdoutBuilder := strings.Builder{}
  193. stderrBuilder := strings.Builder{}
  194. if err := git.NewCommand(gitArgs...).
  195. SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())).
  196. RunInDirTimeoutPipeline(timeout, repoPath, &stdoutBuilder, &stderrBuilder); err != nil {
  197. stdout := stdoutBuilder.String()
  198. stderr := stderrBuilder.String()
  199. // sanitize the output, since it may contain the remote address, which may
  200. // contain a password
  201. stderrMessage, sanitizeErr := sanitizeOutput(stderr, repoPath)
  202. if sanitizeErr != nil {
  203. log.Error("sanitizeOutput failed on stderr: %v", sanitizeErr)
  204. log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdout, stderr, err)
  205. return nil, false
  206. }
  207. stdoutMessage, err := sanitizeOutput(stdout, repoPath)
  208. if err != nil {
  209. log.Error("sanitizeOutput failed: %v", sanitizeErr)
  210. log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdout, stderrMessage, err)
  211. return nil, false
  212. }
  213. log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
  214. desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage)
  215. if err = models.CreateRepositoryNotice(desc); err != nil {
  216. log.Error("CreateRepositoryNotice: %v", err)
  217. }
  218. return nil, false
  219. }
  220. output := stderrBuilder.String()
  221. gitRepo, err := git.OpenRepository(repoPath)
  222. if err != nil {
  223. log.Error("OpenRepository: %v", err)
  224. return nil, false
  225. }
  226. log.Trace("SyncMirrors [repo: %-v]: syncing releases with tags...", m.Repo)
  227. if err = repo_module.SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
  228. gitRepo.Close()
  229. log.Error("Failed to synchronize tags to releases for repository: %v", err)
  230. }
  231. gitRepo.Close()
  232. log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
  233. if err := m.Repo.UpdateSize(models.DefaultDBContext()); err != nil {
  234. log.Error("Failed to update size for mirror repository: %v", err)
  235. }
  236. if m.Repo.HasWiki() {
  237. log.Trace("SyncMirrors [repo: %-v Wiki]: running git remote update...", m.Repo)
  238. stderrBuilder.Reset()
  239. stdoutBuilder.Reset()
  240. if err := git.NewCommand("remote", "update", "--prune").
  241. SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())).
  242. RunInDirTimeoutPipeline(timeout, wikiPath, &stdoutBuilder, &stderrBuilder); err != nil {
  243. stdout := stdoutBuilder.String()
  244. stderr := stderrBuilder.String()
  245. // sanitize the output, since it may contain the remote address, which may
  246. // contain a password
  247. stderrMessage, sanitizeErr := sanitizeOutput(stderr, repoPath)
  248. if sanitizeErr != nil {
  249. log.Error("sanitizeOutput failed on stderr: %v", sanitizeErr)
  250. log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdout, stderr, err)
  251. return nil, false
  252. }
  253. stdoutMessage, err := sanitizeOutput(stdout, repoPath)
  254. if err != nil {
  255. log.Error("sanitizeOutput failed: %v", sanitizeErr)
  256. log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdout, stderrMessage, err)
  257. return nil, false
  258. }
  259. log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
  260. desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage)
  261. if err = models.CreateRepositoryNotice(desc); err != nil {
  262. log.Error("CreateRepositoryNotice: %v", err)
  263. }
  264. return nil, false
  265. }
  266. log.Trace("SyncMirrors [repo: %-v Wiki]: git remote update complete", m.Repo)
  267. }
  268. log.Trace("SyncMirrors [repo: %-v]: invalidating mirror branch caches...", m.Repo)
  269. branches, err := repo_module.GetBranches(m.Repo)
  270. if err != nil {
  271. log.Error("GetBranches: %v", err)
  272. return nil, false
  273. }
  274. for _, branch := range branches {
  275. cache.Remove(m.Repo.GetCommitsCountCacheKey(branch.Name, true))
  276. }
  277. m.UpdatedUnix = timeutil.TimeStampNow()
  278. return parseRemoteUpdateOutput(output), true
  279. }
  280. // Address returns mirror address from Git repository config without credentials.
  281. func Address(m *models.Mirror) string {
  282. readAddress(m)
  283. return util.SanitizeURLCredentials(m.Address, false)
  284. }
  285. // Username returns the mirror address username
  286. func Username(m *models.Mirror) string {
  287. readAddress(m)
  288. u, err := url.Parse(m.Address)
  289. if err != nil {
  290. // this shouldn't happen but if it does return ""
  291. return ""
  292. }
  293. return u.User.Username()
  294. }
  295. // Password returns the mirror address password
  296. func Password(m *models.Mirror) string {
  297. readAddress(m)
  298. u, err := url.Parse(m.Address)
  299. if err != nil {
  300. // this shouldn't happen but if it does return ""
  301. return ""
  302. }
  303. password, _ := u.User.Password()
  304. return password
  305. }
  306. // Update checks and updates mirror repositories.
  307. func Update(ctx context.Context) error {
  308. log.Trace("Doing: Update")
  309. if err := models.MirrorsIterate(func(idx int, bean interface{}) error {
  310. m := bean.(*models.Mirror)
  311. if m.Repo == nil {
  312. log.Error("Disconnected mirror repository found: %d", m.ID)
  313. return nil
  314. }
  315. select {
  316. case <-ctx.Done():
  317. return fmt.Errorf("Aborted")
  318. default:
  319. mirrorQueue.Add(m.RepoID)
  320. return nil
  321. }
  322. }); err != nil {
  323. log.Trace("Update: %v", err)
  324. return err
  325. }
  326. log.Trace("Finished: Update")
  327. return nil
  328. }
  329. // SyncMirrors checks and syncs mirrors.
  330. // FIXME: graceful: this should be a persistable queue
  331. func SyncMirrors(ctx context.Context) {
  332. // Start listening on new sync requests.
  333. for {
  334. select {
  335. case <-ctx.Done():
  336. mirrorQueue.Close()
  337. return
  338. case repoID := <-mirrorQueue.Queue():
  339. syncMirror(repoID)
  340. }
  341. }
  342. }
  343. func syncMirror(repoID string) {
  344. log.Trace("SyncMirrors [repo_id: %v]", repoID)
  345. defer func() {
  346. err := recover()
  347. if err == nil {
  348. return
  349. }
  350. // There was a panic whilst syncMirrors...
  351. log.Error("PANIC whilst syncMirrors[%s] Panic: %v\nStacktrace: %s", repoID, err, log.Stack(2))
  352. }()
  353. mirrorQueue.Remove(repoID)
  354. m, err := models.GetMirrorByRepoID(com.StrTo(repoID).MustInt64())
  355. if err != nil {
  356. log.Error("GetMirrorByRepoID [%s]: %v", repoID, err)
  357. return
  358. }
  359. log.Trace("SyncMirrors [repo: %-v]: Running Sync", m.Repo)
  360. results, ok := runSync(m)
  361. if !ok {
  362. return
  363. }
  364. log.Trace("SyncMirrors [repo: %-v]: Scheduling next update", m.Repo)
  365. m.ScheduleNextUpdate()
  366. if err = models.UpdateMirror(m); err != nil {
  367. log.Error("UpdateMirror [%s]: %v", repoID, err)
  368. return
  369. }
  370. var gitRepo *git.Repository
  371. if len(results) == 0 {
  372. log.Trace("SyncMirrors [repo: %-v]: no branches updated", m.Repo)
  373. } else {
  374. log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
  375. gitRepo, err = git.OpenRepository(m.Repo.RepoPath())
  376. if err != nil {
  377. log.Error("OpenRepository [%d]: %v", m.RepoID, err)
  378. return
  379. }
  380. defer gitRepo.Close()
  381. if ok := checkAndUpdateEmptyRepository(m, gitRepo, results); !ok {
  382. return
  383. }
  384. }
  385. for _, result := range results {
  386. // Discard GitHub pull requests, i.e. refs/pull/*
  387. if strings.HasPrefix(result.refName, "refs/pull/") {
  388. continue
  389. }
  390. tp, _ := git.SplitRefName(result.refName)
  391. // Create reference
  392. if result.oldCommitID == gitShortEmptySha {
  393. notification.NotifySyncCreateRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
  394. continue
  395. }
  396. // Delete reference
  397. if result.newCommitID == gitShortEmptySha {
  398. notification.NotifySyncDeleteRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
  399. continue
  400. }
  401. // Push commits
  402. oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID)
  403. if err != nil {
  404. log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
  405. continue
  406. }
  407. newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID)
  408. if err != nil {
  409. log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
  410. continue
  411. }
  412. commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
  413. if err != nil {
  414. log.Error("CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err)
  415. continue
  416. }
  417. theCommits := repo_module.ListToPushCommits(commits)
  418. if len(theCommits.Commits) > setting.UI.FeedMaxCommitNum {
  419. theCommits.Commits = theCommits.Commits[:setting.UI.FeedMaxCommitNum]
  420. }
  421. theCommits.CompareURL = m.Repo.ComposeCompareURL(oldCommitID, newCommitID)
  422. notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, result.refName, oldCommitID, newCommitID, theCommits)
  423. }
  424. log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo)
  425. // Get latest commit date and update to current repository updated time
  426. commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath())
  427. if err != nil {
  428. log.Error("GetLatestCommitDate [%d]: %v", m.RepoID, err)
  429. return
  430. }
  431. if err = models.UpdateRepositoryUpdatedTime(m.RepoID, commitDate); err != nil {
  432. log.Error("Update repository 'updated_unix' [%d]: %v", m.RepoID, err)
  433. return
  434. }
  435. log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo)
  436. }
  437. func checkAndUpdateEmptyRepository(m *models.Mirror, gitRepo *git.Repository, results []*mirrorSyncResult) bool {
  438. if !m.Repo.IsEmpty {
  439. return true
  440. }
  441. hasDefault := false
  442. hasMaster := false
  443. defaultBranchName := m.Repo.DefaultBranch
  444. if len(defaultBranchName) == 0 {
  445. defaultBranchName = setting.Repository.DefaultBranch
  446. }
  447. firstName := ""
  448. for _, result := range results {
  449. if strings.HasPrefix(result.refName, "refs/pull/") {
  450. continue
  451. }
  452. tp, name := git.SplitRefName(result.refName)
  453. if len(tp) > 0 && tp != git.BranchPrefix {
  454. continue
  455. }
  456. if len(firstName) == 0 {
  457. firstName = name
  458. }
  459. hasDefault = hasDefault || name == defaultBranchName
  460. hasMaster = hasMaster || name == "master"
  461. }
  462. if len(firstName) > 0 {
  463. if hasDefault {
  464. m.Repo.DefaultBranch = defaultBranchName
  465. } else if hasMaster {
  466. m.Repo.DefaultBranch = "master"
  467. } else {
  468. m.Repo.DefaultBranch = firstName
  469. }
  470. // Update the git repository default branch
  471. if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil {
  472. if !git.IsErrUnsupportedVersion(err) {
  473. log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err)
  474. desc := fmt.Sprintf("Failed to uupdate default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err)
  475. if err = models.CreateRepositoryNotice(desc); err != nil {
  476. log.Error("CreateRepositoryNotice: %v", err)
  477. }
  478. return false
  479. }
  480. }
  481. m.Repo.IsEmpty = false
  482. // Update the is empty and default_branch columns
  483. if err := models.UpdateRepositoryCols(m.Repo, "default_branch", "is_empty"); err != nil {
  484. log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err)
  485. desc := fmt.Sprintf("Failed to uupdate default branch of repository '%s': %v", m.Repo.RepoPath(), err)
  486. if err = models.CreateRepositoryNotice(desc); err != nil {
  487. log.Error("CreateRepositoryNotice: %v", err)
  488. }
  489. return false
  490. }
  491. }
  492. return true
  493. }
  494. // InitSyncMirrors initializes a go routine to sync the mirrors
  495. func InitSyncMirrors() {
  496. go graceful.GetManager().RunWithShutdownContext(SyncMirrors)
  497. }
  498. // StartToMirror adds repoID to mirror queue
  499. func StartToMirror(repoID int64) {
  500. go mirrorQueue.Add(repoID)
  501. }