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 26KB

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