// Copyright 2014 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package repo import ( "encoding/json" "errors" "fmt" "strings" "time" "github.com/Unknwon/com" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/mailer" "github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/setting" ) const ( SETTINGS_OPTIONS base.TplName = "repo/settings/options" COLLABORATION base.TplName = "repo/settings/collaboration" HOOKS base.TplName = "repo/settings/hooks" HOOK_NEW base.TplName = "repo/settings/hook_new" ORG_HOOK_NEW base.TplName = "org/settings/hook_new" GITHOOKS base.TplName = "repo/settings/githooks" GITHOOK_EDIT base.TplName = "repo/settings/githook_edit" DEPLOY_KEYS base.TplName = "repo/settings/deploy_keys" ) func Settings(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsOptions"] = true ctx.HTML(200, SETTINGS_OPTIONS) } func SettingsPost(ctx *middleware.Context, form auth.RepoSettingForm) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsOptions"] = true repo := ctx.Repo.Repository switch ctx.Query("action") { case "update": if ctx.HasError() { ctx.HTML(200, SETTINGS_OPTIONS) return } isNameChanged := false oldRepoName := repo.Name newRepoName := form.RepoName // Check if repository name has been changed. if repo.LowerName != strings.ToLower(newRepoName) { isNameChanged = true if err := models.ChangeRepositoryName(ctx.Repo.Owner, repo.Name, newRepoName); err != nil { ctx.Data["Err_RepoName"] = true switch { case models.IsErrRepoAlreadyExist(err): ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), SETTINGS_OPTIONS, &form) case models.IsErrNameReserved(err): ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), SETTINGS_OPTIONS, &form) case models.IsErrNamePatternNotAllowed(err): ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), SETTINGS_OPTIONS, &form) default: ctx.Handle(500, "ChangeRepositoryName", err) } return } log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName) } // In case it's just a case change. repo.Name = newRepoName repo.LowerName = strings.ToLower(newRepoName) if ctx.Repo.GitRepo.IsBranchExist(form.Branch) && repo.DefaultBranch != form.Branch { repo.DefaultBranch = form.Branch if err := ctx.Repo.GitRepo.SetDefaultBranch(form.Branch); err != nil { if !git.IsErrUnsupportedVersion(err) { ctx.Handle(500, "SetDefaultBranch", err) return } } } repo.Description = form.Description repo.Website = form.Website // Visibility of forked repository is forced sync with base repository. if repo.IsFork { form.Private = repo.BaseRepo.IsPrivate } visibilityChanged := repo.IsPrivate != form.Private repo.IsPrivate = form.Private if err := models.UpdateRepository(repo, visibilityChanged); err != nil { ctx.Handle(500, "UpdateRepository", err) return } log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) if isNameChanged { if err := models.RenameRepoAction(ctx.User, oldRepoName, repo); err != nil { log.Error(4, "RenameRepoAction: %v", err) } } if repo.IsMirror { if form.Interval > 0 { ctx.Repo.Mirror.Interval = form.Interval ctx.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(form.Interval) * time.Hour) if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil { log.Error(4, "UpdateMirror: %v", err) } } } ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") case "advanced": repo.EnableWiki = form.EnableWiki repo.EnableIssues = form.EnableIssues repo.EnableExternalTracker = form.EnableExternalTracker repo.ExternalTrackerFormat = form.TrackerURLFormat repo.EnablePulls = form.EnablePulls if err := models.UpdateRepository(repo, false); err != nil { ctx.Handle(500, "UpdateRepository", err) return } log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") case "transfer": if repo.Name != form.RepoName { ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) return } if ctx.Repo.Owner.IsOrganization() { if !ctx.Repo.Owner.IsOwnedBy(ctx.User.Id) { ctx.Error(404) return } } newOwner := ctx.Query("new_owner_name") isExist, err := models.IsUserExist(0, newOwner) if err != nil { ctx.Handle(500, "IsUserExist", err) return } else if !isExist { ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), SETTINGS_OPTIONS, nil) return } if err = models.TransferOwnership(ctx.User, newOwner, repo); err != nil { if models.IsErrRepoAlreadyExist(err) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), SETTINGS_OPTIONS, nil) } else { ctx.Handle(500, "TransferOwnership", err) } return } log.Trace("Repository transfered: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) ctx.Redirect(setting.AppSubUrl + "/" + newOwner + "/" + repo.Name) case "delete": if repo.Name != form.RepoName { ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) return } if ctx.Repo.Owner.IsOrganization() { if !ctx.Repo.Owner.IsOwnedBy(ctx.User.Id) { ctx.Error(404) return } } if err := models.DeleteRepository(ctx.Repo.Owner.Id, repo.ID); err != nil { ctx.Handle(500, "DeleteRepository", err) return } log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Redirect(ctx.Repo.Owner.DashboardLink()) } } func Collaboration(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsCollaboration"] = true if ctx.Req.Method == "POST" { name := strings.ToLower(ctx.Query("collaborator")) if len(name) == 0 || ctx.Repo.Owner.LowerName == name { ctx.Redirect(setting.AppSubUrl + ctx.Req.URL.Path) return } u, err := models.GetUserByName(name) if err != nil { if models.IsErrUserNotExist(err) { ctx.Flash.Error(ctx.Tr("form.user_not_exist")) ctx.Redirect(setting.AppSubUrl + ctx.Req.URL.Path) } else { ctx.Handle(500, "GetUserByName", err) } return } // Check if user is organization member. if ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOrgMember(u.Id) { ctx.Flash.Info(ctx.Tr("repo.settings.user_is_org_member")) ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") return } if err = ctx.Repo.Repository.AddCollaborator(u); err != nil { ctx.Handle(500, "AddCollaborator", err) return } if setting.Service.EnableNotifyMail { if err = mailer.SendCollaboratorMail(ctx.Render, u, ctx.User, ctx.Repo.Repository); err != nil { ctx.Handle(500, "SendCollaboratorMail", err) return } } ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success")) ctx.Redirect(setting.AppSubUrl + ctx.Req.URL.Path) return } // Delete collaborator. remove := strings.ToLower(ctx.Query("remove")) if len(remove) > 0 && remove != ctx.Repo.Owner.LowerName { u, err := models.GetUserByName(remove) if err != nil { ctx.Handle(500, "GetUserByName", err) return } if err := ctx.Repo.Repository.DeleteCollaborator(u); err != nil { ctx.Handle(500, "DeleteCollaborator", err) return } ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") return } users, err := ctx.Repo.Repository.GetCollaborators() if err != nil { ctx.Handle(500, "GetCollaborators", err) return } ctx.Data["Collaborators"] = users ctx.HTML(200, COLLABORATION) } func Webhooks(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.hooks") ctx.Data["PageIsSettingsHooks"] = true ctx.Data["BaseLink"] = ctx.Repo.RepoLink ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://github.com/gogits/go-gogs-client/wiki/Repositories-Webhooks") ws, err := models.GetWebhooksByRepoID(ctx.Repo.Repository.ID) if err != nil { ctx.Handle(500, "GetWebhooksByRepoID", err) return } ctx.Data["Webhooks"] = ws ctx.HTML(200, HOOKS) } type OrgRepoCtx struct { OrgID int64 RepoID int64 Link string NewTemplate base.TplName } // getOrgRepoCtx determines whether this is a repo context or organization context. func getOrgRepoCtx(ctx *middleware.Context) (*OrgRepoCtx, error) { if len(ctx.Repo.RepoLink) > 0 { return &OrgRepoCtx{ RepoID: ctx.Repo.Repository.ID, Link: ctx.Repo.RepoLink, NewTemplate: HOOK_NEW, }, nil } if len(ctx.Org.OrgLink) > 0 { return &OrgRepoCtx{ OrgID: ctx.Org.Organization.Id, Link: ctx.Org.OrgLink, NewTemplate: ORG_HOOK_NEW, }, nil } return nil, errors.New("Unable to set OrgRepo context") } func checkHookType(ctx *middleware.Context) string { hookType := strings.ToLower(ctx.Params(":type")) if !com.IsSliceContainsStr(setting.Webhook.Types, hookType) { ctx.Handle(404, "checkHookType", nil) return "" } return hookType } func WebhooksNew(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["PageIsSettingsHooks"] = true ctx.Data["PageIsSettingsHooksNew"] = true ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} orCtx, err := getOrgRepoCtx(ctx) if err != nil { ctx.Handle(500, "getOrgRepoCtx", err) return } ctx.Data["HookType"] = checkHookType(ctx) if ctx.Written() { return } ctx.Data["BaseLink"] = orCtx.Link ctx.HTML(200, orCtx.NewTemplate) } func ParseHookEvent(form auth.WebhookForm) *models.HookEvent { return &models.HookEvent{ PushOnly: form.PushOnly(), SendEverything: form.SendEverything(), ChooseEvents: form.ChooseEvents(), HookEvents: models.HookEvents{ Create: form.Create, Push: form.Push, }, } } func WebHooksNewPost(ctx *middleware.Context, form auth.NewWebhookForm) { ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["PageIsSettingsHooks"] = true ctx.Data["PageIsSettingsHooksNew"] = true ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} ctx.Data["HookType"] = "gogs" orCtx, err := getOrgRepoCtx(ctx) if err != nil { ctx.Handle(500, "getOrgRepoCtx", err) return } ctx.Data["BaseLink"] = orCtx.Link if ctx.HasError() { ctx.HTML(200, orCtx.NewTemplate) return } contentType := models.JSON if models.HookContentType(form.ContentType) == models.FORM { contentType = models.FORM } w := &models.Webhook{ RepoID: orCtx.RepoID, URL: form.PayloadURL, ContentType: contentType, Secret: form.Secret, HookEvent: ParseHookEvent(form.WebhookForm), IsActive: form.Active, HookTaskType: models.GOGS, OrgID: orCtx.OrgID, } if err := w.UpdateEvent(); err != nil { ctx.Handle(500, "UpdateEvent", err) return } else if err := models.CreateWebhook(w); err != nil { ctx.Handle(500, "CreateWebhook", err) return } ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success")) ctx.Redirect(orCtx.Link + "/settings/hooks") } func SlackHooksNewPost(ctx *middleware.Context, form auth.NewSlackHookForm) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsHooks"] = true ctx.Data["PageIsSettingsHooksNew"] = true ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} orCtx, err := getOrgRepoCtx(ctx) if err != nil { ctx.Handle(500, "getOrgRepoCtx", err) return } if ctx.HasError() { ctx.HTML(200, orCtx.NewTemplate) return } meta, err := json.Marshal(&models.SlackMeta{ Channel: form.Channel, Username: form.Username, IconURL: form.IconURL, Color: form.Color, }) if err != nil { ctx.Handle(500, "Marshal", err) return } w := &models.Webhook{ RepoID: orCtx.RepoID, URL: form.PayloadURL, ContentType: models.JSON, HookEvent: ParseHookEvent(form.WebhookForm), IsActive: form.Active, HookTaskType: models.SLACK, Meta: string(meta), OrgID: orCtx.OrgID, } if err := w.UpdateEvent(); err != nil { ctx.Handle(500, "UpdateEvent", err) return } else if err := models.CreateWebhook(w); err != nil { ctx.Handle(500, "CreateWebhook", err) return } ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success")) ctx.Redirect(orCtx.Link + "/settings/hooks") } func checkWebhook(ctx *middleware.Context) (*OrgRepoCtx, *models.Webhook) { ctx.Data["RequireHighlightJS"] = true orCtx, err := getOrgRepoCtx(ctx) if err != nil { ctx.Handle(500, "getOrgRepoCtx", err) return nil, nil } ctx.Data["BaseLink"] = orCtx.Link w, err := models.GetWebhookByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrWebhookNotExist(err) { ctx.Handle(404, "GetWebhookByID", nil) } else { ctx.Handle(500, "GetWebhookByID", err) } return nil, nil } switch w.HookTaskType { case models.SLACK: ctx.Data["SlackHook"] = w.GetSlackHook() ctx.Data["HookType"] = "slack" default: ctx.Data["HookType"] = "gogs" } ctx.Data["History"], err = w.History(1) if err != nil { ctx.Handle(500, "History", err) } return orCtx, w } func WebHooksEdit(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook") ctx.Data["PageIsSettingsHooks"] = true ctx.Data["PageIsSettingsHooksEdit"] = true orCtx, w := checkWebhook(ctx) if ctx.Written() { return } ctx.Data["Webhook"] = w ctx.HTML(200, orCtx.NewTemplate) } func WebHooksEditPost(ctx *middleware.Context, form auth.NewWebhookForm) { ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook") ctx.Data["PageIsSettingsHooks"] = true ctx.Data["PageIsSettingsHooksEdit"] = true orCtx, w := checkWebhook(ctx) if ctx.Written() { return } ctx.Data["Webhook"] = w if ctx.HasError() { ctx.HTML(200, orCtx.NewTemplate) return } contentType := models.JSON if models.HookContentType(form.ContentType) == models.FORM { contentType = models.FORM } w.URL = form.PayloadURL w.ContentType = contentType w.Secret = form.Secret w.HookEvent = ParseHookEvent(form.WebhookForm) w.IsActive = form.Active if err := w.UpdateEvent(); err != nil { ctx.Handle(500, "UpdateEvent", err) return } else if err := models.UpdateWebhook(w); err != nil { ctx.Handle(500, "WebHooksEditPost", err) return } ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) } func SlackHooksEditPost(ctx *middleware.Context, form auth.NewSlackHookForm) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsHooks"] = true ctx.Data["PageIsSettingsHooksEdit"] = true orCtx, w := checkWebhook(ctx) if ctx.Written() { return } ctx.Data["Webhook"] = w if ctx.HasError() { ctx.HTML(200, orCtx.NewTemplate) return } meta, err := json.Marshal(&models.SlackMeta{ Channel: form.Channel, Username: form.Username, IconURL: form.IconURL, Color: form.Color, }) if err != nil { ctx.Handle(500, "Marshal", err) return } w.URL = form.PayloadURL w.Meta = string(meta) w.HookEvent = ParseHookEvent(form.WebhookForm) w.IsActive = form.Active if err := w.UpdateEvent(); err != nil { ctx.Handle(500, "UpdateEvent", err) return } else if err := models.UpdateWebhook(w); err != nil { ctx.Handle(500, "UpdateWebhook", err) return } ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) } func DeleteWebhook(ctx *middleware.Context) { if err := models.DeleteWebhook(ctx.QueryInt64("id")); err != nil { ctx.Flash.Error("DeleteWebhook: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) } ctx.JSON(200, map[string]interface{}{ "redirect": ctx.Repo.RepoLink + "/settings/hooks", }) } func parseOwnerAndRepo(ctx *middleware.Context) (*models.User, *models.Repository) { owner, err := models.GetUserByName(ctx.Params(":username")) if err != nil { if models.IsErrUserNotExist(err) { ctx.Handle(404, "GetUserByName", err) } else { ctx.Handle(500, "GetUserByName", err) } return nil, nil } repo, err := models.GetRepositoryByName(owner.Id, ctx.Params(":reponame")) if err != nil { if models.IsErrRepoNotExist(err) { ctx.Handle(404, "GetRepositoryByName", err) } else { ctx.Handle(500, "GetRepositoryByName", err) } return nil, nil } return owner, repo } func GitHooks(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.githooks") ctx.Data["PageIsSettingsGitHooks"] = true hooks, err := ctx.Repo.GitRepo.Hooks() if err != nil { ctx.Handle(500, "Hooks", err) return } ctx.Data["Hooks"] = hooks ctx.HTML(200, GITHOOKS) } func GitHooksEdit(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.githooks") ctx.Data["PageIsSettingsGitHooks"] = true name := ctx.Params(":name") hook, err := ctx.Repo.GitRepo.GetHook(name) if err != nil { if err == git.ErrNotValidHook { ctx.Handle(404, "GetHook", err) } else { ctx.Handle(500, "GetHook", err) } return } ctx.Data["Hook"] = hook ctx.HTML(200, GITHOOK_EDIT) } func GitHooksEditPost(ctx *middleware.Context) { name := ctx.Params(":name") hook, err := ctx.Repo.GitRepo.GetHook(name) if err != nil { if err == git.ErrNotValidHook { ctx.Handle(404, "GetHook", err) } else { ctx.Handle(500, "GetHook", err) } return } hook.Content = ctx.Query("content") if err = hook.Update(); err != nil { ctx.Handle(500, "hook.Update", err) return } ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git") } func DeployKeys(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") ctx.Data["PageIsSettingsKeys"] = true keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID) if err != nil { ctx.Handle(500, "ListDeployKeys", err) return } ctx.Data["Deploykeys"] = keys ctx.HTML(200, DEPLOY_KEYS) } func DeployKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) { ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") ctx.Data["PageIsSettingsKeys"] = true keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID) if err != nil { ctx.Handle(500, "ListDeployKeys", err) return } ctx.Data["Deploykeys"] = keys if ctx.HasError() { ctx.HTML(200, DEPLOY_KEYS) return } content, err := models.CheckPublicKeyString(form.Content) if err != nil { if models.IsErrKeyUnableVerify(err) { ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) } else { ctx.Data["HasError"] = true ctx.Data["Err_Content"] = true ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") return } } key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content) if err != nil { ctx.Data["HasError"] = true switch { case models.IsErrKeyAlreadyExist(err): ctx.Data["Err_Content"] = true ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), DEPLOY_KEYS, &form) case models.IsErrKeyNameAlreadyUsed(err): ctx.Data["Err_Title"] = true ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), DEPLOY_KEYS, &form) default: ctx.Handle(500, "AddDeployKey", err) } return } log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID) ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name)) ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") } func DeleteDeployKey(ctx *middleware.Context) { if err := models.DeleteDeployKey(ctx.User, ctx.QueryInt64("id")); err != nil { ctx.Flash.Error("DeleteDeployKey: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success")) } ctx.JSON(200, map[string]interface{}{ "redirect": ctx.Repo.RepoLink + "/settings/keys", }) }