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
6package repo
7
8import (
9	"errors"
10	"fmt"
11	"io"
12	"net/http"
13	"strconv"
14	"strings"
15	"time"
16
17	"code.gitea.io/gitea/models"
18	asymkey_model "code.gitea.io/gitea/models/asymkey"
19	"code.gitea.io/gitea/models/db"
20	"code.gitea.io/gitea/models/perm"
21	repo_model "code.gitea.io/gitea/models/repo"
22	unit_model "code.gitea.io/gitea/models/unit"
23	user_model "code.gitea.io/gitea/models/user"
24	"code.gitea.io/gitea/modules/base"
25	"code.gitea.io/gitea/modules/context"
26	"code.gitea.io/gitea/modules/git"
27	"code.gitea.io/gitea/modules/indexer/code"
28	"code.gitea.io/gitea/modules/indexer/stats"
29	"code.gitea.io/gitea/modules/lfs"
30	"code.gitea.io/gitea/modules/log"
31	"code.gitea.io/gitea/modules/repository"
32	"code.gitea.io/gitea/modules/setting"
33	"code.gitea.io/gitea/modules/structs"
34	"code.gitea.io/gitea/modules/timeutil"
35	"code.gitea.io/gitea/modules/typesniffer"
36	"code.gitea.io/gitea/modules/util"
37	"code.gitea.io/gitea/modules/validation"
38	"code.gitea.io/gitea/modules/web"
39	"code.gitea.io/gitea/routers/utils"
40	asymkey_service "code.gitea.io/gitea/services/asymkey"
41	"code.gitea.io/gitea/services/forms"
42	"code.gitea.io/gitea/services/mailer"
43	"code.gitea.io/gitea/services/migrations"
44	mirror_service "code.gitea.io/gitea/services/mirror"
45	repo_service "code.gitea.io/gitea/services/repository"
46	wiki_service "code.gitea.io/gitea/services/wiki"
47)
48
49const (
50	tplSettingsOptions base.TplName = "repo/settings/options"
51	tplCollaboration   base.TplName = "repo/settings/collaboration"
52	tplBranches        base.TplName = "repo/settings/branches"
53	tplTags            base.TplName = "repo/settings/tags"
54	tplGithooks        base.TplName = "repo/settings/githooks"
55	tplGithookEdit     base.TplName = "repo/settings/githook_edit"
56	tplDeployKeys      base.TplName = "repo/settings/deploy_keys"
57	tplProtectedBranch base.TplName = "repo/settings/protected_branch"
58)
59
60// Settings show a repository's settings page
61func Settings(ctx *context.Context) {
62	ctx.Data["Title"] = ctx.Tr("repo.settings")
63	ctx.Data["PageIsSettingsOptions"] = true
64	ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
65	ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
66	ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
67	ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
68
69	signing, _ := asymkey_service.SigningKey(ctx.Repo.Repository.RepoPath())
70	ctx.Data["SigningKeyAvailable"] = len(signing) > 0
71	ctx.Data["SigningSettings"] = setting.Repository.Signing
72	ctx.Data["CodeIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
73	if ctx.User.IsAdmin {
74		if setting.Indexer.RepoIndexerEnabled {
75			status, err := repo_model.GetIndexerStatus(ctx.Repo.Repository, repo_model.RepoIndexerTypeCode)
76			if err != nil {
77				ctx.ServerError("repo.indexer_status", err)
78				return
79			}
80			ctx.Data["CodeIndexerStatus"] = status
81		}
82		status, err := repo_model.GetIndexerStatus(ctx.Repo.Repository, repo_model.RepoIndexerTypeStats)
83		if err != nil {
84			ctx.ServerError("repo.indexer_status", err)
85			return
86		}
87		ctx.Data["StatsIndexerStatus"] = status
88	}
89	pushMirrors, err := repo_model.GetPushMirrorsByRepoID(ctx.Repo.Repository.ID)
90	if err != nil {
91		ctx.ServerError("GetPushMirrorsByRepoID", err)
92		return
93	}
94	ctx.Data["PushMirrors"] = pushMirrors
95
96	ctx.HTML(http.StatusOK, tplSettingsOptions)
97}
98
99// SettingsPost response for changes of a repository
100func SettingsPost(ctx *context.Context) {
101	form := web.GetForm(ctx).(*forms.RepoSettingForm)
102	ctx.Data["Title"] = ctx.Tr("repo.settings")
103	ctx.Data["PageIsSettingsOptions"] = true
104
105	repo := ctx.Repo.Repository
106
107	switch ctx.FormString("action") {
108	case "update":
109		if ctx.HasError() {
110			ctx.HTML(http.StatusOK, tplSettingsOptions)
111			return
112		}
113
114		newRepoName := form.RepoName
115		// Check if repository name has been changed.
116		if repo.LowerName != strings.ToLower(newRepoName) {
117			// Close the GitRepo if open
118			if ctx.Repo.GitRepo != nil {
119				ctx.Repo.GitRepo.Close()
120				ctx.Repo.GitRepo = nil
121			}
122			if err := repo_service.ChangeRepositoryName(ctx.User, repo, newRepoName); err != nil {
123				ctx.Data["Err_RepoName"] = true
124				switch {
125				case repo_model.IsErrRepoAlreadyExist(err):
126					ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
127				case db.IsErrNameReserved(err):
128					ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
129				case repo_model.IsErrRepoFilesAlreadyExist(err):
130					ctx.Data["Err_RepoName"] = true
131					switch {
132					case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
133						ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
134					case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
135						ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
136					case setting.Repository.AllowDeleteOfUnadoptedRepositories:
137						ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
138					default:
139						ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
140					}
141				case db.IsErrNamePatternNotAllowed(err):
142					ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
143				default:
144					ctx.ServerError("ChangeRepositoryName", err)
145				}
146				return
147			}
148
149			log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
150		}
151		// In case it's just a case change.
152		repo.Name = newRepoName
153		repo.LowerName = strings.ToLower(newRepoName)
154		repo.Description = form.Description
155		repo.Website = form.Website
156		repo.IsTemplate = form.Template
157
158		// Visibility of forked repository is forced sync with base repository.
159		if repo.IsFork {
160			form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
161		}
162
163		visibilityChanged := repo.IsPrivate != form.Private
164		// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
165		if visibilityChanged && setting.Repository.ForcePrivate && !form.Private && !ctx.User.IsAdmin {
166			ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
167			return
168		}
169
170		repo.IsPrivate = form.Private
171		if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
172			ctx.ServerError("UpdateRepository", err)
173			return
174		}
175		log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
176
177		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
178		ctx.Redirect(repo.Link() + "/settings")
179
180	case "mirror":
181		if !setting.Mirror.Enabled || !repo.IsMirror {
182			ctx.NotFound("", nil)
183			return
184		}
185
186		// This section doesn't require repo_name/RepoName to be set in the form, don't show it
187		// as an error on the UI for this action
188		ctx.Data["Err_RepoName"] = nil
189
190		interval, err := time.ParseDuration(form.Interval)
191		if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
192			ctx.Data["Err_Interval"] = true
193			ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
194		} else {
195			ctx.Repo.Mirror.EnablePrune = form.EnablePrune
196			ctx.Repo.Mirror.Interval = interval
197			if interval != 0 {
198				ctx.Repo.Mirror.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(interval)
199			} else {
200				ctx.Repo.Mirror.NextUpdateUnix = 0
201			}
202			if err := repo_model.UpdateMirror(ctx.Repo.Mirror); err != nil {
203				ctx.Data["Err_Interval"] = true
204				ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
205				return
206			}
207		}
208
209		u, _ := git.GetRemoteAddress(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Mirror.GetRemoteName())
210		if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
211			form.MirrorPassword, _ = u.User.Password()
212		}
213
214		address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
215		if err == nil {
216			err = migrations.IsMigrateURLAllowed(address, ctx.User)
217		}
218		if err != nil {
219			ctx.Data["Err_MirrorAddress"] = true
220			handleSettingRemoteAddrError(ctx, err, form)
221			return
222		}
223
224		if err := mirror_service.UpdateAddress(ctx.Repo.Mirror, address); err != nil {
225			ctx.ServerError("UpdateAddress", err)
226			return
227		}
228
229		form.LFS = form.LFS && setting.LFS.StartServer
230
231		if len(form.LFSEndpoint) > 0 {
232			ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
233			if ep == nil {
234				ctx.Data["Err_LFSEndpoint"] = true
235				ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
236				return
237			}
238			err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
239			if err != nil {
240				ctx.Data["Err_LFSEndpoint"] = true
241				handleSettingRemoteAddrError(ctx, err, form)
242				return
243			}
244		}
245
246		ctx.Repo.Mirror.LFS = form.LFS
247		ctx.Repo.Mirror.LFSEndpoint = form.LFSEndpoint
248		if err := repo_model.UpdateMirror(ctx.Repo.Mirror); err != nil {
249			ctx.ServerError("UpdateMirror", err)
250			return
251		}
252
253		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
254		ctx.Redirect(repo.Link() + "/settings")
255
256	case "mirror-sync":
257		if !setting.Mirror.Enabled || !repo.IsMirror {
258			ctx.NotFound("", nil)
259			return
260		}
261
262		mirror_service.StartToMirror(repo.ID)
263
264		ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
265		ctx.Redirect(repo.Link() + "/settings")
266
267	case "push-mirror-sync":
268		if !setting.Mirror.Enabled {
269			ctx.NotFound("", nil)
270			return
271		}
272
273		m, err := selectPushMirrorByForm(form, repo)
274		if err != nil {
275			ctx.NotFound("", nil)
276			return
277		}
278
279		mirror_service.AddPushMirrorToQueue(m.ID)
280
281		ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
282		ctx.Redirect(repo.Link() + "/settings")
283
284	case "push-mirror-remove":
285		if !setting.Mirror.Enabled {
286			ctx.NotFound("", nil)
287			return
288		}
289
290		// This section doesn't require repo_name/RepoName to be set in the form, don't show it
291		// as an error on the UI for this action
292		ctx.Data["Err_RepoName"] = nil
293
294		m, err := selectPushMirrorByForm(form, repo)
295		if err != nil {
296			ctx.NotFound("", nil)
297			return
298		}
299
300		if err = mirror_service.RemovePushMirrorRemote(m); err != nil {
301			ctx.ServerError("RemovePushMirrorRemote", err)
302			return
303		}
304
305		if err = repo_model.DeletePushMirrorByID(m.ID); err != nil {
306			ctx.ServerError("DeletePushMirrorByID", err)
307			return
308		}
309
310		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
311		ctx.Redirect(repo.Link() + "/settings")
312
313	case "push-mirror-add":
314		if setting.Mirror.DisableNewPush {
315			ctx.NotFound("", nil)
316			return
317		}
318
319		// This section doesn't require repo_name/RepoName to be set in the form, don't show it
320		// as an error on the UI for this action
321		ctx.Data["Err_RepoName"] = nil
322
323		interval, err := time.ParseDuration(form.PushMirrorInterval)
324		if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
325			ctx.Data["Err_PushMirrorInterval"] = true
326			ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
327			return
328		}
329
330		address, err := forms.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword)
331		if err == nil {
332			err = migrations.IsMigrateURLAllowed(address, ctx.User)
333		}
334		if err != nil {
335			ctx.Data["Err_PushMirrorAddress"] = true
336			handleSettingRemoteAddrError(ctx, err, form)
337			return
338		}
339
340		remoteSuffix, err := util.RandomString(10)
341		if err != nil {
342			ctx.ServerError("RandomString", err)
343			return
344		}
345
346		m := &repo_model.PushMirror{
347			RepoID:     repo.ID,
348			Repo:       repo,
349			RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix),
350			Interval:   interval,
351		}
352		if err := repo_model.InsertPushMirror(m); err != nil {
353			ctx.ServerError("InsertPushMirror", err)
354			return
355		}
356
357		if err := mirror_service.AddPushMirrorRemote(m, address); err != nil {
358			if err := repo_model.DeletePushMirrorByID(m.ID); err != nil {
359				log.Error("DeletePushMirrorByID %v", err)
360			}
361			ctx.ServerError("AddPushMirrorRemote", err)
362			return
363		}
364
365		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
366		ctx.Redirect(repo.Link() + "/settings")
367
368	case "advanced":
369		var repoChanged bool
370		var units []repo_model.RepoUnit
371		var deleteUnitTypes []unit_model.Type
372
373		// This section doesn't require repo_name/RepoName to be set in the form, don't show it
374		// as an error on the UI for this action
375		ctx.Data["Err_RepoName"] = nil
376
377		if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
378			repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
379			repoChanged = true
380		}
381
382		if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
383			if !validation.IsValidExternalURL(form.ExternalWikiURL) {
384				ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
385				ctx.Redirect(repo.Link() + "/settings")
386				return
387			}
388
389			units = append(units, repo_model.RepoUnit{
390				RepoID: repo.ID,
391				Type:   unit_model.TypeExternalWiki,
392				Config: &repo_model.ExternalWikiConfig{
393					ExternalWikiURL: form.ExternalWikiURL,
394				},
395			})
396			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
397		} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
398			units = append(units, repo_model.RepoUnit{
399				RepoID: repo.ID,
400				Type:   unit_model.TypeWiki,
401				Config: new(repo_model.UnitConfig),
402			})
403			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
404		} else {
405			if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
406				deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
407			}
408			if !unit_model.TypeWiki.UnitGlobalDisabled() {
409				deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
410			}
411		}
412
413		if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
414			if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
415				ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
416				ctx.Redirect(repo.Link() + "/settings")
417				return
418			}
419			if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
420				ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
421				ctx.Redirect(repo.Link() + "/settings")
422				return
423			}
424			units = append(units, repo_model.RepoUnit{
425				RepoID: repo.ID,
426				Type:   unit_model.TypeExternalTracker,
427				Config: &repo_model.ExternalTrackerConfig{
428					ExternalTrackerURL:    form.ExternalTrackerURL,
429					ExternalTrackerFormat: form.TrackerURLFormat,
430					ExternalTrackerStyle:  form.TrackerIssueStyle,
431				},
432			})
433			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
434		} else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
435			units = append(units, repo_model.RepoUnit{
436				RepoID: repo.ID,
437				Type:   unit_model.TypeIssues,
438				Config: &repo_model.IssuesConfig{
439					EnableTimetracker:                form.EnableTimetracker,
440					AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
441					EnableDependencies:               form.EnableIssueDependencies,
442				},
443			})
444			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
445		} else {
446			if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
447				deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
448			}
449			if !unit_model.TypeIssues.UnitGlobalDisabled() {
450				deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
451			}
452		}
453
454		if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
455			units = append(units, repo_model.RepoUnit{
456				RepoID: repo.ID,
457				Type:   unit_model.TypeProjects,
458			})
459		} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
460			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
461		}
462
463		if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
464			units = append(units, repo_model.RepoUnit{
465				RepoID: repo.ID,
466				Type:   unit_model.TypePullRequests,
467				Config: &repo_model.PullRequestsConfig{
468					IgnoreWhitespaceConflicts:     form.PullsIgnoreWhitespace,
469					AllowMerge:                    form.PullsAllowMerge,
470					AllowRebase:                   form.PullsAllowRebase,
471					AllowRebaseMerge:              form.PullsAllowRebaseMerge,
472					AllowSquash:                   form.PullsAllowSquash,
473					AllowManualMerge:              form.PullsAllowManualMerge,
474					AutodetectManualMerge:         form.EnableAutodetectManualMerge,
475					DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
476					DefaultMergeStyle:             repo_model.MergeStyle(form.PullsDefaultMergeStyle),
477				},
478			})
479		} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
480			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
481		}
482
483		if err := repo_model.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
484			ctx.ServerError("UpdateRepositoryUnits", err)
485			return
486		}
487		if repoChanged {
488			if err := models.UpdateRepository(repo, false); err != nil {
489				ctx.ServerError("UpdateRepository", err)
490				return
491			}
492		}
493		log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
494
495		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
496		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
497
498	case "signing":
499		changed := false
500		trustModel := repo_model.ToTrustModel(form.TrustModel)
501		if trustModel != repo.TrustModel {
502			repo.TrustModel = trustModel
503			changed = true
504		}
505
506		if changed {
507			if err := models.UpdateRepository(repo, false); err != nil {
508				ctx.ServerError("UpdateRepository", err)
509				return
510			}
511		}
512		log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
513
514		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
515		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
516
517	case "admin":
518		if !ctx.User.IsAdmin {
519			ctx.Error(http.StatusForbidden)
520			return
521		}
522
523		if repo.IsFsckEnabled != form.EnableHealthCheck {
524			repo.IsFsckEnabled = form.EnableHealthCheck
525		}
526
527		if err := models.UpdateRepository(repo, false); err != nil {
528			ctx.ServerError("UpdateRepository", err)
529			return
530		}
531
532		log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
533
534		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
535		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
536
537	case "admin_index":
538		if !ctx.User.IsAdmin {
539			ctx.Error(http.StatusForbidden)
540			return
541		}
542
543		switch form.RequestReindexType {
544		case "stats":
545			if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil {
546				ctx.ServerError("UpdateStatsRepondexer", err)
547				return
548			}
549		case "code":
550			if !setting.Indexer.RepoIndexerEnabled {
551				ctx.Error(http.StatusForbidden)
552				return
553			}
554			code.UpdateRepoIndexer(ctx.Repo.Repository)
555		default:
556			ctx.NotFound("", nil)
557			return
558		}
559
560		log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name)
561
562		ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
563		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
564
565	case "convert":
566		if !ctx.Repo.IsOwner() {
567			ctx.Error(http.StatusNotFound)
568			return
569		}
570		if repo.Name != form.RepoName {
571			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
572			return
573		}
574
575		if !repo.IsMirror {
576			ctx.Error(http.StatusNotFound)
577			return
578		}
579		repo.IsMirror = false
580
581		if _, err := repository.CleanUpMigrateInfo(repo); err != nil {
582			ctx.ServerError("CleanUpMigrateInfo", err)
583			return
584		} else if err = repo_model.DeleteMirrorByRepoID(ctx.Repo.Repository.ID); err != nil {
585			ctx.ServerError("DeleteMirrorByRepoID", err)
586			return
587		}
588		log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
589		ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
590		ctx.Redirect(repo.Link())
591
592	case "convert_fork":
593		if !ctx.Repo.IsOwner() {
594			ctx.Error(http.StatusNotFound)
595			return
596		}
597		if err := repo.GetOwner(db.DefaultContext); err != nil {
598			ctx.ServerError("Convert Fork", err)
599			return
600		}
601		if repo.Name != form.RepoName {
602			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
603			return
604		}
605
606		if !repo.IsFork {
607			ctx.Error(http.StatusNotFound)
608			return
609		}
610
611		if !ctx.Repo.Owner.CanCreateRepo() {
612			maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
613			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
614			ctx.Flash.Error(msg)
615			ctx.Redirect(repo.Link() + "/settings")
616			return
617		}
618
619		if err := repo_service.ConvertForkToNormalRepository(repo); err != nil {
620			log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err)
621			ctx.ServerError("Convert Fork", err)
622			return
623		}
624
625		log.Trace("Repository converted from fork to regular: %s", repo.FullName())
626		ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
627		ctx.Redirect(repo.Link())
628
629	case "transfer":
630		if !ctx.Repo.IsOwner() {
631			ctx.Error(http.StatusNotFound)
632			return
633		}
634		if repo.Name != form.RepoName {
635			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
636			return
637		}
638
639		newOwner, err := user_model.GetUserByName(ctx.FormString("new_owner_name"))
640		if err != nil {
641			if user_model.IsErrUserNotExist(err) {
642				ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
643				return
644			}
645			ctx.ServerError("IsUserExist", err)
646			return
647		}
648
649		if newOwner.Type == user_model.UserTypeOrganization {
650			if !ctx.User.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !models.OrgFromUser(newOwner).HasMemberWithUserID(ctx.User.ID) {
651				// The user shouldn't know about this organization
652				ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
653				return
654			}
655		}
656
657		// Close the GitRepo if open
658		if ctx.Repo.GitRepo != nil {
659			ctx.Repo.GitRepo.Close()
660			ctx.Repo.GitRepo = nil
661		}
662
663		if err := repo_service.StartRepositoryTransfer(ctx.User, newOwner, repo, nil); err != nil {
664			if repo_model.IsErrRepoAlreadyExist(err) {
665				ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
666			} else if models.IsErrRepoTransferInProgress(err) {
667				ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
668			} else {
669				ctx.ServerError("TransferOwnership", err)
670			}
671
672			return
673		}
674
675		log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
676		ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
677		ctx.Redirect(repo.Link() + "/settings")
678
679	case "cancel_transfer":
680		if !ctx.Repo.IsOwner() {
681			ctx.Error(http.StatusNotFound)
682			return
683		}
684
685		repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
686		if err != nil {
687			if models.IsErrNoPendingTransfer(err) {
688				ctx.Flash.Error("repo.settings.transfer_abort_invalid")
689				ctx.Redirect(repo.Link() + "/settings")
690			} else {
691				ctx.ServerError("GetPendingRepositoryTransfer", err)
692			}
693
694			return
695		}
696
697		if err := repoTransfer.LoadAttributes(); err != nil {
698			ctx.ServerError("LoadRecipient", err)
699			return
700		}
701
702		if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
703			ctx.ServerError("CancelRepositoryTransfer", err)
704			return
705		}
706
707		log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
708		ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
709		ctx.Redirect(repo.Link() + "/settings")
710
711	case "delete":
712		if !ctx.Repo.IsOwner() {
713			ctx.Error(http.StatusNotFound)
714			return
715		}
716		if repo.Name != form.RepoName {
717			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
718			return
719		}
720
721		// Close the gitrepository before doing this.
722		if ctx.Repo.GitRepo != nil {
723			ctx.Repo.GitRepo.Close()
724		}
725
726		if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository, true); err != nil {
727			ctx.ServerError("DeleteRepository", err)
728			return
729		}
730		log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
731
732		ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
733		ctx.Redirect(ctx.Repo.Owner.DashboardLink())
734
735	case "delete-wiki":
736		if !ctx.Repo.IsOwner() {
737			ctx.Error(http.StatusNotFound)
738			return
739		}
740		if repo.Name != form.RepoName {
741			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
742			return
743		}
744
745		err := wiki_service.DeleteWiki(repo)
746		if err != nil {
747			log.Error("Delete Wiki: %v", err.Error())
748		}
749		log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
750
751		ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
752		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
753
754	case "archive":
755		if !ctx.Repo.IsOwner() {
756			ctx.Error(http.StatusForbidden)
757			return
758		}
759
760		if repo.IsMirror {
761			ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
762			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
763			return
764		}
765
766		if err := repo_model.SetArchiveRepoState(repo, true); err != nil {
767			log.Error("Tried to archive a repo: %s", err)
768			ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
769			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
770			return
771		}
772
773		ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
774
775		log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
776		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
777
778	case "unarchive":
779		if !ctx.Repo.IsOwner() {
780			ctx.Error(http.StatusForbidden)
781			return
782		}
783
784		if err := repo_model.SetArchiveRepoState(repo, false); err != nil {
785			log.Error("Tried to unarchive a repo: %s", err)
786			ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
787			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
788			return
789		}
790
791		ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
792
793		log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
794		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
795
796	default:
797		ctx.NotFound("", nil)
798	}
799}
800
801func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
802	if models.IsErrInvalidCloneAddr(err) {
803		addrErr := err.(*models.ErrInvalidCloneAddr)
804		switch {
805		case addrErr.IsProtocolInvalid:
806			ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
807		case addrErr.IsURLError:
808			ctx.RenderWithErr(ctx.Tr("form.url_error"), tplSettingsOptions, form)
809		case addrErr.IsPermissionDenied:
810			if addrErr.LocalPath {
811				ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
812			} else {
813				ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
814			}
815		case addrErr.IsInvalidPath:
816			ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
817		default:
818			ctx.ServerError("Unknown error", err)
819		}
820		return
821	}
822	ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
823}
824
825// Collaboration render a repository's collaboration page
826func Collaboration(ctx *context.Context) {
827	ctx.Data["Title"] = ctx.Tr("repo.settings")
828	ctx.Data["PageIsSettingsCollaboration"] = true
829
830	users, err := models.GetCollaborators(ctx.Repo.Repository.ID, db.ListOptions{})
831	if err != nil {
832		ctx.ServerError("GetCollaborators", err)
833		return
834	}
835	ctx.Data["Collaborators"] = users
836
837	teams, err := models.GetRepoTeams(ctx.Repo.Repository)
838	if err != nil {
839		ctx.ServerError("GetRepoTeams", err)
840		return
841	}
842	ctx.Data["Teams"] = teams
843	ctx.Data["Repo"] = ctx.Repo.Repository
844	ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID
845	ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName
846	ctx.Data["Org"] = ctx.Repo.Repository.Owner
847	ctx.Data["Units"] = unit_model.Units
848
849	ctx.HTML(http.StatusOK, tplCollaboration)
850}
851
852// CollaborationPost response for actions for a collaboration of a repository
853func CollaborationPost(ctx *context.Context) {
854	name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("collaborator")))
855	if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
856		ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
857		return
858	}
859
860	u, err := user_model.GetUserByName(name)
861	if err != nil {
862		if user_model.IsErrUserNotExist(err) {
863			ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
864			ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
865		} else {
866			ctx.ServerError("GetUserByName", err)
867		}
868		return
869	}
870
871	if !u.IsActive {
872		ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user"))
873		ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
874		return
875	}
876
877	// Organization is not allowed to be added as a collaborator.
878	if u.IsOrganization() {
879		ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator"))
880		ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
881		return
882	}
883
884	if got, err := models.IsCollaborator(ctx.Repo.Repository.ID, u.ID); err == nil && got {
885		ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate"))
886		ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
887		return
888	}
889
890	if err = models.AddCollaborator(ctx.Repo.Repository, u); err != nil {
891		ctx.ServerError("AddCollaborator", err)
892		return
893	}
894
895	if setting.Service.EnableNotifyMail {
896		mailer.SendCollaboratorMail(u, ctx.User, ctx.Repo.Repository)
897	}
898
899	ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success"))
900	ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
901}
902
903// ChangeCollaborationAccessMode response for changing access of a collaboration
904func ChangeCollaborationAccessMode(ctx *context.Context) {
905	if err := models.ChangeCollaborationAccessMode(
906		ctx.Repo.Repository,
907		ctx.FormInt64("uid"),
908		perm.AccessMode(ctx.FormInt("mode"))); err != nil {
909		log.Error("ChangeCollaborationAccessMode: %v", err)
910	}
911}
912
913// DeleteCollaboration delete a collaboration for a repository
914func DeleteCollaboration(ctx *context.Context) {
915	if err := models.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil {
916		ctx.Flash.Error("DeleteCollaboration: " + err.Error())
917	} else {
918		ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
919	}
920
921	ctx.JSON(http.StatusOK, map[string]interface{}{
922		"redirect": ctx.Repo.RepoLink + "/settings/collaboration",
923	})
924}
925
926// AddTeamPost response for adding a team to a repository
927func AddTeamPost(ctx *context.Context) {
928	if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
929		ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
930		ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
931		return
932	}
933
934	name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("team")))
935	if len(name) == 0 {
936		ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
937		return
938	}
939
940	team, err := models.OrgFromUser(ctx.Repo.Owner).GetTeam(name)
941	if err != nil {
942		if models.IsErrTeamNotExist(err) {
943			ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
944			ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
945		} else {
946			ctx.ServerError("GetTeam", err)
947		}
948		return
949	}
950
951	if team.OrgID != ctx.Repo.Repository.OwnerID {
952		ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization"))
953		ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
954		return
955	}
956
957	if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) {
958		ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate"))
959		ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
960		return
961	}
962
963	if err = team.AddRepository(ctx.Repo.Repository); err != nil {
964		ctx.ServerError("team.AddRepository", err)
965		return
966	}
967
968	ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success"))
969	ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
970}
971
972// DeleteTeam response for deleting a team from a repository
973func DeleteTeam(ctx *context.Context) {
974	if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
975		ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
976		ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
977		return
978	}
979
980	team, err := models.GetTeamByID(ctx.FormInt64("id"))
981	if err != nil {
982		ctx.ServerError("GetTeamByID", err)
983		return
984	}
985
986	if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil {
987		ctx.ServerError("team.RemoveRepositorys", err)
988		return
989	}
990
991	ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success"))
992	ctx.JSON(http.StatusOK, map[string]interface{}{
993		"redirect": ctx.Repo.RepoLink + "/settings/collaboration",
994	})
995}
996
997// GitHooks hooks of a repository
998func GitHooks(ctx *context.Context) {
999	ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
1000	ctx.Data["PageIsSettingsGitHooks"] = true
1001
1002	hooks, err := ctx.Repo.GitRepo.Hooks()
1003	if err != nil {
1004		ctx.ServerError("Hooks", err)
1005		return
1006	}
1007	ctx.Data["Hooks"] = hooks
1008
1009	ctx.HTML(http.StatusOK, tplGithooks)
1010}
1011
1012// GitHooksEdit render for editing a hook of repository page
1013func GitHooksEdit(ctx *context.Context) {
1014	ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
1015	ctx.Data["PageIsSettingsGitHooks"] = true
1016
1017	name := ctx.Params(":name")
1018	hook, err := ctx.Repo.GitRepo.GetHook(name)
1019	if err != nil {
1020		if err == git.ErrNotValidHook {
1021			ctx.NotFound("GetHook", err)
1022		} else {
1023			ctx.ServerError("GetHook", err)
1024		}
1025		return
1026	}
1027	ctx.Data["Hook"] = hook
1028	ctx.HTML(http.StatusOK, tplGithookEdit)
1029}
1030
1031// GitHooksEditPost response for editing a git hook of a repository
1032func GitHooksEditPost(ctx *context.Context) {
1033	name := ctx.Params(":name")
1034	hook, err := ctx.Repo.GitRepo.GetHook(name)
1035	if err != nil {
1036		if err == git.ErrNotValidHook {
1037			ctx.NotFound("GetHook", err)
1038		} else {
1039			ctx.ServerError("GetHook", err)
1040		}
1041		return
1042	}
1043	hook.Content = ctx.FormString("content")
1044	if err = hook.Update(); err != nil {
1045		ctx.ServerError("hook.Update", err)
1046		return
1047	}
1048	ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
1049}
1050
1051// DeployKeys render the deploy keys list of a repository page
1052func DeployKeys(ctx *context.Context) {
1053	ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
1054	ctx.Data["PageIsSettingsKeys"] = true
1055	ctx.Data["DisableSSH"] = setting.SSH.Disabled
1056
1057	keys, err := asymkey_model.ListDeployKeys(db.DefaultContext, &asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
1058	if err != nil {
1059		ctx.ServerError("ListDeployKeys", err)
1060		return
1061	}
1062	ctx.Data["Deploykeys"] = keys
1063
1064	ctx.HTML(http.StatusOK, tplDeployKeys)
1065}
1066
1067// DeployKeysPost response for adding a deploy key of a repository
1068func DeployKeysPost(ctx *context.Context) {
1069	form := web.GetForm(ctx).(*forms.AddKeyForm)
1070	ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
1071	ctx.Data["PageIsSettingsKeys"] = true
1072
1073	keys, err := asymkey_model.ListDeployKeys(db.DefaultContext, &asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
1074	if err != nil {
1075		ctx.ServerError("ListDeployKeys", err)
1076		return
1077	}
1078	ctx.Data["Deploykeys"] = keys
1079
1080	if ctx.HasError() {
1081		ctx.HTML(http.StatusOK, tplDeployKeys)
1082		return
1083	}
1084
1085	content, err := asymkey_model.CheckPublicKeyString(form.Content)
1086	if err != nil {
1087		if db.IsErrSSHDisabled(err) {
1088			ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
1089		} else if asymkey_model.IsErrKeyUnableVerify(err) {
1090			ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
1091		} else {
1092			ctx.Data["HasError"] = true
1093			ctx.Data["Err_Content"] = true
1094			ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
1095		}
1096		ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
1097		return
1098	}
1099
1100	key, err := asymkey_model.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
1101	if err != nil {
1102		ctx.Data["HasError"] = true
1103		switch {
1104		case asymkey_model.IsErrDeployKeyAlreadyExist(err):
1105			ctx.Data["Err_Content"] = true
1106			ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
1107		case asymkey_model.IsErrKeyAlreadyExist(err):
1108			ctx.Data["Err_Content"] = true
1109			ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
1110		case asymkey_model.IsErrKeyNameAlreadyUsed(err):
1111			ctx.Data["Err_Title"] = true
1112			ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
1113		case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
1114			ctx.Data["Err_Title"] = true
1115			ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
1116		default:
1117			ctx.ServerError("AddDeployKey", err)
1118		}
1119		return
1120	}
1121
1122	log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID)
1123	ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
1124	ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
1125}
1126
1127// DeleteDeployKey response for deleting a deploy key
1128func DeleteDeployKey(ctx *context.Context) {
1129	if err := asymkey_service.DeleteDeployKey(ctx.User, ctx.FormInt64("id")); err != nil {
1130		ctx.Flash.Error("DeleteDeployKey: " + err.Error())
1131	} else {
1132		ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
1133	}
1134
1135	ctx.JSON(http.StatusOK, map[string]interface{}{
1136		"redirect": ctx.Repo.RepoLink + "/settings/keys",
1137	})
1138}
1139
1140// UpdateAvatarSetting update repo's avatar
1141func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
1142	ctxRepo := ctx.Repo.Repository
1143
1144	if form.Avatar == nil {
1145		// No avatar is uploaded and we not removing it here.
1146		// No random avatar generated here.
1147		// Just exit, no action.
1148		if ctxRepo.CustomAvatarRelativePath() == "" {
1149			log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
1150		}
1151		return nil
1152	}
1153
1154	r, err := form.Avatar.Open()
1155	if err != nil {
1156		return fmt.Errorf("Avatar.Open: %v", err)
1157	}
1158	defer r.Close()
1159
1160	if form.Avatar.Size > setting.Avatar.MaxFileSize {
1161		return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
1162	}
1163
1164	data, err := io.ReadAll(r)
1165	if err != nil {
1166		return fmt.Errorf("io.ReadAll: %v", err)
1167	}
1168	st := typesniffer.DetectContentType(data)
1169	if !(st.IsImage() && !st.IsSvgImage()) {
1170		return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
1171	}
1172	if err = repo_service.UploadAvatar(ctxRepo, data); err != nil {
1173		return fmt.Errorf("UploadAvatar: %v", err)
1174	}
1175	return nil
1176}
1177
1178// SettingsAvatar save new POSTed repository avatar
1179func SettingsAvatar(ctx *context.Context) {
1180	form := web.GetForm(ctx).(*forms.AvatarForm)
1181	form.Source = forms.AvatarLocal
1182	if err := UpdateAvatarSetting(ctx, *form); err != nil {
1183		ctx.Flash.Error(err.Error())
1184	} else {
1185		ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success"))
1186	}
1187	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
1188}
1189
1190// SettingsDeleteAvatar delete repository avatar
1191func SettingsDeleteAvatar(ctx *context.Context) {
1192	if err := repo_service.DeleteAvatar(ctx.Repo.Repository); err != nil {
1193		ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
1194	}
1195	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
1196}
1197
1198func selectPushMirrorByForm(form *forms.RepoSettingForm, repo *repo_model.Repository) (*repo_model.PushMirror, error) {
1199	id, err := strconv.ParseInt(form.PushMirrorID, 10, 64)
1200	if err != nil {
1201		return nil, err
1202	}
1203
1204	pushMirrors, err := repo_model.GetPushMirrorsByRepoID(repo.ID)
1205	if err != nil {
1206		return nil, err
1207	}
1208
1209	for _, m := range pushMirrors {
1210		if m.ID == id {
1211			return m, nil
1212		}
1213	}
1214
1215	return nil, fmt.Errorf("PushMirror[%v] not associated to repository %v", id, repo)
1216}
1217