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.

701 lines
21 KiB

  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 cmd
  5. import (
  6. "bufio"
  7. "bytes"
  8. "context"
  9. "fmt"
  10. "io/ioutil"
  11. golog "log"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "strings"
  16. "text/tabwriter"
  17. "code.gitea.io/gitea/models"
  18. "code.gitea.io/gitea/models/migrations"
  19. "code.gitea.io/gitea/modules/git"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/options"
  22. "code.gitea.io/gitea/modules/repository"
  23. "code.gitea.io/gitea/modules/setting"
  24. "code.gitea.io/gitea/modules/util"
  25. "xorm.io/builder"
  26. "xorm.io/xorm"
  27. "github.com/urfave/cli"
  28. )
  29. // CmdDoctor represents the available doctor sub-command.
  30. var CmdDoctor = cli.Command{
  31. Name: "doctor",
  32. Usage: "Diagnose problems",
  33. Description: "A command to diagnose problems with the current Gitea instance according to the given configuration.",
  34. Action: runDoctor,
  35. Flags: []cli.Flag{
  36. cli.BoolFlag{
  37. Name: "list",
  38. Usage: "List the available checks",
  39. },
  40. cli.BoolFlag{
  41. Name: "default",
  42. Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
  43. },
  44. cli.StringSliceFlag{
  45. Name: "run",
  46. Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
  47. },
  48. cli.BoolFlag{
  49. Name: "all",
  50. Usage: "Run all the available checks",
  51. },
  52. cli.BoolFlag{
  53. Name: "fix",
  54. Usage: "Automatically fix what we can",
  55. },
  56. cli.StringFlag{
  57. Name: "log-file",
  58. Usage: `Name of the log file (default: "doctor.log"). Set to "-" to output to stdout, set to "" to disable`,
  59. },
  60. },
  61. Subcommands: []cli.Command{
  62. cmdRecreateTable,
  63. },
  64. }
  65. var cmdRecreateTable = cli.Command{
  66. Name: "recreate-table",
  67. Usage: "Recreate tables from XORM definitions and copy the data.",
  68. ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
  69. Flags: []cli.Flag{
  70. cli.BoolFlag{
  71. Name: "debug",
  72. Usage: "Print SQL commands sent",
  73. },
  74. },
  75. Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.
  76. This command will cause Xorm to recreate tables, copying over the data and deleting the old table.
  77. You should back-up your database before doing this and ensure that your database is up-to-date first.`,
  78. Action: runRecreateTable,
  79. }
  80. type check struct {
  81. title string
  82. name string
  83. isDefault bool
  84. f func(ctx *cli.Context) ([]string, error)
  85. abortIfFailed bool
  86. skipDatabaseInit bool
  87. }
  88. // checklist represents list for all checks
  89. var checklist = []check{
  90. {
  91. // NOTE: this check should be the first in the list
  92. title: "Check paths and basic configuration",
  93. name: "paths",
  94. isDefault: true,
  95. f: runDoctorPathInfo,
  96. abortIfFailed: true,
  97. skipDatabaseInit: true,
  98. },
  99. {
  100. title: "Check Database Version",
  101. name: "check-db-version",
  102. isDefault: true,
  103. f: runDoctorCheckDBVersion,
  104. abortIfFailed: false,
  105. },
  106. {
  107. title: "Check consistency of database",
  108. name: "check-db-consistency",
  109. isDefault: false,
  110. f: runDoctorCheckDBConsistency,
  111. },
  112. {
  113. title: "Check if OpenSSH authorized_keys file is up-to-date",
  114. name: "authorized_keys",
  115. isDefault: true,
  116. f: runDoctorAuthorizedKeys,
  117. },
  118. {
  119. title: "Check if SCRIPT_TYPE is available",
  120. name: "script-type",
  121. isDefault: false,
  122. f: runDoctorScriptType,
  123. },
  124. {
  125. title: "Check if hook files are up-to-date and executable",
  126. name: "hooks",
  127. isDefault: false,
  128. f: runDoctorHooks,
  129. },
  130. {
  131. title: "Recalculate merge bases",
  132. name: "recalculate_merge_bases",
  133. isDefault: false,
  134. f: runDoctorPRMergeBase,
  135. },
  136. {
  137. title: "Recalculate Stars number for all user",
  138. name: "recalculate_stars_number",
  139. isDefault: false,
  140. f: runDoctorUserStarNum,
  141. },
  142. {
  143. title: "Enable push options",
  144. name: "enable-push-options",
  145. isDefault: false,
  146. f: runDoctorEnablePushOptions,
  147. },
  148. // more checks please append here
  149. }
  150. func runRecreateTable(ctx *cli.Context) error {
  151. // Redirect the default golog to here
  152. golog.SetFlags(0)
  153. golog.SetPrefix("")
  154. golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
  155. setting.NewContext()
  156. setting.InitDBConfig()
  157. setting.EnableXORMLog = ctx.Bool("debug")
  158. setting.Database.LogSQL = ctx.Bool("debug")
  159. setting.Cfg.Section("log").Key("XORM").SetValue(",")
  160. setting.NewXORMLogService(!ctx.Bool("debug"))
  161. if err := models.SetEngine(); err != nil {
  162. fmt.Println(err)
  163. fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
  164. return nil
  165. }
  166. args := ctx.Args()
  167. names := make([]string, 0, ctx.NArg())
  168. for i := 0; i < ctx.NArg(); i++ {
  169. names = append(names, args.Get(i))
  170. }
  171. beans, err := models.NamesToBean(names...)
  172. if err != nil {
  173. return err
  174. }
  175. recreateTables := migrations.RecreateTables(beans...)
  176. return models.NewEngine(context.Background(), func(x *xorm.Engine) error {
  177. if err := migrations.EnsureUpToDate(x); err != nil {
  178. return err
  179. }
  180. return recreateTables(x)
  181. })
  182. }
  183. func runDoctor(ctx *cli.Context) error {
  184. // Silence the default loggers
  185. log.DelNamedLogger("console")
  186. log.DelNamedLogger(log.DEFAULT)
  187. // Now setup our own
  188. logFile := ctx.String("log-file")
  189. if !ctx.IsSet("log-file") {
  190. logFile = "doctor.log"
  191. }
  192. if len(logFile) == 0 {
  193. log.NewLogger(1000, "doctor", "console", `{"level":"NONE","stacktracelevel":"NONE","colorize":"%t"}`)
  194. } else if logFile == "-" {
  195. log.NewLogger(1000, "doctor", "console", `{"level":"trace","stacktracelevel":"NONE"}`)
  196. } else {
  197. log.NewLogger(1000, "doctor", "file", fmt.Sprintf(`{"filename":%q,"level":"trace","stacktracelevel":"NONE"}`, logFile))
  198. }
  199. // Finally redirect the default golog to here
  200. golog.SetFlags(0)
  201. golog.SetPrefix("")
  202. golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
  203. if ctx.IsSet("list") {
  204. w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
  205. _, _ = w.Write([]byte("Default\tName\tTitle\n"))
  206. for _, check := range checklist {
  207. if check.isDefault {
  208. _, _ = w.Write([]byte{'*'})
  209. }
  210. _, _ = w.Write([]byte{'\t'})
  211. _, _ = w.Write([]byte(check.name))
  212. _, _ = w.Write([]byte{'\t'})
  213. _, _ = w.Write([]byte(check.title))
  214. _, _ = w.Write([]byte{'\n'})
  215. }
  216. return w.Flush()
  217. }
  218. var checks []check
  219. if ctx.Bool("all") {
  220. checks = checklist
  221. } else if ctx.IsSet("run") {
  222. addDefault := ctx.Bool("default")
  223. names := ctx.StringSlice("run")
  224. for i, name := range names {
  225. names[i] = strings.ToLower(strings.TrimSpace(name))
  226. }
  227. for _, check := range checklist {
  228. if addDefault && check.isDefault {
  229. checks = append(checks, check)
  230. continue
  231. }
  232. for _, name := range names {
  233. if name == check.name {
  234. checks = append(checks, check)
  235. break
  236. }
  237. }
  238. }
  239. } else {
  240. for _, check := range checklist {
  241. if check.isDefault {
  242. checks = append(checks, check)
  243. }
  244. }
  245. }
  246. dbIsInit := false
  247. for i, check := range checks {
  248. if !dbIsInit && !check.skipDatabaseInit {
  249. // Only open database after the most basic configuration check
  250. setting.EnableXORMLog = false
  251. if err := initDBDisableConsole(true); err != nil {
  252. fmt.Println(err)
  253. fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
  254. return nil
  255. }
  256. dbIsInit = true
  257. }
  258. fmt.Println("[", i+1, "]", check.title)
  259. messages, err := check.f(ctx)
  260. for _, message := range messages {
  261. fmt.Println("-", message)
  262. }
  263. if err != nil {
  264. fmt.Println("Error:", err)
  265. if check.abortIfFailed {
  266. return nil
  267. }
  268. } else {
  269. fmt.Println("OK.")
  270. }
  271. fmt.Println()
  272. }
  273. return nil
  274. }
  275. func runDoctorPathInfo(ctx *cli.Context) ([]string, error) {
  276. res := make([]string, 0, 10)
  277. if fi, err := os.Stat(setting.CustomConf); err != nil || !fi.Mode().IsRegular() {
  278. res = append(res, fmt.Sprintf("Failed to find configuration file at '%s'.", setting.CustomConf))
  279. res = append(res, fmt.Sprintf("If you've never ran Gitea yet, this is normal and '%s' will be created for you on first run.", setting.CustomConf))
  280. res = append(res, "Otherwise check that you are running this command from the correct path and/or provide a `--config` parameter.")
  281. return res, fmt.Errorf("can't proceed without a configuration file")
  282. }
  283. setting.NewContext()
  284. fail := false
  285. check := func(name, path string, is_dir, required, is_write bool) {
  286. res = append(res, fmt.Sprintf("%-25s '%s'", name+":", path))
  287. fi, err := os.Stat(path)
  288. if err != nil {
  289. if os.IsNotExist(err) && ctx.Bool("fix") && is_dir {
  290. if err := os.MkdirAll(path, 0777); err != nil {
  291. res = append(res, fmt.Sprintf(" ERROR: %v", err))
  292. fail = true
  293. return
  294. }
  295. fi, err = os.Stat(path)
  296. }
  297. }
  298. if err != nil {
  299. if required {
  300. res = append(res, fmt.Sprintf(" ERROR: %v", err))
  301. fail = true
  302. return
  303. }
  304. res = append(res, fmt.Sprintf(" NOTICE: not accessible (%v)", err))
  305. return
  306. }
  307. if is_dir && !fi.IsDir() {
  308. res = append(res, " ERROR: not a directory")
  309. fail = true
  310. return
  311. } else if !is_dir && !fi.Mode().IsRegular() {
  312. res = append(res, " ERROR: not a regular file")
  313. fail = true
  314. } else if is_write {
  315. if err := runDoctorWritableDir(path); err != nil {
  316. res = append(res, fmt.Sprintf(" ERROR: not writable: %v", err))
  317. fail = true
  318. }
  319. }
  320. }
  321. // Note print paths inside quotes to make any leading/trailing spaces evident
  322. check("Configuration File Path", setting.CustomConf, false, true, false)
  323. check("Repository Root Path", setting.RepoRootPath, true, true, true)
  324. check("Data Root Path", setting.AppDataPath, true, true, true)
  325. check("Custom File Root Path", setting.CustomPath, true, false, false)
  326. check("Work directory", setting.AppWorkPath, true, true, false)
  327. check("Log Root Path", setting.LogRootPath, true, true, true)
  328. if options.IsDynamic() {
  329. // Do not check/report on StaticRootPath if data is embedded in Gitea (-tags bindata)
  330. check("Static File Root Path", setting.StaticRootPath, true, true, false)
  331. }
  332. if fail {
  333. return res, fmt.Errorf("please check your configuration file and try again")
  334. }
  335. return res, nil
  336. }
  337. func runDoctorWritableDir(path string) error {
  338. // There's no platform-independent way of checking if a directory is writable
  339. // https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable
  340. tmpFile, err := ioutil.TempFile(path, "doctors-order")
  341. if err != nil {
  342. return err
  343. }
  344. if err := util.Remove(tmpFile.Name()); err != nil {
  345. fmt.Printf("Warning: can't remove temporary file: '%s'\n", tmpFile.Name())
  346. }
  347. tmpFile.Close()
  348. return nil
  349. }
  350. const tplCommentPrefix = `# gitea public key`
  351. func runDoctorAuthorizedKeys(ctx *cli.Context) ([]string, error) {
  352. if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
  353. return nil, nil
  354. }
  355. fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
  356. f, err := os.Open(fPath)
  357. if err != nil {
  358. if ctx.Bool("fix") {
  359. return []string{fmt.Sprintf("Error whilst opening authorized_keys: %v. Attempting regeneration", err)}, models.RewriteAllPublicKeys()
  360. }
  361. return nil, err
  362. }
  363. defer f.Close()
  364. linesInAuthorizedKeys := map[string]bool{}
  365. scanner := bufio.NewScanner(f)
  366. for scanner.Scan() {
  367. line := scanner.Text()
  368. if strings.HasPrefix(line, tplCommentPrefix) {
  369. continue
  370. }
  371. linesInAuthorizedKeys[line] = true
  372. }
  373. f.Close()
  374. // now we regenerate and check if there are any lines missing
  375. regenerated := &bytes.Buffer{}
  376. if err := models.RegeneratePublicKeys(regenerated); err != nil {
  377. return nil, err
  378. }
  379. scanner = bufio.NewScanner(regenerated)
  380. for scanner.Scan() {
  381. line := scanner.Text()
  382. if strings.HasPrefix(line, tplCommentPrefix) {
  383. continue
  384. }
  385. if ok := linesInAuthorizedKeys[line]; ok {
  386. continue
  387. }
  388. if ctx.Bool("fix") {
  389. return []string{"authorized_keys is out of date, attempting regeneration"}, models.RewriteAllPublicKeys()
  390. }
  391. return nil, fmt.Errorf(`authorized_keys is out of date and should be regenerated with "gitea admin regenerate keys" or "gitea doctor --run authorized_keys --fix"`)
  392. }
  393. return nil, nil
  394. }
  395. func runDoctorCheckDBVersion(ctx *cli.Context) ([]string, error) {
  396. if err := models.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil {
  397. if ctx.Bool("fix") {
  398. return []string{fmt.Sprintf("WARN: Got Error %v during ensure up to date", err), "Attempting to migrate to the latest DB version to fix this."}, models.NewEngine(context.Background(), migrations.Migrate)
  399. }
  400. return nil, err
  401. }
  402. return nil, nil
  403. }
  404. func iterateRepositories(each func(*models.Repository) ([]string, error)) ([]string, error) {
  405. results := []string{}
  406. err := models.Iterate(
  407. models.DefaultDBContext(),
  408. new(models.Repository),
  409. builder.Gt{"id": 0},
  410. func(idx int, bean interface{}) error {
  411. res, err := each(bean.(*models.Repository))
  412. results = append(results, res...)
  413. return err
  414. },
  415. )
  416. return results, err
  417. }
  418. func iteratePRs(repo *models.Repository, each func(*models.Repository, *models.PullRequest) ([]string, error)) ([]string, error) {
  419. results := []string{}
  420. err := models.Iterate(
  421. models.DefaultDBContext(),
  422. new(models.PullRequest),
  423. builder.Eq{"base_repo_id": repo.ID},
  424. func(idx int, bean interface{}) error {
  425. res, err := each(repo, bean.(*models.PullRequest))
  426. results = append(results, res...)
  427. return err
  428. },
  429. )
  430. return results, err
  431. }
  432. func runDoctorHooks(ctx *cli.Context) ([]string, error) {
  433. // Need to iterate across all of the repositories
  434. return iterateRepositories(func(repo *models.Repository) ([]string, error) {
  435. results, err := repository.CheckDelegateHooks(repo.RepoPath())
  436. if err != nil {
  437. return nil, err
  438. }
  439. if len(results) > 0 && ctx.Bool("fix") {
  440. return []string{fmt.Sprintf("regenerated hooks for %s", repo.FullName())}, repository.CreateDelegateHooks(repo.RepoPath())
  441. }
  442. return results, nil
  443. })
  444. }
  445. func runDoctorPRMergeBase(ctx *cli.Context) ([]string, error) {
  446. numRepos := 0
  447. numPRs := 0
  448. numPRsUpdated := 0
  449. results, err := iterateRepositories(func(repo *models.Repository) ([]string, error) {
  450. numRepos++
  451. return iteratePRs(repo, func(repo *models.Repository, pr *models.PullRequest) ([]string, error) {
  452. numPRs++
  453. results := []string{}
  454. pr.BaseRepo = repo
  455. repoPath := repo.RepoPath()
  456. oldMergeBase := pr.MergeBase
  457. if !pr.HasMerged {
  458. var err error
  459. pr.MergeBase, err = git.NewCommand("merge-base", "--", pr.BaseBranch, pr.GetGitRefName()).RunInDir(repoPath)
  460. if err != nil {
  461. var err2 error
  462. pr.MergeBase, err2 = git.NewCommand("rev-parse", git.BranchPrefix+pr.BaseBranch).RunInDir(repoPath)
  463. if err2 != nil {
  464. results = append(results, fmt.Sprintf("WARN: Unable to get merge base for PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  465. log.Error("Unable to get merge base for PR ID %d, Index %d in %s/%s. Error: %v & %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err, err2)
  466. return results, nil
  467. }
  468. }
  469. } else {
  470. parentsString, err := git.NewCommand("rev-list", "--parents", "-n", "1", pr.MergedCommitID).RunInDir(repoPath)
  471. if err != nil {
  472. results = append(results, fmt.Sprintf("WARN: Unable to get parents for merged PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  473. log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
  474. return results, nil
  475. }
  476. parents := strings.Split(strings.TrimSpace(parentsString), " ")
  477. if len(parents) < 2 {
  478. return results, nil
  479. }
  480. args := append([]string{"merge-base", "--"}, parents[1:]...)
  481. args = append(args, pr.GetGitRefName())
  482. pr.MergeBase, err = git.NewCommand(args...).RunInDir(repoPath)
  483. if err != nil {
  484. results = append(results, fmt.Sprintf("WARN: Unable to get merge base for merged PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  485. log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
  486. return results, nil
  487. }
  488. }
  489. pr.MergeBase = strings.TrimSpace(pr.MergeBase)
  490. if pr.MergeBase != oldMergeBase {
  491. if ctx.Bool("fix") {
  492. if err := pr.UpdateCols("merge_base"); err != nil {
  493. return results, err
  494. }
  495. } else {
  496. results = append(results, fmt.Sprintf("#%d onto %s in %s/%s: MergeBase should be %s but is %s", pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, oldMergeBase, pr.MergeBase))
  497. }
  498. numPRsUpdated++
  499. }
  500. return results, nil
  501. })
  502. })
  503. if ctx.Bool("fix") {
  504. results = append(results, fmt.Sprintf("%d PR mergebases updated of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos))
  505. } else {
  506. if numPRsUpdated > 0 && err == nil {
  507. return results, fmt.Errorf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
  508. }
  509. results = append(results, fmt.Sprintf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos))
  510. }
  511. return results, err
  512. }
  513. func runDoctorUserStarNum(ctx *cli.Context) ([]string, error) {
  514. return nil, models.DoctorUserStarNum()
  515. }
  516. func runDoctorScriptType(ctx *cli.Context) ([]string, error) {
  517. path, err := exec.LookPath(setting.ScriptType)
  518. if err != nil {
  519. return []string{fmt.Sprintf("ScriptType %s is not on the current PATH", setting.ScriptType)}, err
  520. }
  521. return []string{fmt.Sprintf("ScriptType %s is on the current PATH at %s", setting.ScriptType, path)}, nil
  522. }
  523. func runDoctorCheckDBConsistency(ctx *cli.Context) ([]string, error) {
  524. var results []string
  525. // make sure DB version is uptodate
  526. if err := models.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil {
  527. return nil, fmt.Errorf("model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
  528. }
  529. //find labels without existing repo or org
  530. count, err := models.CountOrphanedLabels()
  531. if err != nil {
  532. return nil, err
  533. }
  534. if count > 0 {
  535. if ctx.Bool("fix") {
  536. if err = models.DeleteOrphanedLabels(); err != nil {
  537. return nil, err
  538. }
  539. results = append(results, fmt.Sprintf("%d labels without existing repository/organisation deleted", count))
  540. } else {
  541. results = append(results, fmt.Sprintf("%d labels without existing repository/organisation", count))
  542. }
  543. }
  544. //find issues without existing repository
  545. count, err = models.CountOrphanedIssues()
  546. if err != nil {
  547. return nil, err
  548. }
  549. if count > 0 {
  550. if ctx.Bool("fix") {
  551. if err = models.DeleteOrphanedIssues(); err != nil {
  552. return nil, err
  553. }
  554. results = append(results, fmt.Sprintf("%d issues without existing repository deleted", count))
  555. } else {
  556. results = append(results, fmt.Sprintf("%d issues without existing repository", count))
  557. }
  558. }
  559. //find pulls without existing issues
  560. count, err = models.CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
  561. if err != nil {
  562. return nil, err
  563. }
  564. if count > 0 {
  565. if ctx.Bool("fix") {
  566. if err = models.DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id"); err != nil {
  567. return nil, err
  568. }
  569. results = append(results, fmt.Sprintf("%d pull requests without existing issue deleted", count))
  570. } else {
  571. results = append(results, fmt.Sprintf("%d pull requests without existing issue", count))
  572. }
  573. }
  574. //find tracked times without existing issues/pulls
  575. count, err = models.CountOrphanedObjects("tracked_time", "issue", "tracked_time.issue_id=issue.id")
  576. if err != nil {
  577. return nil, err
  578. }
  579. if count > 0 {
  580. if ctx.Bool("fix") {
  581. if err = models.DeleteOrphanedObjects("tracked_time", "issue", "tracked_time.issue_id=issue.id"); err != nil {
  582. return nil, err
  583. }
  584. results = append(results, fmt.Sprintf("%d tracked times without existing issue deleted", count))
  585. } else {
  586. results = append(results, fmt.Sprintf("%d tracked times without existing issue", count))
  587. }
  588. }
  589. count, err = models.CountNullArchivedRepository()
  590. if err != nil {
  591. return nil, err
  592. }
  593. if count > 0 {
  594. if ctx.Bool("fix") {
  595. updatedCount, err := models.FixNullArchivedRepository()
  596. if err != nil {
  597. return nil, err
  598. }
  599. results = append(results, fmt.Sprintf("%d repositories with null is_archived updated", updatedCount))
  600. } else {
  601. results = append(results, fmt.Sprintf("%d repositories with null is_archived", count))
  602. }
  603. }
  604. //ToDo: function to recalc all counters
  605. return results, nil
  606. }
  607. func runDoctorEnablePushOptions(ctx *cli.Context) ([]string, error) {
  608. numRepos := 0
  609. _, err := iterateRepositories(func(repo *models.Repository) ([]string, error) {
  610. numRepos++
  611. r, err := git.OpenRepository(repo.RepoPath())
  612. if err != nil {
  613. return nil, err
  614. }
  615. defer r.Close()
  616. if ctx.Bool("fix") {
  617. _, err := git.NewCommand("config", "receive.advertisePushOptions", "true").RunInDir(r.Path)
  618. return nil, err
  619. }
  620. return nil, nil
  621. })
  622. var prefix string
  623. if !ctx.Bool("fix") {
  624. prefix = "DRY RUN: "
  625. }
  626. return []string{fmt.Sprintf("%sEnabled push options for %d repositories.", prefix, numRepos)}, err
  627. }