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.

setting.go 28KB

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