Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

setting.go 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "errors"
  8. "fmt"
  9. "io/ioutil"
  10. "net/http"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/models"
  14. "code.gitea.io/gitea/modules/base"
  15. "code.gitea.io/gitea/modules/context"
  16. "code.gitea.io/gitea/modules/git"
  17. "code.gitea.io/gitea/modules/lfs"
  18. "code.gitea.io/gitea/modules/log"
  19. "code.gitea.io/gitea/modules/migrations"
  20. "code.gitea.io/gitea/modules/repository"
  21. "code.gitea.io/gitea/modules/setting"
  22. "code.gitea.io/gitea/modules/structs"
  23. "code.gitea.io/gitea/modules/timeutil"
  24. "code.gitea.io/gitea/modules/typesniffer"
  25. "code.gitea.io/gitea/modules/validation"
  26. "code.gitea.io/gitea/modules/web"
  27. "code.gitea.io/gitea/routers/utils"
  28. "code.gitea.io/gitea/services/forms"
  29. "code.gitea.io/gitea/services/mailer"
  30. mirror_service "code.gitea.io/gitea/services/mirror"
  31. repo_service "code.gitea.io/gitea/services/repository"
  32. )
  33. const (
  34. tplSettingsOptions base.TplName = "repo/settings/options"
  35. tplCollaboration base.TplName = "repo/settings/collaboration"
  36. tplBranches base.TplName = "repo/settings/branches"
  37. tplGithooks base.TplName = "repo/settings/githooks"
  38. tplGithookEdit base.TplName = "repo/settings/githook_edit"
  39. tplDeployKeys base.TplName = "repo/settings/deploy_keys"
  40. tplProtectedBranch base.TplName = "repo/settings/protected_branch"
  41. )
  42. // Settings show a repository's settings page
  43. func Settings(ctx *context.Context) {
  44. ctx.Data["Title"] = ctx.Tr("repo.settings")
  45. ctx.Data["PageIsSettingsOptions"] = true
  46. ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
  47. signing, _ := models.SigningKey(ctx.Repo.Repository.RepoPath())
  48. ctx.Data["SigningKeyAvailable"] = len(signing) > 0
  49. ctx.Data["SigningSettings"] = setting.Repository.Signing
  50. ctx.HTML(http.StatusOK, tplSettingsOptions)
  51. }
  52. // SettingsPost response for changes of a repository
  53. func SettingsPost(ctx *context.Context) {
  54. form := web.GetForm(ctx).(*forms.RepoSettingForm)
  55. ctx.Data["Title"] = ctx.Tr("repo.settings")
  56. ctx.Data["PageIsSettingsOptions"] = true
  57. repo := ctx.Repo.Repository
  58. switch ctx.Query("action") {
  59. case "update":
  60. if ctx.HasError() {
  61. ctx.HTML(http.StatusOK, tplSettingsOptions)
  62. return
  63. }
  64. newRepoName := form.RepoName
  65. // Check if repository name has been changed.
  66. if repo.LowerName != strings.ToLower(newRepoName) {
  67. // Close the GitRepo if open
  68. if ctx.Repo.GitRepo != nil {
  69. ctx.Repo.GitRepo.Close()
  70. ctx.Repo.GitRepo = nil
  71. }
  72. if err := repo_service.ChangeRepositoryName(ctx.User, repo, newRepoName); err != nil {
  73. ctx.Data["Err_RepoName"] = true
  74. switch {
  75. case models.IsErrRepoAlreadyExist(err):
  76. ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
  77. case models.IsErrNameReserved(err):
  78. ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplSettingsOptions, &form)
  79. case models.IsErrRepoFilesAlreadyExist(err):
  80. ctx.Data["Err_RepoName"] = true
  81. switch {
  82. case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
  83. ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
  84. case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
  85. ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
  86. case setting.Repository.AllowDeleteOfUnadoptedRepositories:
  87. ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
  88. default:
  89. ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
  90. }
  91. case models.IsErrNamePatternNotAllowed(err):
  92. ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
  93. default:
  94. ctx.ServerError("ChangeRepositoryName", err)
  95. }
  96. return
  97. }
  98. log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
  99. }
  100. // In case it's just a case change.
  101. repo.Name = newRepoName
  102. repo.LowerName = strings.ToLower(newRepoName)
  103. repo.Description = form.Description
  104. repo.Website = form.Website
  105. repo.IsTemplate = form.Template
  106. // Visibility of forked repository is forced sync with base repository.
  107. if repo.IsFork {
  108. form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
  109. }
  110. visibilityChanged := repo.IsPrivate != form.Private
  111. // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
  112. if visibilityChanged && setting.Repository.ForcePrivate && !form.Private && !ctx.User.IsAdmin {
  113. ctx.ServerError("Force Private enabled", errors.New("cannot change private repository to public"))
  114. return
  115. }
  116. repo.IsPrivate = form.Private
  117. if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
  118. ctx.ServerError("UpdateRepository", err)
  119. return
  120. }
  121. log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  122. ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
  123. ctx.Redirect(repo.Link() + "/settings")
  124. case "mirror":
  125. if !repo.IsMirror {
  126. ctx.NotFound("", nil)
  127. return
  128. }
  129. // This section doesn't require repo_name/RepoName to be set in the form, don't show it
  130. // as an error on the UI for this action
  131. ctx.Data["Err_RepoName"] = nil
  132. interval, err := time.ParseDuration(form.Interval)
  133. if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
  134. ctx.Data["Err_Interval"] = true
  135. ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
  136. } else {
  137. ctx.Repo.Mirror.EnablePrune = form.EnablePrune
  138. ctx.Repo.Mirror.Interval = interval
  139. if interval != 0 {
  140. ctx.Repo.Mirror.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(interval)
  141. } else {
  142. ctx.Repo.Mirror.NextUpdateUnix = 0
  143. }
  144. if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
  145. ctx.Data["Err_Interval"] = true
  146. ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
  147. return
  148. }
  149. }
  150. oldUsername := mirror_service.Username(ctx.Repo.Mirror)
  151. oldPassword := mirror_service.Password(ctx.Repo.Mirror)
  152. if form.MirrorPassword == "" && form.MirrorUsername == oldUsername {
  153. form.MirrorPassword = oldPassword
  154. }
  155. address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
  156. if err == nil {
  157. err = migrations.IsMigrateURLAllowed(address, ctx.User)
  158. }
  159. if err != nil {
  160. ctx.Data["Err_MirrorAddress"] = true
  161. handleSettingRemoteAddrError(ctx, err, form)
  162. return
  163. }
  164. if err := mirror_service.UpdateAddress(ctx.Repo.Mirror, address); err != nil {
  165. ctx.ServerError("UpdateAddress", err)
  166. return
  167. }
  168. form.LFS = form.LFS && setting.LFS.StartServer
  169. if len(form.LFSEndpoint) > 0 {
  170. ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
  171. if ep == nil {
  172. ctx.Data["Err_LFSEndpoint"] = true
  173. ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
  174. return
  175. }
  176. err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
  177. if err != nil {
  178. ctx.Data["Err_LFSEndpoint"] = true
  179. handleSettingRemoteAddrError(ctx, err, form)
  180. return
  181. }
  182. }
  183. ctx.Repo.Mirror.LFS = form.LFS
  184. ctx.Repo.Mirror.LFSEndpoint = form.LFSEndpoint
  185. if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
  186. ctx.ServerError("UpdateMirror", err)
  187. return
  188. }
  189. ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
  190. ctx.Redirect(repo.Link() + "/settings")
  191. case "mirror-sync":
  192. if !repo.IsMirror {
  193. ctx.NotFound("", nil)
  194. return
  195. }
  196. mirror_service.StartToMirror(repo.ID)
  197. ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
  198. ctx.Redirect(repo.Link() + "/settings")
  199. case "advanced":
  200. var repoChanged bool
  201. var units []models.RepoUnit
  202. var deleteUnitTypes []models.UnitType
  203. // This section doesn't require repo_name/RepoName to be set in the form, don't show it
  204. // as an error on the UI for this action
  205. ctx.Data["Err_RepoName"] = nil
  206. if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
  207. repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
  208. repoChanged = true
  209. }
  210. if form.EnableWiki && form.EnableExternalWiki && !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
  211. if !validation.IsValidExternalURL(form.ExternalWikiURL) {
  212. ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
  213. ctx.Redirect(repo.Link() + "/settings")
  214. return
  215. }
  216. units = append(units, models.RepoUnit{
  217. RepoID: repo.ID,
  218. Type: models.UnitTypeExternalWiki,
  219. Config: &models.ExternalWikiConfig{
  220. ExternalWikiURL: form.ExternalWikiURL,
  221. },
  222. })
  223. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
  224. } else if form.EnableWiki && !form.EnableExternalWiki && !models.UnitTypeWiki.UnitGlobalDisabled() {
  225. units = append(units, models.RepoUnit{
  226. RepoID: repo.ID,
  227. Type: models.UnitTypeWiki,
  228. Config: new(models.UnitConfig),
  229. })
  230. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
  231. } else {
  232. if !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
  233. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
  234. }
  235. if !models.UnitTypeWiki.UnitGlobalDisabled() {
  236. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
  237. }
  238. }
  239. if form.EnableIssues && form.EnableExternalTracker && !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
  240. if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
  241. ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
  242. ctx.Redirect(repo.Link() + "/settings")
  243. return
  244. }
  245. if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
  246. ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
  247. ctx.Redirect(repo.Link() + "/settings")
  248. return
  249. }
  250. units = append(units, models.RepoUnit{
  251. RepoID: repo.ID,
  252. Type: models.UnitTypeExternalTracker,
  253. Config: &models.ExternalTrackerConfig{
  254. ExternalTrackerURL: form.ExternalTrackerURL,
  255. ExternalTrackerFormat: form.TrackerURLFormat,
  256. ExternalTrackerStyle: form.TrackerIssueStyle,
  257. },
  258. })
  259. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
  260. } else if form.EnableIssues && !form.EnableExternalTracker && !models.UnitTypeIssues.UnitGlobalDisabled() {
  261. units = append(units, models.RepoUnit{
  262. RepoID: repo.ID,
  263. Type: models.UnitTypeIssues,
  264. Config: &models.IssuesConfig{
  265. EnableTimetracker: form.EnableTimetracker,
  266. AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
  267. EnableDependencies: form.EnableIssueDependencies,
  268. },
  269. })
  270. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
  271. } else {
  272. if !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
  273. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
  274. }
  275. if !models.UnitTypeIssues.UnitGlobalDisabled() {
  276. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
  277. }
  278. }
  279. if form.EnableProjects && !models.UnitTypeProjects.UnitGlobalDisabled() {
  280. units = append(units, models.RepoUnit{
  281. RepoID: repo.ID,
  282. Type: models.UnitTypeProjects,
  283. })
  284. } else if !models.UnitTypeProjects.UnitGlobalDisabled() {
  285. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeProjects)
  286. }
  287. if form.EnablePulls && !models.UnitTypePullRequests.UnitGlobalDisabled() {
  288. units = append(units, models.RepoUnit{
  289. RepoID: repo.ID,
  290. Type: models.UnitTypePullRequests,
  291. Config: &models.PullRequestsConfig{
  292. IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
  293. AllowMerge: form.PullsAllowMerge,
  294. AllowRebase: form.PullsAllowRebase,
  295. AllowRebaseMerge: form.PullsAllowRebaseMerge,
  296. AllowSquash: form.PullsAllowSquash,
  297. AllowManualMerge: form.PullsAllowManualMerge,
  298. AutodetectManualMerge: form.EnableAutodetectManualMerge,
  299. DefaultMergeStyle: models.MergeStyle(form.PullsDefaultMergeStyle),
  300. },
  301. })
  302. } else if !models.UnitTypePullRequests.UnitGlobalDisabled() {
  303. deleteUnitTypes = append(deleteUnitTypes, models.UnitTypePullRequests)
  304. }
  305. if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
  306. ctx.ServerError("UpdateRepositoryUnits", err)
  307. return
  308. }
  309. if repoChanged {
  310. if err := models.UpdateRepository(repo, false); err != nil {
  311. ctx.ServerError("UpdateRepository", err)
  312. return
  313. }
  314. }
  315. log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  316. ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
  317. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  318. case "signing":
  319. changed := false
  320. trustModel := models.ToTrustModel(form.TrustModel)
  321. if trustModel != repo.TrustModel {
  322. repo.TrustModel = trustModel
  323. changed = true
  324. }
  325. if changed {
  326. if err := models.UpdateRepository(repo, false); err != nil {
  327. ctx.ServerError("UpdateRepository", err)
  328. return
  329. }
  330. }
  331. log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  332. ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
  333. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  334. case "admin":
  335. if !ctx.User.IsAdmin {
  336. ctx.Error(http.StatusForbidden)
  337. return
  338. }
  339. if repo.IsFsckEnabled != form.EnableHealthCheck {
  340. repo.IsFsckEnabled = form.EnableHealthCheck
  341. }
  342. if err := models.UpdateRepository(repo, false); err != nil {
  343. ctx.ServerError("UpdateRepository", err)
  344. return
  345. }
  346. log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  347. ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
  348. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  349. case "convert":
  350. if !ctx.Repo.IsOwner() {
  351. ctx.Error(http.StatusNotFound)
  352. return
  353. }
  354. if repo.Name != form.RepoName {
  355. ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
  356. return
  357. }
  358. if !repo.IsMirror {
  359. ctx.Error(http.StatusNotFound)
  360. return
  361. }
  362. repo.IsMirror = false
  363. if _, err := repository.CleanUpMigrateInfo(repo); err != nil {
  364. ctx.ServerError("CleanUpMigrateInfo", err)
  365. return
  366. } else if err = models.DeleteMirrorByRepoID(ctx.Repo.Repository.ID); err != nil {
  367. ctx.ServerError("DeleteMirrorByRepoID", err)
  368. return
  369. }
  370. log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
  371. ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
  372. ctx.Redirect(repo.Link())
  373. case "convert_fork":
  374. if !ctx.Repo.IsOwner() {
  375. ctx.Error(http.StatusNotFound)
  376. return
  377. }
  378. if err := repo.GetOwner(); err != nil {
  379. ctx.ServerError("Convert Fork", err)
  380. return
  381. }
  382. if repo.Name != form.RepoName {
  383. ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
  384. return
  385. }
  386. if !repo.IsFork {
  387. ctx.Error(http.StatusNotFound)
  388. return
  389. }
  390. if !ctx.Repo.Owner.CanCreateRepo() {
  391. ctx.Flash.Error(ctx.Tr("repo.form.reach_limit_of_creation", ctx.User.MaxCreationLimit()))
  392. ctx.Redirect(repo.Link() + "/settings")
  393. return
  394. }
  395. repo.IsFork = false
  396. repo.ForkID = 0
  397. if err := models.UpdateRepository(repo, false); err != nil {
  398. log.Error("Unable to update repository %-v whilst converting from fork", repo)
  399. ctx.ServerError("Convert Fork", err)
  400. return
  401. }
  402. log.Trace("Repository converted from fork to regular: %s", repo.FullName())
  403. ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
  404. ctx.Redirect(repo.Link())
  405. case "transfer":
  406. if !ctx.Repo.IsOwner() {
  407. ctx.Error(http.StatusNotFound)
  408. return
  409. }
  410. if repo.Name != form.RepoName {
  411. ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
  412. return
  413. }
  414. newOwner, err := models.GetUserByName(ctx.Query("new_owner_name"))
  415. if err != nil {
  416. if models.IsErrUserNotExist(err) {
  417. ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
  418. return
  419. }
  420. ctx.ServerError("IsUserExist", err)
  421. return
  422. }
  423. if newOwner.Type == models.UserTypeOrganization {
  424. if !ctx.User.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !newOwner.HasMemberWithUserID(ctx.User.ID) {
  425. // The user shouldn't know about this organization
  426. ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
  427. return
  428. }
  429. }
  430. // Close the GitRepo if open
  431. if ctx.Repo.GitRepo != nil {
  432. ctx.Repo.GitRepo.Close()
  433. ctx.Repo.GitRepo = nil
  434. }
  435. if err := repo_service.StartRepositoryTransfer(ctx.User, newOwner, repo, nil); err != nil {
  436. if models.IsErrRepoAlreadyExist(err) {
  437. ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
  438. } else if models.IsErrRepoTransferInProgress(err) {
  439. ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
  440. } else {
  441. ctx.ServerError("TransferOwnership", err)
  442. }
  443. return
  444. }
  445. log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
  446. ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
  447. ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
  448. case "cancel_transfer":
  449. if !ctx.Repo.IsOwner() {
  450. ctx.Error(http.StatusNotFound)
  451. return
  452. }
  453. repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
  454. if err != nil {
  455. if models.IsErrNoPendingTransfer(err) {
  456. ctx.Flash.Error("repo.settings.transfer_abort_invalid")
  457. ctx.Redirect(ctx.User.HomeLink() + "/" + repo.Name + "/settings")
  458. } else {
  459. ctx.ServerError("GetPendingRepositoryTransfer", err)
  460. }
  461. return
  462. }
  463. if err := repoTransfer.LoadAttributes(); err != nil {
  464. ctx.ServerError("LoadRecipient", err)
  465. return
  466. }
  467. if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
  468. ctx.ServerError("CancelRepositoryTransfer", err)
  469. return
  470. }
  471. log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
  472. ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
  473. ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
  474. case "delete":
  475. if !ctx.Repo.IsOwner() {
  476. ctx.Error(http.StatusNotFound)
  477. return
  478. }
  479. if repo.Name != form.RepoName {
  480. ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
  481. return
  482. }
  483. // Close the gitrepository before doing this.
  484. if ctx.Repo.GitRepo != nil {
  485. ctx.Repo.GitRepo.Close()
  486. }
  487. if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository); err != nil {
  488. ctx.ServerError("DeleteRepository", err)
  489. return
  490. }
  491. log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  492. ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
  493. ctx.Redirect(ctx.Repo.Owner.DashboardLink())
  494. case "delete-wiki":
  495. if !ctx.Repo.IsOwner() {
  496. ctx.Error(http.StatusNotFound)
  497. return
  498. }
  499. if repo.Name != form.RepoName {
  500. ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
  501. return
  502. }
  503. err := repo.DeleteWiki()
  504. if err != nil {
  505. log.Error("Delete Wiki: %v", err.Error())
  506. }
  507. log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  508. ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
  509. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  510. case "archive":
  511. if !ctx.Repo.IsOwner() {
  512. ctx.Error(http.StatusForbidden)
  513. return
  514. }
  515. if repo.IsMirror {
  516. ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
  517. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  518. return
  519. }
  520. if err := repo.SetArchiveRepoState(true); err != nil {
  521. log.Error("Tried to archive a repo: %s", err)
  522. ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
  523. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  524. return
  525. }
  526. ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
  527. log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  528. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  529. case "unarchive":
  530. if !ctx.Repo.IsOwner() {
  531. ctx.Error(http.StatusForbidden)
  532. return
  533. }
  534. if err := repo.SetArchiveRepoState(false); err != nil {
  535. log.Error("Tried to unarchive a repo: %s", err)
  536. ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
  537. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  538. return
  539. }
  540. ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
  541. log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  542. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  543. default:
  544. ctx.NotFound("", nil)
  545. }
  546. }
  547. func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
  548. if models.IsErrInvalidCloneAddr(err) {
  549. addrErr := err.(*models.ErrInvalidCloneAddr)
  550. switch {
  551. case addrErr.IsProtocolInvalid:
  552. ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
  553. case addrErr.IsURLError:
  554. ctx.RenderWithErr(ctx.Tr("form.url_error"), tplSettingsOptions, form)
  555. case addrErr.IsPermissionDenied:
  556. if addrErr.LocalPath {
  557. ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
  558. } else if len(addrErr.PrivateNet) == 0 {
  559. ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
  560. } else {
  561. ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_private_ip"), tplSettingsOptions, form)
  562. }
  563. case addrErr.IsInvalidPath:
  564. ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
  565. default:
  566. ctx.ServerError("Unknown error", err)
  567. }
  568. }
  569. ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
  570. }
  571. // Collaboration render a repository's collaboration page
  572. func Collaboration(ctx *context.Context) {
  573. ctx.Data["Title"] = ctx.Tr("repo.settings")
  574. ctx.Data["PageIsSettingsCollaboration"] = true
  575. users, err := ctx.Repo.Repository.GetCollaborators(models.ListOptions{})
  576. if err != nil {
  577. ctx.ServerError("GetCollaborators", err)
  578. return
  579. }
  580. ctx.Data["Collaborators"] = users
  581. teams, err := ctx.Repo.Repository.GetRepoTeams()
  582. if err != nil {
  583. ctx.ServerError("GetRepoTeams", err)
  584. return
  585. }
  586. ctx.Data["Teams"] = teams
  587. ctx.Data["Repo"] = ctx.Repo.Repository
  588. ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID
  589. ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName
  590. ctx.Data["Org"] = ctx.Repo.Repository.Owner
  591. ctx.Data["Units"] = models.Units
  592. ctx.HTML(http.StatusOK, tplCollaboration)
  593. }
  594. // CollaborationPost response for actions for a collaboration of a repository
  595. func CollaborationPost(ctx *context.Context) {
  596. name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("collaborator")))
  597. if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
  598. ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
  599. return
  600. }
  601. u, err := models.GetUserByName(name)
  602. if err != nil {
  603. if models.IsErrUserNotExist(err) {
  604. ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
  605. ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
  606. } else {
  607. ctx.ServerError("GetUserByName", err)
  608. }
  609. return
  610. }
  611. if !u.IsActive {
  612. ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user"))
  613. ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
  614. return
  615. }
  616. // Organization is not allowed to be added as a collaborator.
  617. if u.IsOrganization() {
  618. ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator"))
  619. ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
  620. return
  621. }
  622. if got, err := ctx.Repo.Repository.IsCollaborator(u.ID); err == nil && got {
  623. ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate"))
  624. ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
  625. return
  626. }
  627. if err = ctx.Repo.Repository.AddCollaborator(u); err != nil {
  628. ctx.ServerError("AddCollaborator", err)
  629. return
  630. }
  631. if setting.Service.EnableNotifyMail {
  632. mailer.SendCollaboratorMail(u, ctx.User, ctx.Repo.Repository)
  633. }
  634. ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success"))
  635. ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
  636. }
  637. // ChangeCollaborationAccessMode response for changing access of a collaboration
  638. func ChangeCollaborationAccessMode(ctx *context.Context) {
  639. if err := ctx.Repo.Repository.ChangeCollaborationAccessMode(
  640. ctx.QueryInt64("uid"),
  641. models.AccessMode(ctx.QueryInt("mode"))); err != nil {
  642. log.Error("ChangeCollaborationAccessMode: %v", err)
  643. }
  644. }
  645. // DeleteCollaboration delete a collaboration for a repository
  646. func DeleteCollaboration(ctx *context.Context) {
  647. if err := ctx.Repo.Repository.DeleteCollaboration(ctx.QueryInt64("id")); err != nil {
  648. ctx.Flash.Error("DeleteCollaboration: " + err.Error())
  649. } else {
  650. ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
  651. }
  652. ctx.JSON(http.StatusOK, map[string]interface{}{
  653. "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
  654. })
  655. }
  656. // AddTeamPost response for adding a team to a repository
  657. func AddTeamPost(ctx *context.Context) {
  658. if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
  659. ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
  660. ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
  661. return
  662. }
  663. name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("team")))
  664. if len(name) == 0 {
  665. ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
  666. return
  667. }
  668. team, err := ctx.Repo.Owner.GetTeam(name)
  669. if err != nil {
  670. if models.IsErrTeamNotExist(err) {
  671. ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
  672. ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
  673. } else {
  674. ctx.ServerError("GetTeam", err)
  675. }
  676. return
  677. }
  678. if team.OrgID != ctx.Repo.Repository.OwnerID {
  679. ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization"))
  680. ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
  681. return
  682. }
  683. if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) {
  684. ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate"))
  685. ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
  686. return
  687. }
  688. if err = team.AddRepository(ctx.Repo.Repository); err != nil {
  689. ctx.ServerError("team.AddRepository", err)
  690. return
  691. }
  692. ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success"))
  693. ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
  694. }
  695. // DeleteTeam response for deleting a team from a repository
  696. func DeleteTeam(ctx *context.Context) {
  697. if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
  698. ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
  699. ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
  700. return
  701. }
  702. team, err := models.GetTeamByID(ctx.QueryInt64("id"))
  703. if err != nil {
  704. ctx.ServerError("GetTeamByID", err)
  705. return
  706. }
  707. if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil {
  708. ctx.ServerError("team.RemoveRepositorys", err)
  709. return
  710. }
  711. ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success"))
  712. ctx.JSON(http.StatusOK, map[string]interface{}{
  713. "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
  714. })
  715. }
  716. // parseOwnerAndRepo get repos by owner
  717. func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
  718. owner, err := models.GetUserByName(ctx.Params(":username"))
  719. if err != nil {
  720. if models.IsErrUserNotExist(err) {
  721. ctx.NotFound("GetUserByName", err)
  722. } else {
  723. ctx.ServerError("GetUserByName", err)
  724. }
  725. return nil, nil
  726. }
  727. repo, err := models.GetRepositoryByName(owner.ID, ctx.Params(":reponame"))
  728. if err != nil {
  729. if models.IsErrRepoNotExist(err) {
  730. ctx.NotFound("GetRepositoryByName", err)
  731. } else {
  732. ctx.ServerError("GetRepositoryByName", err)
  733. }
  734. return nil, nil
  735. }
  736. return owner, repo
  737. }
  738. // GitHooks hooks of a repository
  739. func GitHooks(ctx *context.Context) {
  740. ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
  741. ctx.Data["PageIsSettingsGitHooks"] = true
  742. hooks, err := ctx.Repo.GitRepo.Hooks()
  743. if err != nil {
  744. ctx.ServerError("Hooks", err)
  745. return
  746. }
  747. ctx.Data["Hooks"] = hooks
  748. ctx.HTML(http.StatusOK, tplGithooks)
  749. }
  750. // GitHooksEdit render for editing a hook of repository page
  751. func GitHooksEdit(ctx *context.Context) {
  752. ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
  753. ctx.Data["PageIsSettingsGitHooks"] = true
  754. name := ctx.Params(":name")
  755. hook, err := ctx.Repo.GitRepo.GetHook(name)
  756. if err != nil {
  757. if err == git.ErrNotValidHook {
  758. ctx.NotFound("GetHook", err)
  759. } else {
  760. ctx.ServerError("GetHook", err)
  761. }
  762. return
  763. }
  764. ctx.Data["Hook"] = hook
  765. ctx.HTML(http.StatusOK, tplGithookEdit)
  766. }
  767. // GitHooksEditPost response for editing a git hook of a repository
  768. func GitHooksEditPost(ctx *context.Context) {
  769. name := ctx.Params(":name")
  770. hook, err := ctx.Repo.GitRepo.GetHook(name)
  771. if err != nil {
  772. if err == git.ErrNotValidHook {
  773. ctx.NotFound("GetHook", err)
  774. } else {
  775. ctx.ServerError("GetHook", err)
  776. }
  777. return
  778. }
  779. hook.Content = ctx.Query("content")
  780. if err = hook.Update(); err != nil {
  781. ctx.ServerError("hook.Update", err)
  782. return
  783. }
  784. ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
  785. }
  786. // DeployKeys render the deploy keys list of a repository page
  787. func DeployKeys(ctx *context.Context) {
  788. ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
  789. ctx.Data["PageIsSettingsKeys"] = true
  790. ctx.Data["DisableSSH"] = setting.SSH.Disabled
  791. keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
  792. if err != nil {
  793. ctx.ServerError("ListDeployKeys", err)
  794. return
  795. }
  796. ctx.Data["Deploykeys"] = keys
  797. ctx.HTML(http.StatusOK, tplDeployKeys)
  798. }
  799. // DeployKeysPost response for adding a deploy key of a repository
  800. func DeployKeysPost(ctx *context.Context) {
  801. form := web.GetForm(ctx).(*forms.AddKeyForm)
  802. ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
  803. ctx.Data["PageIsSettingsKeys"] = true
  804. keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
  805. if err != nil {
  806. ctx.ServerError("ListDeployKeys", err)
  807. return
  808. }
  809. ctx.Data["Deploykeys"] = keys
  810. if ctx.HasError() {
  811. ctx.HTML(http.StatusOK, tplDeployKeys)
  812. return
  813. }
  814. content, err := models.CheckPublicKeyString(form.Content)
  815. if err != nil {
  816. if models.IsErrSSHDisabled(err) {
  817. ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
  818. } else if models.IsErrKeyUnableVerify(err) {
  819. ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
  820. } else {
  821. ctx.Data["HasError"] = true
  822. ctx.Data["Err_Content"] = true
  823. ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
  824. }
  825. ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
  826. return
  827. }
  828. key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
  829. if err != nil {
  830. ctx.Data["HasError"] = true
  831. switch {
  832. case models.IsErrDeployKeyAlreadyExist(err):
  833. ctx.Data["Err_Content"] = true
  834. ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
  835. case models.IsErrKeyAlreadyExist(err):
  836. ctx.Data["Err_Content"] = true
  837. ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
  838. case models.IsErrKeyNameAlreadyUsed(err):
  839. ctx.Data["Err_Title"] = true
  840. ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
  841. case models.IsErrDeployKeyNameAlreadyUsed(err):
  842. ctx.Data["Err_Title"] = true
  843. ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
  844. default:
  845. ctx.ServerError("AddDeployKey", err)
  846. }
  847. return
  848. }
  849. log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID)
  850. ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
  851. ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
  852. }
  853. // DeleteDeployKey response for deleting a deploy key
  854. func DeleteDeployKey(ctx *context.Context) {
  855. if err := models.DeleteDeployKey(ctx.User, ctx.QueryInt64("id")); err != nil {
  856. ctx.Flash.Error("DeleteDeployKey: " + err.Error())
  857. } else {
  858. ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
  859. }
  860. ctx.JSON(http.StatusOK, map[string]interface{}{
  861. "redirect": ctx.Repo.RepoLink + "/settings/keys",
  862. })
  863. }
  864. // UpdateAvatarSetting update repo's avatar
  865. func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
  866. ctxRepo := ctx.Repo.Repository
  867. if form.Avatar == nil {
  868. // No avatar is uploaded and we not removing it here.
  869. // No random avatar generated here.
  870. // Just exit, no action.
  871. if ctxRepo.CustomAvatarRelativePath() == "" {
  872. log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
  873. }
  874. return nil
  875. }
  876. r, err := form.Avatar.Open()
  877. if err != nil {
  878. return fmt.Errorf("Avatar.Open: %v", err)
  879. }
  880. defer r.Close()
  881. if form.Avatar.Size > setting.Avatar.MaxFileSize {
  882. return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
  883. }
  884. data, err := ioutil.ReadAll(r)
  885. if err != nil {
  886. return fmt.Errorf("ioutil.ReadAll: %v", err)
  887. }
  888. st := typesniffer.DetectContentType(data)
  889. if !(st.IsImage() && !st.IsSvgImage()) {
  890. return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
  891. }
  892. if err = ctxRepo.UploadAvatar(data); err != nil {
  893. return fmt.Errorf("UploadAvatar: %v", err)
  894. }
  895. return nil
  896. }
  897. // SettingsAvatar save new POSTed repository avatar
  898. func SettingsAvatar(ctx *context.Context) {
  899. form := web.GetForm(ctx).(*forms.AvatarForm)
  900. form.Source = forms.AvatarLocal
  901. if err := UpdateAvatarSetting(ctx, *form); err != nil {
  902. ctx.Flash.Error(err.Error())
  903. } else {
  904. ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success"))
  905. }
  906. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  907. }
  908. // SettingsDeleteAvatar delete repository avatar
  909. func SettingsDeleteAvatar(ctx *context.Context) {
  910. if err := ctx.Repo.Repository.DeleteAvatar(); err != nil {
  911. ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
  912. }
  913. ctx.Redirect(ctx.Repo.RepoLink + "/settings")
  914. }