From 8a84d82d5341ff6572e7c77f5779e2583ed45c25 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 6 Nov 2019 14:43:03 +0800 Subject: [PATCH 01/11] Move repofiles webhooks to notification (#8807) --- modules/notification/base/notifier.go | 2 + modules/notification/base/null.go | 8 +++ modules/notification/notification.go | 14 +++++ modules/notification/webhook/webhook.go | 47 +++++++++++++++- modules/repofiles/action.go | 74 +++---------------------- 5 files changed, 77 insertions(+), 68 deletions(-) diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 286ebe5d69a..72bf52c938a 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -43,4 +43,6 @@ type Notifier interface { NotifyDeleteRelease(doer *models.User, rel *models.Release) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *models.PushCommits) + NotifyCreateRef(doer *models.User, repo *models.Repository, refType, refFullName string) + NotifyDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) } diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 5b6359cbd55..a9d9d6a1640 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -114,3 +114,11 @@ func (*NullNotifier) NotifyMigrateRepository(doer *models.User, u *models.User, // NotifyPushCommits notifies commits pushed to notifiers func (*NullNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *models.PushCommits) { } + +// NotifyCreateRef notifies branch or tag creation to notifiers +func (*NullNotifier) NotifyCreateRef(doer *models.User, repo *models.Repository, refType, refFullName string) { +} + +// NotifyDeleteRef notifies branch or tag deleteion to notifiers +func (*NullNotifier) NotifyDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) { +} diff --git a/modules/notification/notification.go b/modules/notification/notification.go index a5e450ee66c..5ac09a72e5c 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -199,3 +199,17 @@ func NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, ol notifier.NotifyPushCommits(pusher, repo, refName, oldCommitID, newCommitID, commits) } } + +// NotifyCreateRef notifies branch or tag creation to notifiers +func NotifyCreateRef(pusher *models.User, repo *models.Repository, refType, refFullName string) { + for _, notifier := range notifiers { + notifier.NotifyCreateRef(pusher, repo, refType, refFullName) + } +} + +// NotifyDeleteRef notifies branch or tag deletion to notifiers +func NotifyDeleteRef(pusher *models.User, repo *models.Repository, refType, refFullName string) { + for _, notifier := range notifiers { + notifier.NotifyDeleteRef(pusher, repo, refType, refFullName) + } +} diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 39c63edb05c..6eb03d3ebc6 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -6,6 +6,7 @@ package webhook import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification/base" "code.gitea.io/gitea/modules/setting" @@ -562,6 +563,34 @@ func (m *webhookNotifier) NotifyPullRequestReview(pr *models.PullRequest, review } } +func (m *webhookNotifier) NotifyCreateRef(pusher *models.User, repo *models.Repository, refType, refFullName string) { + apiPusher := pusher.APIFormat() + apiRepo := repo.APIFormat(models.AccessModeNone) + refName := git.RefEndName(refFullName) + + gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err != nil { + log.Error("OpenRepository[%s]: %v", repo.RepoPath(), err) + return + } + + shaSum, err := gitRepo.GetBranchCommitID(refName) + if err != nil { + log.Error("GetBranchCommitID[%s]: %v", refFullName, err) + return + } + + if err = webhook_module.PrepareWebhooks(repo, models.HookEventCreate, &api.CreatePayload{ + Ref: refName, + Sha: shaSum, + RefType: refType, + Repo: apiRepo, + Sender: apiPusher, + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } +} + func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) { if err := pr.LoadIssue(); err != nil { log.Error("pr.LoadIssue: %v", err) @@ -572,7 +601,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *m return } - if err := webhook.PrepareWebhooks(pr.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + if err := webhook_module.PrepareWebhooks(pr.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueSynchronized, Index: pr.Issue.Index, PullRequest: pr.Issue.PullRequest.APIFormat(), @@ -582,3 +611,19 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *m log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err) } } + +func (m *webhookNotifier) NotifyDeleteRef(pusher *models.User, repo *models.Repository, refType, refFullName string) { + apiPusher := pusher.APIFormat() + apiRepo := repo.APIFormat(models.AccessModeNone) + refName := git.RefEndName(refFullName) + + if err := webhook_module.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{ + Ref: refName, + RefType: "branch", + PusherType: api.PusherTypeUser, + Repo: apiRepo, + Sender: apiPusher, + }); err != nil { + log.Error("PrepareWebhooks.(delete branch): %v", err) + } +} diff --git a/modules/repofiles/action.go b/modules/repofiles/action.go index e5f6bf87187..996363863d9 100644 --- a/modules/repofiles/action.go +++ b/modules/repofiles/action.go @@ -14,8 +14,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/webhook" ) // CommitRepoActionOptions represent options of a new commit action. @@ -113,81 +111,23 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { return fmt.Errorf("NotifyWatchers: %v", err) } - apiPusher := pusher.APIFormat() - apiRepo := repo.APIFormat(models.AccessModeNone) - - var shaSum string - var isHookEventPush = false + var isHookEventPush = true switch opType { case models.ActionCommitRepo: // Push - isHookEventPush = true - if isNewBranch { - gitRepo, err := git.OpenRepository(repo.RepoPath()) - if err != nil { - log.Error("OpenRepository[%s]: %v", repo.RepoPath(), err) - } - - shaSum, err = gitRepo.GetBranchCommitID(refName) - if err != nil { - log.Error("GetBranchCommitID[%s]: %v", opts.RefFullName, err) - } - if err = webhook.PrepareWebhooks(repo, models.HookEventCreate, &api.CreatePayload{ - Ref: refName, - Sha: shaSum, - RefType: "branch", - Repo: apiRepo, - Sender: apiPusher, - }); err != nil { - return fmt.Errorf("PrepareWebhooks: %v", err) - } + notification.NotifyCreateRef(pusher, repo, "branch", opts.RefFullName) } case models.ActionDeleteBranch: // Delete Branch - isHookEventPush = true - - if err = webhook.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{ - Ref: refName, - RefType: "branch", - PusherType: api.PusherTypeUser, - Repo: apiRepo, - Sender: apiPusher, - }); err != nil { - return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err) - } + notification.NotifyDeleteRef(pusher, repo, "branch", opts.RefFullName) case models.ActionPushTag: // Create - isHookEventPush = true + notification.NotifyCreateRef(pusher, repo, "tag", opts.RefFullName) - gitRepo, err := git.OpenRepository(repo.RepoPath()) - if err != nil { - log.Error("OpenRepository[%s]: %v", repo.RepoPath(), err) - } - shaSum, err = gitRepo.GetTagCommitID(refName) - if err != nil { - log.Error("GetTagCommitID[%s]: %v", opts.RefFullName, err) - } - if err = webhook.PrepareWebhooks(repo, models.HookEventCreate, &api.CreatePayload{ - Ref: refName, - Sha: shaSum, - RefType: "tag", - Repo: apiRepo, - Sender: apiPusher, - }); err != nil { - return fmt.Errorf("PrepareWebhooks: %v", err) - } case models.ActionDeleteTag: // Delete Tag - isHookEventPush = true - - if err = webhook.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{ - Ref: refName, - RefType: "tag", - PusherType: api.PusherTypeUser, - Repo: apiRepo, - Sender: apiPusher, - }); err != nil { - return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err) - } + notification.NotifyDeleteRef(pusher, repo, "tag", opts.RefFullName) + default: + isHookEventPush = false } if isHookEventPush { From 6d42add37f6120f9fa6b4f2f32f6ee316c621d41 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 6 Nov 2019 06:44:54 +0000 Subject: [PATCH 02/11] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 3 +++ options/locale/locale_fr-FR.ini | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index a37d9d7dd79..5d28c765eb0 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1089,6 +1089,9 @@ activity.period.daily=1 Tag activity.period.halfweekly=3 Tage activity.period.weekly=1 Woche activity.period.monthly=1 Monat +activity.period.quarterly=3 Monate +activity.period.semiyearly=6 Monate +activity.period.yearly=1 Jahr activity.overview=Übersicht activity.active_prs_count_1=%d aktiver Pull-Request activity.active_prs_count_n=%d aktive Pull-Requests diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index c9a8711533a..c1509b10899 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -586,6 +586,8 @@ fork_visibility_helper=La visibilité d'un dépôt bifurqué ne peut pas être m repo_desc=Description repo_lang=Langue repo_gitignore_helper=Choisissez un modèle de fichier .gitignore. +issue_labels=Étiquettes des tickets +issue_labels_helper=Sélectionnez une étiquette de ticket. license=Licence license_helper=Sélectionner un fichier de licence. readme=LISEZMOI @@ -846,6 +848,8 @@ issues.create_comment=Créer un commentaire issues.closed_at=`a fermé %[2]s` issues.reopened_at=`réouvert à %[2]s` issues.commit_ref_at=`a référencé ce ticket depuis une révision %[2]s` +issues.ref_issue_at=`a fait référence à ce ticket : %[1]s` +issues.ref_issue_ext_at=`a fait référence à ce ticket depuis : %[1]s %[2]s` issues.poster=Publier issues.collaborator=Collaborateur issues.owner=Propriétaire @@ -1330,6 +1334,8 @@ settings.protect_merge_whitelist_committers_desc=N'autoriser que les utilisateur settings.protect_merge_whitelist_users=Utilisateurs en liste blanche de fusion : settings.protect_merge_whitelist_teams=Équipes en liste blanche de fusion : settings.protect_check_status_contexts=Activer le Contrôle Qualité +settings.protect_check_status_contexts_desc=Exiger le passage du contrôle qualité avant de fusionner Choisir quels contrôles qualité doivent être validés avant que les branches puissent être fusionnées dans une branche qui correspond à cette règle. Si activé, les commits doivent d'abord être poussés vers une autre branche avant d'être fusionnés ou bien poussés directement vers une branche qui correspond à cette règle après que les contrôles qualité soient passés. Si aucun contexte n'a été choisi, le dernier commit doit passer le contrôle qualité peu-importe le contexte. +settings.protect_check_status_contexts_list=Contrôles qualité trouvés au cours de la semaine dernière pour ce dépôt settings.protect_required_approvals=Agréments nécessaires : settings.protect_required_approvals_desc=N'autoriser la fusion qu'avec suffisamment de revues positives d'utilisateurs ou équipes sur liste blanche. settings.protect_approvals_whitelist_users=Réviseurs sur liste blanche : @@ -1366,6 +1372,10 @@ diff.parent=Parent diff.commit=révision diff.git-notes=Notes diff.data_not_available=Contenu de la comparaison indisponible +diff.options_button=Option de Diff +diff.show_diff_stats=Voir les Statistiques +diff.download_patch=Télécharger le Fichier Patch +diff.download_diff=Télécharger le Fichier des Différences diff.show_split_view=Vue séparée diff.show_unified_view=Vue unifiée diff.whitespace_button=Espace @@ -1376,6 +1386,11 @@ diff.whitespace_ignore_at_eol=Ignorer les changements quand ce sont des espaces diff.stats_desc= %d fichiers modifiés avec %d ajouts et %d suppressions diff.bin=BIN diff.view_file=Voir le fichier +diff.file_before=Avant +diff.file_after=Après +diff.file_image_width=Largeur +diff.file_image_height=Hauteur +diff.file_byte_size=Taille diff.file_suppressed=Fichier diff supprimé car celui-ci est trop grand diff.too_many_files=Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff diff.comment.placeholder=Laisser un commentaire @@ -1443,6 +1458,8 @@ branch.restore_failed=La restauration de la branche '%s' a échoué. branch.protected_deletion_failed=La branche '%s' est protégé. Il ne peut pas être supprimé. branch.restore=Restaurer la branche '%s' branch.download=Télécharger la branche '%s' +branch.included_desc=Cette branche fait partie de la branche par défaut +branch.included=Incluses topic.manage_topics=Gérer les sujets topic.done=Terminé @@ -1478,6 +1495,8 @@ settings.options=Organisation settings.full_name=Non Complet settings.website=Site Web settings.location=Localisation +settings.permission=Autorisations +settings.repoadminchangeteam=L'administrateur de dépôt peut ajouter et supprimer l'accès aux équipes settings.visibility=Visibilité settings.visibility.public=Public settings.visibility.limited=Limité (Visible uniquement aux utilisateurs connectés) @@ -1724,6 +1743,7 @@ auths.tip.google_plus=Obtenez des identifiants OAuth2 sur la console API de Goog auths.tip.openid_connect=Utilisez l'URL de découvert OpenID (/.well-known/openid-configuration) pour spécifier les points d'accès auths.tip.twitter=Rendez-vous sur https://dev.twitter.com/apps, créez une application et assurez-vous que l'option "Autoriser l'application à être utilisée avec Twitter Connect" est activée auths.tip.discord=Enregistrer une nouvelle application sur https://discordapp.com/developers/applications/me +auths.tip.gitea=Enregistrez une nouvelle application OAuth2. Un guide peut être trouvé sur https://docs.gitea.io/en-us/oauth2-provider/ auths.edit=Mettre à jour la source d'authentification auths.activated=Cette source d'authentification est activée auths.new_success=L'authentification "%s" a été ajoutée. @@ -1956,6 +1976,7 @@ mark_as_unread=Marquer comme non lue mark_all_as_read=Tout marquer comme lu [gpg] +default_key=Signé avec la clé par défaut error.extract_sign=Impossible d'extraire la signature error.generate_hash=Impossible de générer la chaine de hachage de la révision error.no_committer_account=Aucun compte lié à l'adresse e-mail de l'auteur From 0109229928d8603ebedc2364943538f788635370 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 6 Nov 2019 16:25:50 +0800 Subject: [PATCH 03/11] Move release webhook to notification (#8817) * Move release webhook to notification * Extract release webhook method * fix bug * fix import --- modules/notification/webhook/webhook.go | 34 ++++++++++++++++++-- services/release/release.go | 42 +++---------------------- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 6eb03d3ebc6..8059ec1c00f 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/modules/notification/base" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -289,7 +288,7 @@ func (m *webhookNotifier) NotifyNewPullRequest(pull *models.PullRequest) { } mode, _ := models.AccessLevel(pull.Issue.Poster, pull.Issue.Repo) - if err := webhook.PrepareWebhooks(pull.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + if err := webhook_module.PrepareWebhooks(pull.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Issue.Index, PullRequest: pull.APIFormat(), @@ -548,7 +547,7 @@ func (m *webhookNotifier) NotifyPullRequestReview(pr *models.PullRequest, review log.Error("models.AccessLevel: %v", err) return } - if err := webhook.PrepareWebhooks(review.Issue.Repo, reviewHookType, &api.PullRequestPayload{ + if err := webhook_module.PrepareWebhooks(review.Issue.Repo, reviewHookType, &api.PullRequestPayload{ Action: api.HookIssueSynchronized, Index: review.Issue.Index, PullRequest: pr.APIFormat(), @@ -627,3 +626,32 @@ func (m *webhookNotifier) NotifyDeleteRef(pusher *models.User, repo *models.Repo log.Error("PrepareWebhooks.(delete branch): %v", err) } } + +func sendReleaseHook(doer *models.User, rel *models.Release, action api.HookReleaseAction) { + if err := rel.LoadAttributes(); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + mode, _ := models.AccessLevel(rel.Publisher, rel.Repo) + if err := webhook_module.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ + Action: action, + Release: rel.APIFormat(), + Repository: rel.Repo.APIFormat(mode), + Sender: rel.Publisher.APIFormat(), + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } +} + +func (m *webhookNotifier) NotifyNewRelease(rel *models.Release) { + sendReleaseHook(rel.Publisher, rel, api.HookReleasePublished) +} + +func (m *webhookNotifier) NotifyUpdateRelease(doer *models.User, rel *models.Release) { + sendReleaseHook(doer, rel, api.HookReleaseUpdated) +} + +func (m *webhookNotifier) NotifyDeleteRelease(doer *models.User, rel *models.Release) { + sendReleaseHook(doer, rel, api.HookReleaseDeleted) +} diff --git a/services/release/release.go b/services/release/release.go index a3f027c9498..681e8c0d9aa 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -12,10 +12,9 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/process" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/webhook" ) func createTag(gitRepo *git.Repository, rel *models.Release) error { @@ -81,19 +80,7 @@ func CreateRelease(gitRepo *git.Repository, rel *models.Release, attachmentUUIDs } if !rel.IsDraft { - if err := rel.LoadAttributes(); err != nil { - log.Error("LoadAttributes: %v", err) - } else { - mode, _ := models.AccessLevel(rel.Publisher, rel.Repo) - if err := webhook.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ - Action: api.HookReleasePublished, - Release: rel.APIFormat(), - Repository: rel.Repo.APIFormat(mode), - Sender: rel.Publisher.APIFormat(), - }); err != nil { - log.Error("PrepareWebhooks: %v", err) - } - } + notification.NotifyNewRelease(rel) } return nil @@ -114,20 +101,7 @@ func UpdateRelease(doer *models.User, gitRepo *git.Repository, rel *models.Relea log.Error("AddReleaseAttachments: %v", err) } - if err = rel.LoadAttributes(); err != nil { - return err - } - - // even if attachments added failed, hooks will be still triggered - mode, _ := models.AccessLevel(doer, rel.Repo) - if err1 := webhook.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ - Action: api.HookReleaseUpdated, - Release: rel.APIFormat(), - Repository: rel.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }); err1 != nil { - log.Error("PrepareWebhooks: %v", err) - } + notification.NotifyUpdateRelease(doer, rel) return err } @@ -183,15 +157,7 @@ func DeleteReleaseByID(id int64, doer *models.User, delTag bool) error { } } - mode, _ := models.AccessLevel(doer, rel.Repo) - if err := webhook.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ - Action: api.HookReleaseDeleted, - Release: rel.APIFormat(), - Repository: rel.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }); err != nil { - log.Error("PrepareWebhooks: %v", err) - } + notification.NotifyDeleteRelease(doer, rel) return nil } From 72aa5a20ecf8aa3f7c110fd51c37994d950e0ba8 Mon Sep 17 00:00:00 2001 From: David Svantesson Date: Wed, 6 Nov 2019 10:37:14 +0100 Subject: [PATCH 04/11] Add team option to grant rights for all organization repositories (#8688) * Add field IsAllRepositories to team * Add AllRepositories to team UI * Manage team with access to all repositories * Add field IsAllRepositories to team API * put backticks around table/column names * rename IsAllRepositories to IncludesAllRepositories * do not reload slice if already loaded * add repo to teams with access to all repositories when changing repo owner * improve tests for teams with access to all repositories * Merge branch 'master' * Change code for adding all repositories Signed-off-by: David Svantesson * fmt after merge * Change code in API EditTeam similar to EditTeamPost web interface Signed-off-by: David Svantesson * Clarify that all repositories will be added Signed-off-by: David Svantesson * All repositories option under Permissions headline * New setting group 'Repository access' * Move check IncludeAllRepositories to removeRepository. * Revert "Move check IncludeAllRepositories to removeRepository." and add comment instead. This reverts commit 753b7d205be260b8be465b5291a02975a81f3093. * Clarify help text what options do. --- integrations/api_team_test.go | 40 ++++---- models/migrations/migrations.go | 2 + models/migrations/v105.go | 25 +++++ models/org.go | 14 ++- models/org_team.go | 70 +++++++++++--- models/org_team_test.go | 137 ++++++++++++++++++++++++++- models/repo.go | 29 +++--- modules/auth/org.go | 1 + modules/structs/org_team.go | 19 ++-- options/locale/locale_en-US.ini | 8 ++ routers/api/v1/convert/convert.go | 11 ++- routers/api/v1/org/team.go | 31 ++++-- routers/org/teams.go | 20 +++- templates/org/team/new.tmpl | 18 ++++ templates/org/team/repositories.tmpl | 2 +- templates/org/team/sidebar.tmpl | 18 +++- templates/swagger/v1_json.tmpl | 12 +++ 17 files changed, 382 insertions(+), 75 deletions(-) create mode 100644 models/migrations/v105.go diff --git a/integrations/api_team_test.go b/integrations/api_team_test.go index 38e202f239f..e25ffdf7b12 100644 --- a/integrations/api_team_test.go +++ b/integrations/api_team_test.go @@ -55,37 +55,44 @@ func TestAPITeam(t *testing.T) { // Create team. teamToCreate := &api.CreateTeamOption{ - Name: "team1", - Description: "team one", - Permission: "write", - Units: []string{"repo.code", "repo.issues"}, + Name: "team1", + Description: "team one", + IncludesAllRepositories: true, + Permission: "write", + Units: []string{"repo.code", "repo.issues"}, } req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate) resp = session.MakeRequest(t, req, http.StatusCreated) DecodeJSON(t, resp, &apiTeam) - checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.Permission, teamToCreate.Units) - checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.Permission, teamToCreate.Units) + checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, + teamToCreate.Permission, teamToCreate.Units) + checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, + teamToCreate.Permission, teamToCreate.Units) teamID := apiTeam.ID // Edit team. teamToEdit := &api.EditTeamOption{ - Name: "teamone", - Description: "team 1", - Permission: "admin", - Units: []string{"repo.code", "repo.pulls", "repo.releases"}, + Name: "teamone", + Description: "team 1", + IncludesAllRepositories: false, + Permission: "admin", + Units: []string{"repo.code", "repo.pulls", "repo.releases"}, } req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiTeam) - checkTeamResponse(t, &apiTeam, teamToEdit.Name, teamToEdit.Description, teamToEdit.Permission, teamToEdit.Units) - checkTeamBean(t, apiTeam.ID, teamToEdit.Name, teamToEdit.Description, teamToEdit.Permission, teamToEdit.Units) + checkTeamResponse(t, &apiTeam, teamToEdit.Name, teamToEdit.Description, teamToEdit.IncludesAllRepositories, + teamToEdit.Permission, teamToEdit.Units) + checkTeamBean(t, apiTeam.ID, teamToEdit.Name, teamToEdit.Description, teamToEdit.IncludesAllRepositories, + teamToEdit.Permission, teamToEdit.Units) // Read team. teamRead := models.AssertExistsAndLoadBean(t, &models.Team{ID: teamID}).(*models.Team) req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiTeam) - checkTeamResponse(t, &apiTeam, teamRead.Name, teamRead.Description, teamRead.Authorize.String(), teamRead.GetUnitNames()) + checkTeamResponse(t, &apiTeam, teamRead.Name, teamRead.Description, teamRead.IncludesAllRepositories, + teamRead.Authorize.String(), teamRead.GetUnitNames()) // Delete team. req = NewRequestf(t, "DELETE", "/api/v1/teams/%d?token="+token, teamID) @@ -93,19 +100,20 @@ func TestAPITeam(t *testing.T) { models.AssertNotExistsBean(t, &models.Team{ID: teamID}) } -func checkTeamResponse(t *testing.T, apiTeam *api.Team, name, description string, permission string, units []string) { +func checkTeamResponse(t *testing.T, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string) { assert.Equal(t, name, apiTeam.Name, "name") assert.Equal(t, description, apiTeam.Description, "description") + assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories") assert.Equal(t, permission, apiTeam.Permission, "permission") sort.StringSlice(units).Sort() sort.StringSlice(apiTeam.Units).Sort() assert.EqualValues(t, units, apiTeam.Units, "units") } -func checkTeamBean(t *testing.T, id int64, name, description string, permission string, units []string) { +func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission string, units []string) { team := models.AssertExistsAndLoadBean(t, &models.Team{ID: id}).(*models.Team) assert.NoError(t, team.GetUnits(), "GetUnits") - checkTeamResponse(t, convert.ToTeam(team), name, description, permission, units) + checkTeamResponse(t, convert.ToTeam(team), name, description, includesAllRepositories, permission, units) } type TeamSearchResults struct { diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e214f16a2a1..5ed70dc4f5f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -264,6 +264,8 @@ var migrations = []Migration{ NewMigration("Add WhitelistDeployKeys to protected branch", addWhitelistDeployKeysToBranches), // v104 -> v105 NewMigration("remove unnecessary columns from label", removeLabelUneededCols), + // v105 -> v106 + NewMigration("add includes_all_repositories to teams", addTeamIncludesAllRepositories), } // Migrate database to current version diff --git a/models/migrations/v105.go b/models/migrations/v105.go new file mode 100644 index 00000000000..6c9a5817af1 --- /dev/null +++ b/models/migrations/v105.go @@ -0,0 +1,25 @@ +// Copyright 2019 The Gitea 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 migrations + +import ( + "xorm.io/xorm" +) + +func addTeamIncludesAllRepositories(x *xorm.Engine) error { + + type Team struct { + ID int64 `xorm:"pk autoincr"` + IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` + } + + if err := x.Sync2(new(Team)); err != nil { + return err + } + + _, err := x.Exec("UPDATE `team` SET `includes_all_repositories` = ? WHERE `name`=?", + true, "Owners") + return err +} diff --git a/models/org.go b/models/org.go index 2cc302dac60..78b035b1014 100644 --- a/models/org.go +++ b/models/org.go @@ -48,6 +48,9 @@ func (org *User) GetOwnerTeam() (*Team, error) { } func (org *User) getTeams(e Engine) error { + if org.Teams != nil { + return nil + } return e. Where("org_id=?", org.ID). OrderBy("CASE WHEN name LIKE '" + ownerTeamName + "' THEN '' ELSE name END"). @@ -149,11 +152,12 @@ func CreateOrganization(org, owner *User) (err error) { // Create default owner team. t := &Team{ - OrgID: org.ID, - LowerName: strings.ToLower(ownerTeamName), - Name: ownerTeamName, - Authorize: AccessModeOwner, - NumMembers: 1, + OrgID: org.ID, + LowerName: strings.ToLower(ownerTeamName), + Name: ownerTeamName, + Authorize: AccessModeOwner, + NumMembers: 1, + IncludesAllRepositories: true, } if _, err = sess.Insert(t); err != nil { return fmt.Errorf("insert owner team: %v", err) diff --git a/models/org_team.go b/models/org_team.go index a7a179f1044..d740e1c2407 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -22,17 +22,18 @@ const ownerTeamName = "Owners" // Team represents a organization team. type Team struct { - ID int64 `xorm:"pk autoincr"` - OrgID int64 `xorm:"INDEX"` - LowerName string - Name string - Description string - Authorize AccessMode - Repos []*Repository `xorm:"-"` - Members []*User `xorm:"-"` - NumRepos int - NumMembers int - Units []*TeamUnit `xorm:"-"` + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + LowerName string + Name string + Description string + Authorize AccessMode + Repos []*Repository `xorm:"-"` + Members []*User `xorm:"-"` + NumRepos int + NumMembers int + Units []*TeamUnit `xorm:"-"` + IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` } // SearchTeamOptions holds the search options @@ -149,6 +150,9 @@ func (t *Team) IsMember(userID int64) bool { } func (t *Team) getRepositories(e Engine) error { + if t.Repos != nil { + return nil + } return e.Join("INNER", "team_repo", "repository.id = team_repo.repo_id"). Where("team_repo.team_id=?", t.ID). OrderBy("repository.name"). @@ -220,6 +224,25 @@ func (t *Team) addRepository(e Engine, repo *Repository) (err error) { return nil } +// addAllRepositories adds all repositories to the team. +// If the team already has some repositories they will be left unchanged. +func (t *Team) addAllRepositories(e Engine) error { + var orgRepos []Repository + if err := e.Where("owner_id = ?", t.OrgID).Find(&orgRepos); err != nil { + return fmt.Errorf("get org repos: %v", err) + } + + for _, repo := range orgRepos { + if !t.hasRepository(e, repo.ID) { + if err := t.addRepository(e, &repo); err != nil { + return fmt.Errorf("addRepository: %v", err) + } + } + } + + return nil +} + // AddRepository adds new repository to team of organization. func (t *Team) AddRepository(repo *Repository) (err error) { if repo.OwnerID != t.OrgID { @@ -241,6 +264,8 @@ func (t *Team) AddRepository(repo *Repository) (err error) { return sess.Commit() } +// removeRepository removes a repository from a team and recalculates access +// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted) func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (err error) { if err = removeTeamRepo(e, t.ID, repo.ID); err != nil { return err @@ -284,11 +309,16 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e } // RemoveRepository removes repository from team of organization. +// If the team shall include all repositories the request is ignored. func (t *Team) RemoveRepository(repoID int64) error { if !t.HasRepository(repoID) { return nil } + if t.IncludesAllRepositories { + return nil + } + repo, err := GetRepositoryByID(repoID) if err != nil { return err @@ -394,6 +424,14 @@ func NewTeam(t *Team) (err error) { } } + // Add all repositories to the team if it has access to all of them. + if t.IncludesAllRepositories { + err = t.addAllRepositories(sess) + if err != nil { + return fmt.Errorf("addAllRepositories: %v", err) + } + } + // Update organization number of teams. if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil { errRollback := sess.Rollback() @@ -446,7 +484,7 @@ func GetTeamByID(teamID int64) (*Team, error) { } // UpdateTeam updates information of team. -func UpdateTeam(t *Team, authChanged bool) (err error) { +func UpdateTeam(t *Team, authChanged bool, includeAllChanged bool) (err error) { if len(t.Name) == 0 { return errors.New("empty team name") } @@ -511,6 +549,14 @@ func UpdateTeam(t *Team, authChanged bool) (err error) { } } + // Add all repositories to the team if it has access to all of them. + if includeAllChanged && t.IncludesAllRepositories { + err = t.addAllRepositories(sess) + if err != nil { + return fmt.Errorf("addAllRepositories: %v", err) + } + } + return sess.Commit() } diff --git a/models/org_team_test.go b/models/org_team_test.go index 06ab4637d8c..b7e2ef113d3 100644 --- a/models/org_team_test.go +++ b/models/org_team_test.go @@ -5,9 +5,12 @@ package models import ( + "fmt" "strings" "testing" + "code.gitea.io/gitea/modules/structs" + "github.com/stretchr/testify/assert" ) @@ -206,7 +209,7 @@ func TestUpdateTeam(t *testing.T) { team.Name = "newName" team.Description = strings.Repeat("A long description!", 100) team.Authorize = AccessModeAdmin - assert.NoError(t, UpdateTeam(team, true)) + assert.NoError(t, UpdateTeam(team, true, false)) team = AssertExistsAndLoadBean(t, &Team{Name: "newName"}).(*Team) assert.True(t, strings.HasPrefix(team.Description, "A long description!")) @@ -225,7 +228,7 @@ func TestUpdateTeam2(t *testing.T) { team.LowerName = "owners" team.Name = "Owners" team.Description = strings.Repeat("A long description!", 100) - err := UpdateTeam(team, true) + err := UpdateTeam(team, true, false) assert.True(t, IsErrTeamAlreadyExist(err)) CheckConsistencyFor(t, &Team{ID: team.ID}) @@ -374,3 +377,133 @@ func TestUsersInTeamsCount(t *testing.T) { test([]int64{1, 2, 3, 4, 5}, []int64{2, 5}, 2) // userid 2,4 test([]int64{1, 2, 3, 4, 5}, []int64{2, 3, 5}, 3) // userid 2,4,5 } + +func TestIncludesAllRepositoriesTeams(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + testTeamRepositories := func(teamID int64, repoIds []int64) { + team := AssertExistsAndLoadBean(t, &Team{ID: teamID}).(*Team) + assert.NoError(t, team.GetRepositories(), "%s: GetRepositories", team.Name) + assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) + assert.Equal(t, len(repoIds), len(team.Repos), "%s: repo count", team.Name) + for i, rid := range repoIds { + if rid > 0 { + assert.True(t, team.HasRepository(rid), "%s: HasRepository(%d) %d", rid, i) + } + } + } + + // Get an admin user. + user, err := GetUserByID(1) + assert.NoError(t, err, "GetUserByID") + + // Create org. + org := &User{ + Name: "All repo", + IsActive: true, + Type: UserTypeOrganization, + Visibility: structs.VisibleTypePublic, + } + assert.NoError(t, CreateOrganization(org, user), "CreateOrganization") + + // Check Owner team. + ownerTeam, err := org.GetOwnerTeam() + assert.NoError(t, err, "GetOwnerTeam") + assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") + + // Create repos. + repoIds := make([]int64, 0) + for i := 0; i < 3; i++ { + r, err := CreateRepository(user, org, CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) + assert.NoError(t, err, "CreateRepository %d", i) + if r != nil { + repoIds = append(repoIds, r.ID) + } + } + // Get fresh copy of Owner team after creating repos. + ownerTeam, err = org.GetOwnerTeam() + assert.NoError(t, err, "GetOwnerTeam") + + // Create teams and check repositories. + teams := []*Team{ + ownerTeam, + { + OrgID: org.ID, + Name: "team one", + Authorize: AccessModeRead, + IncludesAllRepositories: true, + }, + { + OrgID: org.ID, + Name: "team 2", + Authorize: AccessModeRead, + IncludesAllRepositories: false, + }, + { + OrgID: org.ID, + Name: "team three", + Authorize: AccessModeWrite, + IncludesAllRepositories: true, + }, + { + OrgID: org.ID, + Name: "team 4", + Authorize: AccessModeWrite, + IncludesAllRepositories: false, + }, + } + teamRepos := [][]int64{ + repoIds, + repoIds, + {}, + repoIds, + {}, + } + for i, team := range teams { + if i > 0 { // first team is Owner. + assert.NoError(t, NewTeam(team), "%s: NewTeam", team.Name) + } + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Update teams and check repositories. + teams[3].IncludesAllRepositories = false + teams[4].IncludesAllRepositories = true + teamRepos[4] = repoIds + for i, team := range teams { + assert.NoError(t, UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name) + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Create repo and check teams repositories. + org.Teams = nil // Reset teams to allow their reloading. + r, err := CreateRepository(user, org, CreateRepoOptions{Name: "repo-last"}) + assert.NoError(t, err, "CreateRepository last") + if r != nil { + repoIds = append(repoIds, r.ID) + } + teamRepos[0] = repoIds + teamRepos[1] = repoIds + teamRepos[4] = repoIds + for i, team := range teams { + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Remove repo and check teams repositories. + assert.NoError(t, DeleteRepository(user, org.ID, repoIds[0]), "DeleteRepository") + teamRepos[0] = repoIds[1:] + teamRepos[1] = repoIds[1:] + teamRepos[3] = repoIds[1:3] + teamRepos[4] = repoIds[1:] + for i, team := range teams { + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Wipe created items. + for i, rid := range repoIds { + if i > 0 { // first repo already deleted. + assert.NoError(t, DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i) + } + } + assert.NoError(t, DeleteOrganization(org), "DeleteOrganization") +} diff --git a/models/repo.go b/models/repo.go index 7945cb309d3..89e579d1ec8 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1447,14 +1447,17 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err } u.NumRepos++ - // Give access to all members in owner team. + // Give access to all members in teams with access to all repositories. if u.IsOrganization() { - t, err := u.getOwnerTeam(e) - if err != nil { - return fmt.Errorf("getOwnerTeam: %v", err) + if err := u.GetTeams(); err != nil { + return fmt.Errorf("GetTeams: %v", err) } - if err = t.addRepository(e, repo); err != nil { - return fmt.Errorf("addRepository: %v", err) + for _, t := range u.Teams { + if t.IncludesAllRepositories { + if err := t.addRepository(e, repo); err != nil { + return fmt.Errorf("addRepository: %v", err) + } + } } } else if err = repo.recalculateAccesses(e); err != nil { // Organization automatically called this in addRepository method. @@ -1641,11 +1644,15 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error } if newOwner.IsOrganization() { - t, err := newOwner.getOwnerTeam(sess) - if err != nil { - return fmt.Errorf("getOwnerTeam: %v", err) - } else if err = t.addRepository(sess, repo); err != nil { - return fmt.Errorf("add to owner team: %v", err) + if err := newOwner.GetTeams(); err != nil { + return fmt.Errorf("GetTeams: %v", err) + } + for _, t := range newOwner.Teams { + if t.IncludesAllRepositories { + if err := t.addRepository(sess, repo); err != nil { + return fmt.Errorf("addRepository: %v", err) + } + } } } else if err = repo.recalculateAccesses(sess); err != nil { // Organization called this in addRepository method. diff --git a/modules/auth/org.go b/modules/auth/org.go index 94e659cb5bd..509358882a3 100644 --- a/modules/auth/org.go +++ b/modules/auth/org.go @@ -62,6 +62,7 @@ type CreateTeamForm struct { Description string `binding:"MaxSize(255)"` Permission string Units []models.UnitType + RepoAccess string } // Validate validates the fields diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index bea4a10ad49..5053468b4a5 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -7,10 +7,11 @@ package structs // Team represents a team in an organization type Team struct { - ID int64 `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Organization *Organization `json:"organization"` + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Organization *Organization `json:"organization"` + IncludesAllRepositories bool `json:"includes_all_repositories"` // enum: none,read,write,admin,owner Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] @@ -20,8 +21,9 @@ type Team struct { // CreateTeamOption options for creating a team type CreateTeamOption struct { // required: true - Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` - Description string `json:"description" binding:"MaxSize(255)"` + Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` + Description string `json:"description" binding:"MaxSize(255)"` + IncludesAllRepositories bool `json:"includes_all_repositories"` // enum: read,write,admin Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] @@ -31,8 +33,9 @@ type CreateTeamOption struct { // EditTeamOption options for editing a team type EditTeamOption struct { // required: true - Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` - Description string `json:"description" binding:"MaxSize(255)"` + Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` + Description string `json:"description" binding:"MaxSize(255)"` + IncludesAllRepositories bool `json:"includes_all_repositories"` // enum: read,write,admin Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index db28fcede03..932d0bceac9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1515,6 +1515,7 @@ team_name = Team Name team_desc = Description team_name_helper = Team names should be short and memorable. team_desc_helper = Describe the purpose or role of the team. +team_access_desc = Repository access team_permission_desc = Permission team_unit_desc = Allow Access to Repository Sections @@ -1588,6 +1589,13 @@ teams.add_nonexistent_repo = "The repository you're trying to add does not exist teams.add_duplicate_users = User is already a team member. teams.repos.none = No repositories could be accessed by this team. teams.members.none = No members on this team. +teams.specific_repositories = Specific repositories +teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this will not automatically remove repositories already added with All repositories. +teams.all_repositories = All repositories +teams.all_repositories_helper = Team has access to all repositories. Selecting this will add all existing repositories to the team. +teams.all_repositories_read_permission_desc = This team grants Read access to all repositories: members can view and clone repositories. +teams.all_repositories_write_permission_desc = This team grants Write access to all repositories: members can read from and push to repositories. +teams.all_repositories_admin_permission_desc = This team grants Admin access to all repositories: members can read from, push to and add collaborators to repositories. [admin] dashboard = Dashboard diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go index 6da53d62750..f52ed63476d 100644 --- a/routers/api/v1/convert/convert.go +++ b/routers/api/v1/convert/convert.go @@ -227,11 +227,12 @@ func ToOrganization(org *models.User) *api.Organization { // ToTeam convert models.Team to api.Team func ToTeam(team *models.Team) *api.Team { return &api.Team{ - ID: team.ID, - Name: team.Name, - Description: team.Description, - Permission: team.Authorize.String(), - Units: team.GetUnitNames(), + ID: team.ID, + Name: team.Name, + Description: team.Description, + IncludesAllRepositories: team.IncludesAllRepositories, + Permission: team.Authorize.String(), + Units: team.GetUnitNames(), } } diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index d01f051626f..a22b60a2c68 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -128,10 +128,11 @@ func CreateTeam(ctx *context.APIContext, form api.CreateTeamOption) { // "201": // "$ref": "#/responses/Team" team := &models.Team{ - OrgID: ctx.Org.Organization.ID, - Name: form.Name, - Description: form.Description, - Authorize: models.ParseAccessMode(form.Permission), + OrgID: ctx.Org.Organization.ID, + Name: form.Name, + Description: form.Description, + IncludesAllRepositories: form.IncludesAllRepositories, + Authorize: models.ParseAccessMode(form.Permission), } unitTypes := models.FindUnitTypes(form.Units...) @@ -182,11 +183,27 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) { // "200": // "$ref": "#/responses/Team" team := ctx.Org.Team - team.Name = form.Name team.Description = form.Description - team.Authorize = models.ParseAccessMode(form.Permission) unitTypes := models.FindUnitTypes(form.Units...) + isAuthChanged := false + isIncludeAllChanged := false + if !team.IsOwnerTeam() { + // Validate permission level. + auth := models.ParseAccessMode(form.Permission) + + team.Name = form.Name + if team.Authorize != auth { + isAuthChanged = true + team.Authorize = auth + } + + if team.IncludesAllRepositories != form.IncludesAllRepositories { + isIncludeAllChanged = true + team.IncludesAllRepositories = form.IncludesAllRepositories + } + } + if team.Authorize < models.AccessModeOwner { var units = make([]*models.TeamUnit, 0, len(form.Units)) for _, tp := range unitTypes { @@ -198,7 +215,7 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) { team.Units = units } - if err := models.UpdateTeam(team, true); err != nil { + if err := models.UpdateTeam(team, isAuthChanged, isIncludeAllChanged); err != nil { ctx.Error(500, "EditTeam", err) return } diff --git a/routers/org/teams.go b/routers/org/teams.go index 7ead6ea5ff8..24612459a4f 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -1,4 +1,5 @@ // Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -180,12 +181,14 @@ func NewTeamPost(ctx *context.Context, form auth.CreateTeamForm) { ctx.Data["PageIsOrgTeams"] = true ctx.Data["PageIsOrgTeamsNew"] = true ctx.Data["Units"] = models.Units + var includesAllRepositories = (form.RepoAccess == "all") t := &models.Team{ - OrgID: ctx.Org.Organization.ID, - Name: form.TeamName, - Description: form.Description, - Authorize: models.ParseAccessMode(form.Permission), + OrgID: ctx.Org.Organization.ID, + Name: form.TeamName, + Description: form.Description, + Authorize: models.ParseAccessMode(form.Permission), + IncludesAllRepositories: includesAllRepositories, } if t.Authorize < models.AccessModeOwner { @@ -268,6 +271,8 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) { ctx.Data["Units"] = models.Units isAuthChanged := false + isIncludeAllChanged := false + var includesAllRepositories = (form.RepoAccess == "all") if !t.IsOwnerTeam() { // Validate permission level. auth := models.ParseAccessMode(form.Permission) @@ -277,6 +282,11 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) { isAuthChanged = true t.Authorize = auth } + + if t.IncludesAllRepositories != includesAllRepositories { + isIncludeAllChanged = true + t.IncludesAllRepositories = includesAllRepositories + } } t.Description = form.Description if t.Authorize < models.AccessModeOwner { @@ -305,7 +315,7 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) { return } - if err := models.UpdateTeam(t, isAuthChanged); err != nil { + if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil { ctx.Data["Err_TeamName"] = true switch { case models.IsErrTeamAlreadyExist(err): diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index fb79c9b7fba..e50a1777d20 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -24,6 +24,24 @@ {{.i18n.Tr "org.team_desc_helper"}} {{if not (eq .Team.LowerName "owners")}} +
+ +
+
+
+ + + {{.i18n.Tr "org.teams.specific_repositories_helper"}} +
+
+
+
+ + + {{.i18n.Tr "org.teams.all_repositories_helper"}} +
+
+

diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index eeb86564fd4..1b2a411c2bb 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -7,7 +7,7 @@ {{template "org/team/sidebar" .}}
{{template "org/team/navbar" .}} - {{$canAddRemove := and $.IsOrganizationOwner (not (eq $.Team.LowerName "owners"))}} + {{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}} {{if $canAddRemove}}
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 846613e32e7..dd189df5f31 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -22,11 +22,23 @@ {{if eq .Team.LowerName "owners"}} {{.i18n.Tr "org.teams.owners_permission_desc" | Str2html}} {{else if (eq .Team.Authorize 1)}} - {{.i18n.Tr "org.teams.read_permission_desc" | Str2html}} + {{if .Team.IncludesAllRepositories}} + {{.i18n.Tr "org.teams.all_repositories_read_permission_desc" | Str2html}} + {{else}} + {{.i18n.Tr "org.teams.read_permission_desc" | Str2html}} + {{end}} {{else if (eq .Team.Authorize 2)}} - {{.i18n.Tr "org.teams.write_permission_desc" | Str2html}} + {{if .Team.IncludesAllRepositories}} + {{.i18n.Tr "org.teams.all_repositories_write_permission_desc" | Str2html}} + {{else}} + {{.i18n.Tr "org.teams.write_permission_desc" | Str2html}} + {{end}} {{else if (eq .Team.Authorize 3)}} - {{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}} + {{if .Team.IncludesAllRepositories}} + {{.i18n.Tr "org.teams.all_repositories_admin_permission_desc" | Str2html}} + {{else}} + {{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}} + {{end}} {{end}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 17b8eab6c5e..dc162bc37d3 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -8242,6 +8242,10 @@ "type": "string", "x-go-name": "Description" }, + "includes_all_repositories": { + "type": "boolean", + "x-go-name": "IncludesAllRepositories" + }, "name": { "type": "string", "x-go-name": "Name" @@ -8801,6 +8805,10 @@ "type": "string", "x-go-name": "Description" }, + "includes_all_repositories": { + "type": "boolean", + "x-go-name": "IncludesAllRepositories" + }, "name": { "type": "string", "x-go-name": "Name" @@ -10457,6 +10465,10 @@ "format": "int64", "x-go-name": "ID" }, + "includes_all_repositories": { + "type": "boolean", + "x-go-name": "IncludesAllRepositories" + }, "name": { "type": "string", "x-go-name": "Name" From 12170d26a7fc75beef46311996db3798d7ad7c1c Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 6 Nov 2019 21:39:29 +0800 Subject: [PATCH 05/11] Split sendCreateCommentAction as two parts, one for update comment related informations, another for actions (#8784) * Split sendCreateCommentAction as two parts, one for update comment related informations, another for actions * fix lint --- models/issue_comment.go | 94 +++++++++++++++++++++++------------------ models/review.go | 8 +++- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/models/issue_comment.go b/models/issue_comment.go index 90bb8c53acf..63f5f6b7788 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -535,6 +535,10 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err return nil, err } + if err = updateCommentInfos(e, opts, comment); err != nil { + return nil, err + } + if err = sendCreateCommentAction(e, opts, comment); err != nil { return nil, err } @@ -546,6 +550,56 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err return comment, nil } +func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) { + // Check comment type. + switch opts.Type { + case CommentTypeCode: + if comment.ReviewID != 0 { + if comment.Review == nil { + if err := comment.loadReview(e); err != nil { + return err + } + } + if comment.Review.Type <= ReviewTypePending { + return nil + } + } + fallthrough + case CommentTypeComment: + if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { + return err + } + + // Check attachments + attachments := make([]*Attachment, 0, len(opts.Attachments)) + for _, uuid := range opts.Attachments { + attach, err := getAttachmentByUUID(e, uuid) + if err != nil { + if IsErrAttachmentNotExist(err) { + continue + } + return fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err) + } + attachments = append(attachments, attach) + } + + for i := range attachments { + attachments[i].IssueID = opts.Issue.ID + attachments[i].CommentID = comment.ID + // No assign value could be 0, so ignore AllCols(). + if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { + return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err) + } + } + case CommentTypeReopen, CommentTypeClose: + if err = opts.Issue.updateClosedNum(e); err != nil { + return err + } + } + // update the issue's updated_unix column + return updateIssueCols(e, opts.Issue, "updated_unix") +} + func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) { // Compose comment action, could be plain comment, close or reopen issue/pull request. // This object will be used to notify watchers in the end of function. @@ -575,56 +629,16 @@ func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, commen fallthrough case CommentTypeComment: act.OpType = ActionCommentIssue - - if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { - return err - } - - // Check attachments - attachments := make([]*Attachment, 0, len(opts.Attachments)) - for _, uuid := range opts.Attachments { - attach, err := getAttachmentByUUID(e, uuid) - if err != nil { - if IsErrAttachmentNotExist(err) { - continue - } - return fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err) - } - attachments = append(attachments, attach) - } - - for i := range attachments { - attachments[i].IssueID = opts.Issue.ID - attachments[i].CommentID = comment.ID - // No assign value could be 0, so ignore AllCols(). - if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err) - } - } - case CommentTypeReopen: act.OpType = ActionReopenIssue if opts.Issue.IsPull { act.OpType = ActionReopenPullRequest } - - if err = opts.Issue.updateClosedNum(e); err != nil { - return err - } - case CommentTypeClose: act.OpType = ActionCloseIssue if opts.Issue.IsPull { act.OpType = ActionClosePullRequest } - - if err = opts.Issue.updateClosedNum(e); err != nil { - return err - } - } - // update the issue's updated_unix column - if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil { - return err } // Notify watchers for whatever action comes in, ignore if no action type. if act.OpType > 0 { diff --git a/models/review.go b/models/review.go index 58660b2e3d0..89a26d6fdb9 100644 --- a/models/review.go +++ b/models/review.go @@ -129,13 +129,17 @@ func (r *Review) publish(e *xorm.Engine) error { go func(en *xorm.Engine, review *Review, comm *Comment) { sess := en.NewSession() defer sess.Close() - if err := sendCreateCommentAction(sess, &CreateCommentOptions{ + opts := &CreateCommentOptions{ Doer: comm.Poster, Issue: review.Issue, Repo: review.Issue.Repo, Type: comm.Type, Content: comm.Content, - }, comm); err != nil { + } + if err := updateCommentInfos(sess, opts, comm); err != nil { + log.Warn("updateCommentInfos: %v", err) + } + if err := sendCreateCommentAction(sess, opts, comm); err != nil { log.Warn("sendCreateCommentAction: %v", err) } }(e, r, comment) From f05cd3e31792859156ac3b77954f9f343bb3620e Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 6 Nov 2019 13:42:48 +0000 Subject: [PATCH 06/11] [skip ci] Updated translations via Crowdin --- options/locale/locale_pl-PL.ini | 7 ++++--- options/locale/locale_pt-BR.ini | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 50eed5558d7..71c9522921f 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -74,6 +74,7 @@ preview=Podgląd loading=Ładowanie… [startpage] +app_desc=Bezbolesna usługa Git na własnym serwerze [install] install=Instalacja @@ -282,9 +283,9 @@ AuthName=Nazwa autoryzacji AdminEmail=E-mail administratora NewBranchName=Nazwa nowej gałęzi -CommitSummary=Podsumowanie commitu -CommitMessage=Wiadomość commitu -CommitChoice=Wybór commitu +CommitSummary=Podsumowanie commita +CommitMessage=Wiadomość commita +CommitChoice=Wybór commita TreeName=Ścieżka pliku Content=Treść diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index df37b233577..33da0ed0c41 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1514,6 +1514,7 @@ team_name=Nome da equipe team_desc=Descrição team_name_helper=Nomes de equipe devem ser curtos e memoráveis. team_desc_helper=Descreva a finalidade ou o papel da equipe. +team_access_desc=Acesso ao repositório team_permission_desc=Permissão team_unit_desc=Permitir o acesso a seções de repositório @@ -1587,6 +1588,13 @@ teams.add_nonexistent_repo=O repositório que você está tentando adicionar nã teams.add_duplicate_users=Usuário já é um membro da equipe. teams.repos.none=Nenhum repositório pode ser acessado por essa equipe. teams.members.none=Nenhum membro nesta equipe. +teams.specific_repositories=Repositórios específicos +teams.specific_repositories_helper=Os membros terão acesso apenas aos repositórios explicitamente adicionados à equipe. Selecionar este não removerá automaticamente os repositórios já adicionados com Todos os repositórios. +teams.all_repositories=Todos os repositórios +teams.all_repositories_helper=A equipe tem acesso a todos os repositórios. Selecionar isto irá adicionar todos os repositórios existentes à equipe. +teams.all_repositories_read_permission_desc=Esta equipe concede acesso Leitura a todos os repositórios: membros podem ver e clonar repositórios. +teams.all_repositories_write_permission_desc=Esta equipe concede acesso Escrita a todos os repositórios: os membros podem ler de e fazer push para os repositórios. +teams.all_repositories_admin_permission_desc=Esta equipe concede acesso Administrativo a todos os repositórios: os membros podem ler, fazer push e adicionar colaboradores aos repositórios. [admin] dashboard=Painel From d5b1e6bc51f87eb1be07a4682798428bf4bbb9ce Mon Sep 17 00:00:00 2001 From: Damien Pollet Date: Wed, 6 Nov 2019 18:23:06 +0100 Subject: [PATCH 07/11] Theme arc-green: reverse heatmap colors (#8840) * Theme arc-green: reverse heatmap colors This uses the same colors as the updated palette in the base theme. See #8709 and #5864, in particular [my comment showing the problem](https://github.com/go-gitea/gitea/issues/5864#issuecomment-462334171) * Rebuild CSS * Use link color as hot, interpolate between hot and cold colors * Use color from a:hover --- public/css/theme-arc-green.css | 5 +++++ public/less/themes/arc-green.less | 28 +++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/public/css/theme-arc-green.css b/public/css/theme-arc-green.css index 28a127a3dd7..74a7c3ddc31 100644 --- a/public/css/theme-arc-green.css +++ b/public/css/theme-arc-green.css @@ -249,6 +249,11 @@ a.ui.label:hover,a.ui.labels .label:hover{background-color:#505667!important;col .xdsoft_datetimepicker .xdsoft_datepicker .xdsoft_calendar td,.xdsoft_datetimepicker .xdsoft_datepicker .xdsoft_calendar th{border-color:#4c505c;background-color:#2a2e39} .xdsoft_datetimepicker .xdsoft_datepicker .xdsoft_calendar td.xdsoft_disabled,.xdsoft_datetimepicker .xdsoft_datepicker .xdsoft_calendar td.xdsoft_other_month{opacity:.8;background:#a0cc75;color:#000} .heatmap-color-0{background-color:#2d303b} +.heatmap-color-1{background-color:#444f47} +.heatmap-color-2{background-color:#5b6e52} +.heatmap-color-3{background-color:#728e5e} +.heatmap-color-4{background-color:#89ad69} +.heatmap-color-5{background-color:#a0cc75} .CodeMirror{color:#9daccc;background-color:#2b2b2b;border-top:0} .CodeMirror div.CodeMirror-cursor{border-left:1px solid #9e9e9e} .CodeMirror .CodeMirror-gutters{background-color:#2b2b2b} diff --git a/public/less/themes/arc-green.less b/public/less/themes/arc-green.less index 27c32728a2f..06e7c78d41b 100644 --- a/public/less/themes/arc-green.less +++ b/public/less/themes/arc-green.less @@ -1294,8 +1294,34 @@ a.ui.labels .label:hover { } } +.heatmap(@heat) { + @heatmap-cold: #2d303b; + @heatmap-hot: #a0cc75; + background-color: mix(@heatmap-hot, @heatmap-cold, @heat); +} + .heatmap-color-0 { - background-color: #2d303b; + .heatmap(0%); +} + +.heatmap-color-1 { + .heatmap(20%); +} + +.heatmap-color-2 { + .heatmap(40%); +} + +.heatmap-color-3 { + .heatmap(60%); +} + +.heatmap-color-4 { + .heatmap(80%); +} + +.heatmap-color-5 { + .heatmap(100%); } /* code mirror dark theme */ From 1f90147f3942065e2a7f564e8a3c97d23d41e6c0 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Thu, 7 Nov 2019 10:34:28 -0300 Subject: [PATCH 08/11] Use templates for issue e-mail subject and body (#8329) * Add template capability for issue mail subject * Remove test string * Fix trim subject length * Add comment to template and run make fmt * Add information for the template * Rename defaultMailSubject() to fallbackMailSubject() * General rewrite of the mail template code * Fix .Doer name * Use text/template for subject instead of html * Fix subject Re: prefix * Fix mail tests * Fix static templates * [skip ci] Updated translations via Crowdin * Expose db.SetMaxOpenConns and allow non MySQL dbs to set conn pool params (#8528) * Expose db.SetMaxOpenConns and allow other dbs to set their connection params * Add note about port exhaustion Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Prevent .code-view from overriding font on icon fonts (#8614) * Correct some outdated statements in the contributing guidelines (#8612) * More information for drone-cli in CONTRIBUTING.md * Increases the version of drone-cli to 1.2.0 * Adds a note for the Docker Toolbox on Windows Signed-off-by: LukBukkit * Fix the url for the blog repository (now on gitea.com) Signed-off-by: LukBukkit * Remove TrN due to lack of lang context * Redo templates to match previous code * Fix extra character in template * Unify PR & Issue tempaltes, fix format * Remove default subject * Add template tests * Fix template * Remove replaced function * Provide User as models.User for better consistency * Add docs * Fix doc inaccuracies, improve examples * Change mail footer to math AppName * Add test for mail subject/body template separation * Add support for code review comments * Update docs/content/doc/advanced/mail-templates-us.md Co-Authored-By: 6543 <24977596+6543@users.noreply.github.com> --- .../content/doc/advanced/mail-templates-us.md | 272 ++++++++++++++++++ modules/templates/dynamic.go | 33 +-- modules/templates/helper.go | 130 +++++++++ modules/templates/helper_test.go | 55 ++++ modules/templates/static.go | 23 +- services/mailer/mail.go | 175 ++++++++--- services/mailer/mail_comment.go | 20 +- services/mailer/mail_issue.go | 36 +-- services/mailer/mail_test.go | 131 ++++++++- templates/mail/issue/assigned.tmpl | 4 +- templates/mail/issue/comment.tmpl | 16 -- templates/mail/issue/default.tmpl | 31 ++ templates/mail/issue/mention.tmpl | 17 -- 13 files changed, 781 insertions(+), 162 deletions(-) create mode 100644 docs/content/doc/advanced/mail-templates-us.md create mode 100644 modules/templates/helper_test.go delete mode 100644 templates/mail/issue/comment.tmpl create mode 100644 templates/mail/issue/default.tmpl delete mode 100644 templates/mail/issue/mention.tmpl diff --git a/docs/content/doc/advanced/mail-templates-us.md b/docs/content/doc/advanced/mail-templates-us.md new file mode 100644 index 00000000000..ffe2d4a27bc --- /dev/null +++ b/docs/content/doc/advanced/mail-templates-us.md @@ -0,0 +1,272 @@ +--- +date: "2019-10-23T17:00:00-03:00" +title: "Mail templates" +slug: "mail-templates" +weight: 45 +toc: true +draft: false +menu: + sidebar: + parent: "advanced" + name: "Mail templates" + weight: 45 + identifier: "mail-templates" +--- + +# Mail templates + +To craft the e-mail subject and contents for certain operations, Gitea can be customized by using templates. The templates +for these functions are located under the [`custom` directory](https://docs.gitea.io/en-us/customizing-gitea/). +Gitea has an internal template that serves as default in case there's no custom alternative. + +Custom templates are loaded when Gitea starts. Changes made to them are not recognized until Gitea is restarted again. + +## Mail notifications supporting templates + +Currently, the following notification events make use of templates: + +| Action name | Usage | +|---------------|--------------------------------------------------------------------------------------------------------------| +| `new` | A new issue or pull request was created. | +| `comment` | A new comment was created in an existing issue or pull request. | +| `close` | An issue or pull request was closed. | +| `reopen` | An issue or pull request was reopened. | +| `review` | The head comment of a review in a pull request. | +| `code` | A single comment on the code of a pull request. | +| `assigned` | Used was assigned to an issue or pull request. | +| `default` | Any action not included in the above categories, or when the corresponding category template is not present. | + +The path for the template of a particular message type is: + +``` +custom/templates/mail/{action type}/{action name}.tmpl +``` + +Where `{action type}` is one of `issue` or `pull` (for pull requests), and `{action name}` is one of the names listed above. + +For example, the specific template for a mail regarding a comment in a pull request is: +``` +custom/templates/mail/pull/comment.tmpl +``` + +However, creating templates for each and every action type/name combination is not required. +A fallback system is used to choose the appropriate template for an event. The _first existing_ +template on this list is used: + +* The specific template for the desired **action type** and **action name**. +* The template for action type `issue` and the desired **action name**. +* The template for the desired **action type**, action name `default`. +* The template for action type `issue`, action name `default`. + +The only mandatory template is action type `issue`, action name `default`, which is already embedded in Gitea +unless it's overridden by the user in the `custom` directory. + +## Template syntax + +Mail templates are UTF-8 encoded text files that need to follow one of the following formats: + +``` +Text and macros for the subject line +------------ +Text and macros for the mail body +``` + +or + +``` +Text and macros for the mail body +``` + +Specifying a _subject_ section is optional (and therefore also the dash line separator). When used, the separator between +_subject_ and _mail body_ templates requires at least three dashes; no other characters are allowed in the separator line. + + +_Subject_ and _mail body_ are parsed by [Golang's template engine](https://golang.org/pkg/text/template/) and +are provided with a _metadata context_ assembled for each notification. The context contains the following elements: + +| Name | Type | Available | Usage | +|--------------------|----------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `.FallbackSubject` | string | Always | A default subject line. See Below. | +| `.Subject` | string | Only in body | The _subject_, once resolved. | +| `.Body` | string | Always | The message of the issue, pull request or comment, parsed from Markdown into HTML and sanitized. Do not confuse with the _mail body_ | +| `.Link` | string | Always | The address of the originating issue, pull request or comment. | +| `.Issue` | models.Issue | Always | The issue (or pull request) originating the notification. To get data specific to a pull request (e.g. `HasMerged`), `.Issue.PullRequest` can be used, but care should be taken as this field will be `nil` if the issue is *not* a pull request. | +| `.Comment` | models.Comment | If applicable | If the notification is from a comment added to an issue or pull request, this will contain the information about the comment. | +| `.IsPull` | bool | Always | `true` if the mail notification is associated with a pull request (i.e. `.Issue.PullRequest` is not `nil`). | +| `.Repo` | string | Always | Name of the repository, including owner name (e.g. `mike/stuff`) | +| `.User` | models.User | Always | Owner of the repository from which the event originated. To get the user name (e.g. `mike`),`.User.Name` can be used. | +| `.Doer` | models.User | Always | User that executed the action triggering the notification event. To get the user name (e.g. `rhonda`), `.Doer.Name` can be used. | +| `.IsMention` | bool | Always | `true` if this notification was only generated because the user was mentioned in the comment, while not being subscribed to the source. It will be `false` if the recipient was subscribed to the issue or repository. | +| `.SubjectPrefix` | string | Always | `Re: ` if the notification is about other than issue or pull request creation; otherwise an empty string. | +| `.ActionType` | string | Always | `"issue"` or `"pull"`. Will correspond to the actual _action type_ independently of which template was selected. | +| `.ActionName` | string | Always | It will be one of the action types described above (`new`, `comment`, etc.), and will correspond to the actual _action name_ independently of which template was selected. | + +All names are case sensitive. + +### The _subject_ part of the template + +The template engine used for the mail _subject_ is golang's [`text/template`](https://golang.org/pkg/text/template/). +Please refer to the linked documentation for details about its syntax. + +The _subject_ is built using the following steps: + +* A template is selected according to the type of notification and to what templates are present. +* The template is parsed and resolved (e.g. `{{.Issue.Index}}` is converted to the number of the issue + or pull request). +* All space-like characters (e.g. `TAB`, `LF`, etc.) are converted to normal spaces. +* All leading, trailing and redundant spaces are removed. +* The string is truncated to its first 256 runes (characters). + +If the end result is an empty string, **or** no subject template was available (i.e. the selected template +did not include a subject part), Gitea's **internal default** will be used. + +The internal default (fallback) subject is the equivalent of: + +``` +{{.SubjectPrefix}}[{{.Repo}}] {{.Issue.Title}} (#.Issue.Index) +``` + +For example: `Re: [mike/stuff] New color palette (#38)` + +Gitea's default subject can also be found in the template _metadata_ as `.FallbackSubject` from any of +the two templates, even if a valid subject template is present. + +### The _mail body_ part of the template + +The template engine used for the _mail body_ is golang's [`html/template`](https://golang.org/pkg/html/template/). +Please refer to the linked documentation for details about its syntax. + +The _mail body_ is parsed after the mail subject, so there is an additional _metadata_ field which is +the actual rendered subject, after all considerations. + +The expected result is HTML (including structural elements like``, ``, etc.). Styling +through `