forked from gitea/gitea
Merge branch 'master' into refactor_issues-subscription
This commit is contained in:
commit
d667e5920f
|
@ -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`<html>`, `<body>`, etc.). Styling
|
||||||
|
through `<style>` blocks, `class` and `style` attributes is possible. However, `html/template`
|
||||||
|
does some [automatic escaping](https://golang.org/pkg/html/template/#hdr-Contexts) that should be considered.
|
||||||
|
|
||||||
|
Attachments (such as images or external style sheets) are not supported. However, other templates can
|
||||||
|
be referenced too, for example to provide the contents of a `<style>` element in a centralized fashion.
|
||||||
|
The external template must be placed under `custom/mail` and referenced relative to that directory.
|
||||||
|
For example, `custom/mail/styles/base.tmpl` can be included using `{{template styles/base}}`.
|
||||||
|
|
||||||
|
The mail is sent with `Content-Type: multipart/alternative`, so the body is sent in both HTML
|
||||||
|
and text formats. The latter is obtained by stripping the HTML markup.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
How a mail is rendered is directly dependent on the capabilities of the mail application. Many mail
|
||||||
|
clients don't even support HTML, so they show the text version included in the generated mail.
|
||||||
|
|
||||||
|
If the template fails to render, it will be noticed only at the moment the mail is sent.
|
||||||
|
A default subject is used if the subject template fails, and whatever was rendered successfully
|
||||||
|
from the the _mail body_ is used, disregarding the rest.
|
||||||
|
|
||||||
|
Please check [Gitea's logs](https://docs.gitea.io/en-us/logging-configuration/) for error messages in case of trouble.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
`custom/templates/mail/issue/default.tmpl`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[{{.Repo}}] @{{.Doer.Name}}
|
||||||
|
{{if eq .ActionName "new"}}
|
||||||
|
created
|
||||||
|
{{else if eq .ActionName "comment"}}
|
||||||
|
commented on
|
||||||
|
{{else if eq .ActionName "close"}}
|
||||||
|
closed
|
||||||
|
{{else if eq .ActionName "reopen"}}
|
||||||
|
reopened
|
||||||
|
{{else}}
|
||||||
|
updated
|
||||||
|
{{end}}
|
||||||
|
{{if eq .ActionType "issue"}}
|
||||||
|
issue
|
||||||
|
{{else}}
|
||||||
|
pull request
|
||||||
|
{{end}}
|
||||||
|
#{{.Issue.Index}}: {{.Issue.Title}}
|
||||||
|
------------
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<title>{{.Subject}}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{if .IsMention}}
|
||||||
|
<p>
|
||||||
|
You are receiving this because @{{.Doer.Name}} mentioned you.
|
||||||
|
</p>
|
||||||
|
{{end}}
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
<a href="{{AppURL}}/{{.Doer.LowerName}}">@{{.Doer.Name}}</a>
|
||||||
|
{{if not (eq .Doer.FullName "")}}
|
||||||
|
({{.Doer.FullName}})
|
||||||
|
{{end}}
|
||||||
|
{{if eq .ActionName "new"}}
|
||||||
|
created
|
||||||
|
{{else if eq .ActionName "close"}}
|
||||||
|
closed
|
||||||
|
{{else if eq .ActionName "reopen"}}
|
||||||
|
reopened
|
||||||
|
{{else}}
|
||||||
|
updated
|
||||||
|
{{end}}
|
||||||
|
<a href="{{.Link}}">{{.Repo}}#{{.Issue.Index}}</a>.
|
||||||
|
</p>
|
||||||
|
{{if not (eq .Body "")}}
|
||||||
|
<h3>Message content:</h3>
|
||||||
|
<hr>
|
||||||
|
{{.Body | Str2html}}
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
<a href="{{.Link}}">View it on Gitea</a>.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
This template produces something along these lines:
|
||||||
|
|
||||||
|
#### Subject
|
||||||
|
|
||||||
|
> [mike/stuff] @rhonda commented on pull request #38: New color palette
|
||||||
|
|
||||||
|
#### Mail body
|
||||||
|
|
||||||
|
> [@rhonda](#) (Rhonda Myers) updated [mike/stuff#38](#).
|
||||||
|
>
|
||||||
|
> #### Message content:
|
||||||
|
>
|
||||||
|
> \__________________________________________________________________
|
||||||
|
>
|
||||||
|
> Mike, I think we should tone down the blues a little.
|
||||||
|
> \__________________________________________________________________
|
||||||
|
>
|
||||||
|
> [View it on Gitea](#).
|
||||||
|
|
||||||
|
## Advanced
|
||||||
|
|
||||||
|
The template system contains several functions that can be used to further process and format
|
||||||
|
the messages. Here's a list of some of them:
|
||||||
|
|
||||||
|
| Name | Parameters | Available | Usage |
|
||||||
|
|----------------------|-------------|-----------|---------------------------------------------------------------------|
|
||||||
|
| `AppUrl` | - | Any | Gitea's URL |
|
||||||
|
| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" |
|
||||||
|
| `AppDomain` | - | Any | Gitea's host name |
|
||||||
|
| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed |
|
||||||
|
| `Str2html` | string | Body only | Sanitizes text by removing any HTML tags from it. |
|
||||||
|
|
||||||
|
These are _functions_, not metadata, so they have to be used:
|
||||||
|
|
||||||
|
```
|
||||||
|
Like this: {{Str2html "Escape<my>text"}}
|
||||||
|
Or this: {{"Escape<my>text" | Str2html}}
|
||||||
|
Or this: {{AppUrl}}
|
||||||
|
But not like this: {{.AppUrl}}
|
||||||
|
```
|
|
@ -55,37 +55,44 @@ func TestAPITeam(t *testing.T) {
|
||||||
|
|
||||||
// Create team.
|
// Create team.
|
||||||
teamToCreate := &api.CreateTeamOption{
|
teamToCreate := &api.CreateTeamOption{
|
||||||
Name: "team1",
|
Name: "team1",
|
||||||
Description: "team one",
|
Description: "team one",
|
||||||
Permission: "write",
|
IncludesAllRepositories: true,
|
||||||
Units: []string{"repo.code", "repo.issues"},
|
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)
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate)
|
||||||
resp = session.MakeRequest(t, req, http.StatusCreated)
|
resp = session.MakeRequest(t, req, http.StatusCreated)
|
||||||
DecodeJSON(t, resp, &apiTeam)
|
DecodeJSON(t, resp, &apiTeam)
|
||||||
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.Permission, teamToCreate.Units)
|
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
|
||||||
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.Permission, teamToCreate.Units)
|
teamToCreate.Permission, teamToCreate.Units)
|
||||||
|
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
|
||||||
|
teamToCreate.Permission, teamToCreate.Units)
|
||||||
teamID := apiTeam.ID
|
teamID := apiTeam.ID
|
||||||
|
|
||||||
// Edit team.
|
// Edit team.
|
||||||
teamToEdit := &api.EditTeamOption{
|
teamToEdit := &api.EditTeamOption{
|
||||||
Name: "teamone",
|
Name: "teamone",
|
||||||
Description: "team 1",
|
Description: "team 1",
|
||||||
Permission: "admin",
|
IncludesAllRepositories: false,
|
||||||
Units: []string{"repo.code", "repo.pulls", "repo.releases"},
|
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)
|
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit)
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &apiTeam)
|
DecodeJSON(t, resp, &apiTeam)
|
||||||
checkTeamResponse(t, &apiTeam, teamToEdit.Name, teamToEdit.Description, teamToEdit.Permission, teamToEdit.Units)
|
checkTeamResponse(t, &apiTeam, teamToEdit.Name, teamToEdit.Description, teamToEdit.IncludesAllRepositories,
|
||||||
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, teamToEdit.Description, teamToEdit.Permission, teamToEdit.Units)
|
teamToEdit.Permission, teamToEdit.Units)
|
||||||
|
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, teamToEdit.Description, teamToEdit.IncludesAllRepositories,
|
||||||
|
teamToEdit.Permission, teamToEdit.Units)
|
||||||
|
|
||||||
// Read team.
|
// Read team.
|
||||||
teamRead := models.AssertExistsAndLoadBean(t, &models.Team{ID: teamID}).(*models.Team)
|
teamRead := models.AssertExistsAndLoadBean(t, &models.Team{ID: teamID}).(*models.Team)
|
||||||
req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID)
|
req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID)
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &apiTeam)
|
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.
|
// Delete team.
|
||||||
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d?token="+token, teamID)
|
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})
|
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, name, apiTeam.Name, "name")
|
||||||
assert.Equal(t, description, apiTeam.Description, "description")
|
assert.Equal(t, description, apiTeam.Description, "description")
|
||||||
|
assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories")
|
||||||
assert.Equal(t, permission, apiTeam.Permission, "permission")
|
assert.Equal(t, permission, apiTeam.Permission, "permission")
|
||||||
sort.StringSlice(units).Sort()
|
sort.StringSlice(units).Sort()
|
||||||
sort.StringSlice(apiTeam.Units).Sort()
|
sort.StringSlice(apiTeam.Units).Sort()
|
||||||
assert.EqualValues(t, units, apiTeam.Units, "units")
|
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)
|
team := models.AssertExistsAndLoadBean(t, &models.Team{ID: id}).(*models.Team)
|
||||||
assert.NoError(t, team.GetUnits(), "GetUnits")
|
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 {
|
type TeamSearchResults struct {
|
||||||
|
|
|
@ -535,6 +535,10 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = updateCommentInfos(e, opts, comment); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err = sendCreateCommentAction(e, opts, comment); err != nil {
|
if err = sendCreateCommentAction(e, opts, comment); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -546,6 +550,56 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
return comment, nil
|
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) {
|
func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) {
|
||||||
// Compose comment action, could be plain comment, close or reopen issue/pull request.
|
// 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.
|
// 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
|
fallthrough
|
||||||
case CommentTypeComment:
|
case CommentTypeComment:
|
||||||
act.OpType = ActionCommentIssue
|
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:
|
case CommentTypeReopen:
|
||||||
act.OpType = ActionReopenIssue
|
act.OpType = ActionReopenIssue
|
||||||
if opts.Issue.IsPull {
|
if opts.Issue.IsPull {
|
||||||
act.OpType = ActionReopenPullRequest
|
act.OpType = ActionReopenPullRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = opts.Issue.updateClosedNum(e); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case CommentTypeClose:
|
case CommentTypeClose:
|
||||||
act.OpType = ActionCloseIssue
|
act.OpType = ActionCloseIssue
|
||||||
if opts.Issue.IsPull {
|
if opts.Issue.IsPull {
|
||||||
act.OpType = ActionClosePullRequest
|
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.
|
// Notify watchers for whatever action comes in, ignore if no action type.
|
||||||
if act.OpType > 0 {
|
if act.OpType > 0 {
|
||||||
|
|
|
@ -264,6 +264,8 @@ var migrations = []Migration{
|
||||||
NewMigration("Add WhitelistDeployKeys to protected branch", addWhitelistDeployKeysToBranches),
|
NewMigration("Add WhitelistDeployKeys to protected branch", addWhitelistDeployKeysToBranches),
|
||||||
// v104 -> v105
|
// v104 -> v105
|
||||||
NewMigration("remove unnecessary columns from label", removeLabelUneededCols),
|
NewMigration("remove unnecessary columns from label", removeLabelUneededCols),
|
||||||
|
// v105 -> v106
|
||||||
|
NewMigration("add includes_all_repositories to teams", addTeamIncludesAllRepositories),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -48,6 +48,9 @@ func (org *User) GetOwnerTeam() (*Team, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (org *User) getTeams(e Engine) error {
|
func (org *User) getTeams(e Engine) error {
|
||||||
|
if org.Teams != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return e.
|
return e.
|
||||||
Where("org_id=?", org.ID).
|
Where("org_id=?", org.ID).
|
||||||
OrderBy("CASE WHEN name LIKE '" + ownerTeamName + "' THEN '' ELSE name END").
|
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.
|
// Create default owner team.
|
||||||
t := &Team{
|
t := &Team{
|
||||||
OrgID: org.ID,
|
OrgID: org.ID,
|
||||||
LowerName: strings.ToLower(ownerTeamName),
|
LowerName: strings.ToLower(ownerTeamName),
|
||||||
Name: ownerTeamName,
|
Name: ownerTeamName,
|
||||||
Authorize: AccessModeOwner,
|
Authorize: AccessModeOwner,
|
||||||
NumMembers: 1,
|
NumMembers: 1,
|
||||||
|
IncludesAllRepositories: true,
|
||||||
}
|
}
|
||||||
if _, err = sess.Insert(t); err != nil {
|
if _, err = sess.Insert(t); err != nil {
|
||||||
return fmt.Errorf("insert owner team: %v", err)
|
return fmt.Errorf("insert owner team: %v", err)
|
||||||
|
|
|
@ -22,17 +22,18 @@ const ownerTeamName = "Owners"
|
||||||
|
|
||||||
// Team represents a organization team.
|
// Team represents a organization team.
|
||||||
type Team struct {
|
type Team struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
OrgID int64 `xorm:"INDEX"`
|
OrgID int64 `xorm:"INDEX"`
|
||||||
LowerName string
|
LowerName string
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Authorize AccessMode
|
Authorize AccessMode
|
||||||
Repos []*Repository `xorm:"-"`
|
Repos []*Repository `xorm:"-"`
|
||||||
Members []*User `xorm:"-"`
|
Members []*User `xorm:"-"`
|
||||||
NumRepos int
|
NumRepos int
|
||||||
NumMembers int
|
NumMembers int
|
||||||
Units []*TeamUnit `xorm:"-"`
|
Units []*TeamUnit `xorm:"-"`
|
||||||
|
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchTeamOptions holds the search options
|
// SearchTeamOptions holds the search options
|
||||||
|
@ -149,6 +150,9 @@ func (t *Team) IsMember(userID int64) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Team) getRepositories(e Engine) error {
|
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").
|
return e.Join("INNER", "team_repo", "repository.id = team_repo.repo_id").
|
||||||
Where("team_repo.team_id=?", t.ID).
|
Where("team_repo.team_id=?", t.ID).
|
||||||
OrderBy("repository.name").
|
OrderBy("repository.name").
|
||||||
|
@ -220,6 +224,25 @@ func (t *Team) addRepository(e Engine, repo *Repository) (err error) {
|
||||||
return nil
|
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.
|
// AddRepository adds new repository to team of organization.
|
||||||
func (t *Team) AddRepository(repo *Repository) (err error) {
|
func (t *Team) AddRepository(repo *Repository) (err error) {
|
||||||
if repo.OwnerID != t.OrgID {
|
if repo.OwnerID != t.OrgID {
|
||||||
|
@ -241,6 +264,8 @@ func (t *Team) AddRepository(repo *Repository) (err error) {
|
||||||
return sess.Commit()
|
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) {
|
func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (err error) {
|
||||||
if err = removeTeamRepo(e, t.ID, repo.ID); err != nil {
|
if err = removeTeamRepo(e, t.ID, repo.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -284,11 +309,16 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveRepository removes repository from team of organization.
|
// 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 {
|
func (t *Team) RemoveRepository(repoID int64) error {
|
||||||
if !t.HasRepository(repoID) {
|
if !t.HasRepository(repoID) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.IncludesAllRepositories {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
repo, err := GetRepositoryByID(repoID)
|
repo, err := GetRepositoryByID(repoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// Update organization number of teams.
|
||||||
if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil {
|
if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil {
|
||||||
errRollback := sess.Rollback()
|
errRollback := sess.Rollback()
|
||||||
|
@ -446,7 +484,7 @@ func GetTeamByID(teamID int64) (*Team, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTeam updates information of team.
|
// 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 {
|
if len(t.Name) == 0 {
|
||||||
return errors.New("empty team name")
|
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()
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,12 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -206,7 +209,7 @@ func TestUpdateTeam(t *testing.T) {
|
||||||
team.Name = "newName"
|
team.Name = "newName"
|
||||||
team.Description = strings.Repeat("A long description!", 100)
|
team.Description = strings.Repeat("A long description!", 100)
|
||||||
team.Authorize = AccessModeAdmin
|
team.Authorize = AccessModeAdmin
|
||||||
assert.NoError(t, UpdateTeam(team, true))
|
assert.NoError(t, UpdateTeam(team, true, false))
|
||||||
|
|
||||||
team = AssertExistsAndLoadBean(t, &Team{Name: "newName"}).(*Team)
|
team = AssertExistsAndLoadBean(t, &Team{Name: "newName"}).(*Team)
|
||||||
assert.True(t, strings.HasPrefix(team.Description, "A long description!"))
|
assert.True(t, strings.HasPrefix(team.Description, "A long description!"))
|
||||||
|
@ -225,7 +228,7 @@ func TestUpdateTeam2(t *testing.T) {
|
||||||
team.LowerName = "owners"
|
team.LowerName = "owners"
|
||||||
team.Name = "Owners"
|
team.Name = "Owners"
|
||||||
team.Description = strings.Repeat("A long description!", 100)
|
team.Description = strings.Repeat("A long description!", 100)
|
||||||
err := UpdateTeam(team, true)
|
err := UpdateTeam(team, true, false)
|
||||||
assert.True(t, IsErrTeamAlreadyExist(err))
|
assert.True(t, IsErrTeamAlreadyExist(err))
|
||||||
|
|
||||||
CheckConsistencyFor(t, &Team{ID: team.ID})
|
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, 5}, 2) // userid 2,4
|
||||||
test([]int64{1, 2, 3, 4, 5}, []int64{2, 3, 5}, 3) // userid 2,4,5
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -1447,14 +1447,17 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err
|
||||||
}
|
}
|
||||||
u.NumRepos++
|
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() {
|
if u.IsOrganization() {
|
||||||
t, err := u.getOwnerTeam(e)
|
if err := u.GetTeams(); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("GetTeams: %v", err)
|
||||||
return fmt.Errorf("getOwnerTeam: %v", err)
|
|
||||||
}
|
}
|
||||||
if err = t.addRepository(e, repo); err != nil {
|
for _, t := range u.Teams {
|
||||||
return fmt.Errorf("addRepository: %v", err)
|
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 {
|
} else if err = repo.recalculateAccesses(e); err != nil {
|
||||||
// Organization automatically called this in addRepository method.
|
// Organization automatically called this in addRepository method.
|
||||||
|
@ -1641,11 +1644,15 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
|
||||||
}
|
}
|
||||||
|
|
||||||
if newOwner.IsOrganization() {
|
if newOwner.IsOrganization() {
|
||||||
t, err := newOwner.getOwnerTeam(sess)
|
if err := newOwner.GetTeams(); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("GetTeams: %v", err)
|
||||||
return fmt.Errorf("getOwnerTeam: %v", err)
|
}
|
||||||
} else if err = t.addRepository(sess, repo); err != nil {
|
for _, t := range newOwner.Teams {
|
||||||
return fmt.Errorf("add to owner team: %v", err)
|
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 {
|
} else if err = repo.recalculateAccesses(sess); err != nil {
|
||||||
// Organization called this in addRepository method.
|
// Organization called this in addRepository method.
|
||||||
|
|
|
@ -129,13 +129,17 @@ func (r *Review) publish(e *xorm.Engine) error {
|
||||||
go func(en *xorm.Engine, review *Review, comm *Comment) {
|
go func(en *xorm.Engine, review *Review, comm *Comment) {
|
||||||
sess := en.NewSession()
|
sess := en.NewSession()
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
if err := sendCreateCommentAction(sess, &CreateCommentOptions{
|
opts := &CreateCommentOptions{
|
||||||
Doer: comm.Poster,
|
Doer: comm.Poster,
|
||||||
Issue: review.Issue,
|
Issue: review.Issue,
|
||||||
Repo: review.Issue.Repo,
|
Repo: review.Issue.Repo,
|
||||||
Type: comm.Type,
|
Type: comm.Type,
|
||||||
Content: comm.Content,
|
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)
|
log.Warn("sendCreateCommentAction: %v", err)
|
||||||
}
|
}
|
||||||
}(e, r, comment)
|
}(e, r, comment)
|
||||||
|
|
|
@ -62,6 +62,7 @@ type CreateTeamForm struct {
|
||||||
Description string `binding:"MaxSize(255)"`
|
Description string `binding:"MaxSize(255)"`
|
||||||
Permission string
|
Permission string
|
||||||
Units []models.UnitType
|
Units []models.UnitType
|
||||||
|
RepoAccess string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// Validate validates the fields
|
||||||
|
|
|
@ -248,6 +248,16 @@ func CommitChanges(repoPath string, opts CommitChangesOptions) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllCommitsCount returns count of all commits in repository
|
||||||
|
func AllCommitsCount(repoPath string) (int64, error) {
|
||||||
|
stdout, err := NewCommand("rev-list", "--all", "--count").RunInDir(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
func commitsCount(repoPath, revision, relpath string) (int64, error) {
|
func commitsCount(repoPath, revision, relpath string) (int64, error) {
|
||||||
cmd := NewCommand("rev-list", "--count")
|
cmd := NewCommand("rev-list", "--count")
|
||||||
cmd.AddArguments(revision)
|
cmd.AddArguments(revision)
|
||||||
|
|
|
@ -46,6 +46,11 @@ type GPGSettings struct {
|
||||||
|
|
||||||
const prettyLogFormat = `--pretty=format:%H`
|
const prettyLogFormat = `--pretty=format:%H`
|
||||||
|
|
||||||
|
// GetAllCommitsCount returns count of all commits in repository
|
||||||
|
func (repo *Repository) GetAllCommitsCount() (int64, error) {
|
||||||
|
return AllCommitsCount(repo.Path)
|
||||||
|
}
|
||||||
|
|
||||||
func (repo *Repository) parsePrettyFormatLogToList(logs []byte) (*list.List, error) {
|
func (repo *Repository) parsePrettyFormatLogToList(logs []byte) (*list.List, error) {
|
||||||
l := list.New()
|
l := list.New()
|
||||||
if len(logs) == 0 {
|
if len(logs) == 0 {
|
||||||
|
|
|
@ -43,4 +43,6 @@ type Notifier interface {
|
||||||
NotifyDeleteRelease(doer *models.User, rel *models.Release)
|
NotifyDeleteRelease(doer *models.User, rel *models.Release)
|
||||||
|
|
||||||
NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *models.PushCommits)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,3 +114,11 @@ func (*NullNotifier) NotifyMigrateRepository(doer *models.User, u *models.User,
|
||||||
// NotifyPushCommits notifies commits pushed to notifiers
|
// NotifyPushCommits notifies commits pushed to notifiers
|
||||||
func (*NullNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *models.PushCommits) {
|
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) {
|
||||||
|
}
|
||||||
|
|
|
@ -199,3 +199,17 @@ func NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, ol
|
||||||
notifier.NotifyPushCommits(pusher, repo, refName, oldCommitID, newCommitID, commits)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,11 +6,11 @@ package webhook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/notification/base"
|
"code.gitea.io/gitea/modules/notification/base"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/webhook"
|
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -288,7 +288,7 @@ func (m *webhookNotifier) NotifyNewPullRequest(pull *models.PullRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mode, _ := models.AccessLevel(pull.Issue.Poster, pull.Issue.Repo)
|
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,
|
Action: api.HookIssueOpened,
|
||||||
Index: pull.Issue.Index,
|
Index: pull.Issue.Index,
|
||||||
PullRequest: pull.APIFormat(),
|
PullRequest: pull.APIFormat(),
|
||||||
|
@ -547,7 +547,7 @@ func (m *webhookNotifier) NotifyPullRequestReview(pr *models.PullRequest, review
|
||||||
log.Error("models.AccessLevel: %v", err)
|
log.Error("models.AccessLevel: %v", err)
|
||||||
return
|
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,
|
Action: api.HookIssueSynchronized,
|
||||||
Index: review.Issue.Index,
|
Index: review.Issue.Index,
|
||||||
PullRequest: pr.APIFormat(),
|
PullRequest: pr.APIFormat(),
|
||||||
|
@ -562,6 +562,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) {
|
func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) {
|
||||||
if err := pr.LoadIssue(); err != nil {
|
if err := pr.LoadIssue(); err != nil {
|
||||||
log.Error("pr.LoadIssue: %v", err)
|
log.Error("pr.LoadIssue: %v", err)
|
||||||
|
@ -572,7 +600,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *m
|
||||||
return
|
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,
|
Action: api.HookIssueSynchronized,
|
||||||
Index: pr.Issue.Index,
|
Index: pr.Issue.Index,
|
||||||
PullRequest: pr.Issue.PullRequest.APIFormat(),
|
PullRequest: pr.Issue.PullRequest.APIFormat(),
|
||||||
|
@ -582,3 +610,48 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *m
|
||||||
log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -14,8 +14,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/notification"
|
"code.gitea.io/gitea/modules/notification"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"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.
|
// CommitRepoActionOptions represent options of a new commit action.
|
||||||
|
@ -113,81 +111,23 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
|
||||||
return fmt.Errorf("NotifyWatchers: %v", err)
|
return fmt.Errorf("NotifyWatchers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiPusher := pusher.APIFormat()
|
var isHookEventPush = true
|
||||||
apiRepo := repo.APIFormat(models.AccessModeNone)
|
|
||||||
|
|
||||||
var shaSum string
|
|
||||||
var isHookEventPush = false
|
|
||||||
switch opType {
|
switch opType {
|
||||||
case models.ActionCommitRepo: // Push
|
case models.ActionCommitRepo: // Push
|
||||||
isHookEventPush = true
|
|
||||||
|
|
||||||
if isNewBranch {
|
if isNewBranch {
|
||||||
gitRepo, err := git.OpenRepository(repo.RepoPath())
|
notification.NotifyCreateRef(pusher, repo, "branch", opts.RefFullName)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case models.ActionDeleteBranch: // Delete Branch
|
case models.ActionDeleteBranch: // Delete Branch
|
||||||
isHookEventPush = true
|
notification.NotifyDeleteRef(pusher, repo, "branch", opts.RefFullName)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
case models.ActionPushTag: // Create
|
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
|
case models.ActionDeleteTag: // Delete Tag
|
||||||
isHookEventPush = true
|
notification.NotifyDeleteRef(pusher, repo, "tag", opts.RefFullName)
|
||||||
|
default:
|
||||||
if err = webhook.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{
|
isHookEventPush = false
|
||||||
Ref: refName,
|
|
||||||
RefType: "tag",
|
|
||||||
PusherType: api.PusherTypeUser,
|
|
||||||
Repo: apiRepo,
|
|
||||||
Sender: apiPusher,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isHookEventPush {
|
if isHookEventPush {
|
||||||
|
|
|
@ -7,10 +7,11 @@ package structs
|
||||||
|
|
||||||
// Team represents a team in an organization
|
// Team represents a team in an organization
|
||||||
type Team struct {
|
type Team struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Organization *Organization `json:"organization"`
|
Organization *Organization `json:"organization"`
|
||||||
|
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||||
// enum: none,read,write,admin,owner
|
// enum: none,read,write,admin,owner
|
||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"]
|
// 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
|
// CreateTeamOption options for creating a team
|
||||||
type CreateTeamOption struct {
|
type CreateTeamOption struct {
|
||||||
// required: true
|
// required: true
|
||||||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"`
|
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"`
|
||||||
Description string `json:"description" binding:"MaxSize(255)"`
|
Description string `json:"description" binding:"MaxSize(255)"`
|
||||||
|
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||||
// enum: read,write,admin
|
// enum: read,write,admin
|
||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"]
|
// 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
|
// EditTeamOption options for editing a team
|
||||||
type EditTeamOption struct {
|
type EditTeamOption struct {
|
||||||
// required: true
|
// required: true
|
||||||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"`
|
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"`
|
||||||
Description string `json:"description" binding:"MaxSize(255)"`
|
Description string `json:"description" binding:"MaxSize(255)"`
|
||||||
|
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||||
// enum: read,write,admin
|
// enum: read,write,admin
|
||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"]
|
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"]
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
texttmpl "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -20,7 +21,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
templates = template.New("")
|
subjectTemplates = texttmpl.New("")
|
||||||
|
bodyTemplates = template.New("")
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTMLRenderer implements the macaron handler for serving HTML templates.
|
// HTMLRenderer implements the macaron handler for serving HTML templates.
|
||||||
|
@ -59,9 +61,12 @@ func JSRenderer() macaron.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mailer provides the templates required for sending notification mails.
|
// Mailer provides the templates required for sending notification mails.
|
||||||
func Mailer() *template.Template {
|
func Mailer() (*texttmpl.Template, *template.Template) {
|
||||||
|
for _, funcs := range NewTextFuncMap() {
|
||||||
|
subjectTemplates.Funcs(funcs)
|
||||||
|
}
|
||||||
for _, funcs := range NewFuncMap() {
|
for _, funcs := range NewFuncMap() {
|
||||||
templates.Funcs(funcs)
|
bodyTemplates.Funcs(funcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
staticDir := path.Join(setting.StaticRootPath, "templates", "mail")
|
staticDir := path.Join(setting.StaticRootPath, "templates", "mail")
|
||||||
|
@ -84,15 +89,7 @@ func Mailer() *template.Template {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = templates.New(
|
buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content)
|
||||||
strings.TrimSuffix(
|
|
||||||
filePath,
|
|
||||||
".tmpl",
|
|
||||||
),
|
|
||||||
).Parse(string(content))
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to parse template %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,18 +114,10 @@ func Mailer() *template.Template {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = templates.New(
|
buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content)
|
||||||
strings.TrimSuffix(
|
|
||||||
filePath,
|
|
||||||
".tmpl",
|
|
||||||
),
|
|
||||||
).Parse(string(content))
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to parse template %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates
|
return subjectTemplates, bodyTemplates
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,10 @@ import (
|
||||||
"mime"
|
"mime"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
texttmpl "text/template"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
@ -34,6 +36,9 @@ import (
|
||||||
"github.com/editorconfig/editorconfig-core-go/v2"
|
"github.com/editorconfig/editorconfig-core-go/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Used from static.go && dynamic.go
|
||||||
|
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
|
||||||
|
|
||||||
// NewFuncMap returns functions for injecting to templates
|
// NewFuncMap returns functions for injecting to templates
|
||||||
func NewFuncMap() []template.FuncMap {
|
func NewFuncMap() []template.FuncMap {
|
||||||
return []template.FuncMap{map[string]interface{}{
|
return []template.FuncMap{map[string]interface{}{
|
||||||
|
@ -261,6 +266,112 @@ func NewFuncMap() []template.FuncMap {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTextFuncMap returns functions for injecting to text templates
|
||||||
|
// It's a subset of those used for HTML and other templates
|
||||||
|
func NewTextFuncMap() []texttmpl.FuncMap {
|
||||||
|
return []texttmpl.FuncMap{map[string]interface{}{
|
||||||
|
"GoVer": func() string {
|
||||||
|
return strings.Title(runtime.Version())
|
||||||
|
},
|
||||||
|
"AppName": func() string {
|
||||||
|
return setting.AppName
|
||||||
|
},
|
||||||
|
"AppSubUrl": func() string {
|
||||||
|
return setting.AppSubURL
|
||||||
|
},
|
||||||
|
"AppUrl": func() string {
|
||||||
|
return setting.AppURL
|
||||||
|
},
|
||||||
|
"AppVer": func() string {
|
||||||
|
return setting.AppVer
|
||||||
|
},
|
||||||
|
"AppBuiltWith": func() string {
|
||||||
|
return setting.AppBuiltWith
|
||||||
|
},
|
||||||
|
"AppDomain": func() string {
|
||||||
|
return setting.Domain
|
||||||
|
},
|
||||||
|
"TimeSince": timeutil.TimeSince,
|
||||||
|
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
||||||
|
"RawTimeSince": timeutil.RawTimeSince,
|
||||||
|
"DateFmtLong": func(t time.Time) string {
|
||||||
|
return t.Format(time.RFC1123Z)
|
||||||
|
},
|
||||||
|
"DateFmtShort": func(t time.Time) string {
|
||||||
|
return t.Format("Jan 02, 2006")
|
||||||
|
},
|
||||||
|
"List": List,
|
||||||
|
"SubStr": func(str string, start, length int) string {
|
||||||
|
if len(str) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
end := start + length
|
||||||
|
if length == -1 {
|
||||||
|
end = len(str)
|
||||||
|
}
|
||||||
|
if len(str) < end {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return str[start:end]
|
||||||
|
},
|
||||||
|
"EllipsisString": base.EllipsisString,
|
||||||
|
"URLJoin": util.URLJoin,
|
||||||
|
"Dict": func(values ...interface{}) (map[string]interface{}, error) {
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("invalid dict call")
|
||||||
|
}
|
||||||
|
dict := make(map[string]interface{}, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, ok := values[i].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("dict keys must be strings")
|
||||||
|
}
|
||||||
|
dict[key] = values[i+1]
|
||||||
|
}
|
||||||
|
return dict, nil
|
||||||
|
},
|
||||||
|
"Printf": fmt.Sprintf,
|
||||||
|
"Escape": Escape,
|
||||||
|
"Sec2Time": models.SecToTime,
|
||||||
|
"ParseDeadline": func(deadline string) []string {
|
||||||
|
return strings.Split(deadline, "|")
|
||||||
|
},
|
||||||
|
"dict": func(values ...interface{}) (map[string]interface{}, error) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil, errors.New("invalid dict call")
|
||||||
|
}
|
||||||
|
|
||||||
|
dict := make(map[string]interface{})
|
||||||
|
|
||||||
|
for i := 0; i < len(values); i++ {
|
||||||
|
switch key := values[i].(type) {
|
||||||
|
case string:
|
||||||
|
i++
|
||||||
|
if i == len(values) {
|
||||||
|
return nil, errors.New("specify the key for non array values")
|
||||||
|
}
|
||||||
|
dict[key] = values[i]
|
||||||
|
case map[string]interface{}:
|
||||||
|
m := values[i].(map[string]interface{})
|
||||||
|
for i, v := range m {
|
||||||
|
dict[i] = v
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("dict values must be maps")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dict, nil
|
||||||
|
},
|
||||||
|
"percentage": func(n int, values ...int) float32 {
|
||||||
|
var sum = 0
|
||||||
|
for i := 0; i < len(values); i++ {
|
||||||
|
sum += values[i]
|
||||||
|
}
|
||||||
|
return float32(n) * 100 / float32(sum)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
// Safe render raw as HTML
|
// Safe render raw as HTML
|
||||||
func Safe(raw string) template.HTML {
|
func Safe(raw string) template.HTML {
|
||||||
return template.HTML(raw)
|
return template.HTML(raw)
|
||||||
|
@ -551,3 +662,22 @@ func MigrationIcon(hostname string) string {
|
||||||
return "fa-git-alt"
|
return "fa-git-alt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
|
||||||
|
// Split template into subject and body
|
||||||
|
var subjectContent []byte
|
||||||
|
bodyContent := content
|
||||||
|
loc := mailSubjectSplit.FindIndex(content)
|
||||||
|
if loc != nil {
|
||||||
|
subjectContent = content[0:loc[0]]
|
||||||
|
bodyContent = content[loc[1]:]
|
||||||
|
}
|
||||||
|
if _, err := stpl.New(name).
|
||||||
|
Parse(string(subjectContent)); err != nil {
|
||||||
|
log.Warn("Failed to parse template [%s/subject]: %v", name, err)
|
||||||
|
}
|
||||||
|
if _, err := btpl.New(name).
|
||||||
|
Parse(string(bodyContent)); err != nil {
|
||||||
|
log.Warn("Failed to parse template [%s/body]: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
// 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 templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubjectBodySeparator(t *testing.T) {
|
||||||
|
test := func(input, subject, body string) {
|
||||||
|
loc := mailSubjectSplit.FindIndex([]byte(input))
|
||||||
|
if loc == nil {
|
||||||
|
assert.Empty(t, subject, "no subject found, but one expected")
|
||||||
|
assert.Equal(t, body, input)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, subject, string(input[0:loc[0]]))
|
||||||
|
assert.Equal(t, body, string(input[loc[1]:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Simple\n---------------\nCase",
|
||||||
|
"Simple\n",
|
||||||
|
"\nCase")
|
||||||
|
test("Only\nBody",
|
||||||
|
"",
|
||||||
|
"Only\nBody")
|
||||||
|
test("Minimal\n---\nseparator",
|
||||||
|
"Minimal\n",
|
||||||
|
"\nseparator")
|
||||||
|
test("False --- separator",
|
||||||
|
"",
|
||||||
|
"False --- separator")
|
||||||
|
test("False\n--- separator",
|
||||||
|
"",
|
||||||
|
"False\n--- separator")
|
||||||
|
test("False ---\nseparator",
|
||||||
|
"",
|
||||||
|
"False ---\nseparator")
|
||||||
|
test("With extra spaces\n----- \t \nBody",
|
||||||
|
"With extra spaces\n",
|
||||||
|
"\nBody")
|
||||||
|
test("With leading spaces\n -------\nOnly body",
|
||||||
|
"",
|
||||||
|
"With leading spaces\n -------\nOnly body")
|
||||||
|
test("Multiple\n---\n-------\n---\nSeparators",
|
||||||
|
"Multiple\n",
|
||||||
|
"\n-------\n---\nSeparators")
|
||||||
|
test("Insuficient\n--\nSeparators",
|
||||||
|
"",
|
||||||
|
"Insuficient\n--\nSeparators")
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
texttmpl "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -23,7 +24,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
templates = template.New("")
|
subjectTemplates = texttmpl.New("")
|
||||||
|
bodyTemplates = template.New("")
|
||||||
)
|
)
|
||||||
|
|
||||||
type templateFileSystem struct {
|
type templateFileSystem struct {
|
||||||
|
@ -140,9 +142,12 @@ func JSRenderer() macaron.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mailer provides the templates required for sending notification mails.
|
// Mailer provides the templates required for sending notification mails.
|
||||||
func Mailer() *template.Template {
|
func Mailer() (*texttmpl.Template, *template.Template) {
|
||||||
|
for _, funcs := range NewTextFuncMap() {
|
||||||
|
subjectTemplates.Funcs(funcs)
|
||||||
|
}
|
||||||
for _, funcs := range NewFuncMap() {
|
for _, funcs := range NewFuncMap() {
|
||||||
templates.Funcs(funcs)
|
bodyTemplates.Funcs(funcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, assetPath := range AssetNames() {
|
for _, assetPath := range AssetNames() {
|
||||||
|
@ -161,7 +166,8 @@ func Mailer() *template.Template {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
templates.New(
|
buildSubjectBodyTemplate(subjectTemplates,
|
||||||
|
bodyTemplates,
|
||||||
strings.TrimPrefix(
|
strings.TrimPrefix(
|
||||||
strings.TrimSuffix(
|
strings.TrimSuffix(
|
||||||
assetPath,
|
assetPath,
|
||||||
|
@ -169,7 +175,7 @@ func Mailer() *template.Template {
|
||||||
),
|
),
|
||||||
"mail/",
|
"mail/",
|
||||||
),
|
),
|
||||||
).Parse(string(content))
|
content)
|
||||||
}
|
}
|
||||||
|
|
||||||
customDir := path.Join(setting.CustomPath, "templates", "mail")
|
customDir := path.Join(setting.CustomPath, "templates", "mail")
|
||||||
|
@ -192,17 +198,18 @@ func Mailer() *template.Template {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
templates.New(
|
buildSubjectBodyTemplate(subjectTemplates,
|
||||||
|
bodyTemplates,
|
||||||
strings.TrimSuffix(
|
strings.TrimSuffix(
|
||||||
filePath,
|
filePath,
|
||||||
".tmpl",
|
".tmpl",
|
||||||
),
|
),
|
||||||
).Parse(string(content))
|
content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates
|
return subjectTemplates, bodyTemplates
|
||||||
}
|
}
|
||||||
|
|
||||||
func Asset(name string) ([]byte, error) {
|
func Asset(name string) ([]byte, error) {
|
||||||
|
|
|
@ -1089,6 +1089,9 @@ activity.period.daily=1 Tag
|
||||||
activity.period.halfweekly=3 Tage
|
activity.period.halfweekly=3 Tage
|
||||||
activity.period.weekly=1 Woche
|
activity.period.weekly=1 Woche
|
||||||
activity.period.monthly=1 Monat
|
activity.period.monthly=1 Monat
|
||||||
|
activity.period.quarterly=3 Monate
|
||||||
|
activity.period.semiyearly=6 Monate
|
||||||
|
activity.period.yearly=1 Jahr
|
||||||
activity.overview=Übersicht
|
activity.overview=Übersicht
|
||||||
activity.active_prs_count_1=<strong>%d</strong> aktiver Pull-Request
|
activity.active_prs_count_1=<strong>%d</strong> aktiver Pull-Request
|
||||||
activity.active_prs_count_n=<strong>%d</strong> aktive Pull-Requests
|
activity.active_prs_count_n=<strong>%d</strong> aktive Pull-Requests
|
||||||
|
@ -1511,6 +1514,7 @@ team_name=Teamname
|
||||||
team_desc=Beschreibung
|
team_desc=Beschreibung
|
||||||
team_name_helper=Teamnamen sollten kurz und einprägsam sein.
|
team_name_helper=Teamnamen sollten kurz und einprägsam sein.
|
||||||
team_desc_helper=Beschreibe den Zweck oder die Rolle des Teams.
|
team_desc_helper=Beschreibe den Zweck oder die Rolle des Teams.
|
||||||
|
team_access_desc=Zugriff auf das Repository
|
||||||
team_permission_desc=Berechtigungen
|
team_permission_desc=Berechtigungen
|
||||||
team_unit_desc=Zugriff auf Repositorybereiche erlauben
|
team_unit_desc=Zugriff auf Repositorybereiche erlauben
|
||||||
|
|
||||||
|
@ -1584,6 +1588,13 @@ teams.add_nonexistent_repo=Das Repository, das du hinzufügen möchten, existier
|
||||||
teams.add_duplicate_users=Dieser Benutzer ist bereits ein Teammitglied.
|
teams.add_duplicate_users=Dieser Benutzer ist bereits ein Teammitglied.
|
||||||
teams.repos.none=Dieses Team hat Zugang zu keinem Repository.
|
teams.repos.none=Dieses Team hat Zugang zu keinem Repository.
|
||||||
teams.members.none=Keine Mitglieder in diesem Team.
|
teams.members.none=Keine Mitglieder in diesem Team.
|
||||||
|
teams.specific_repositories=Bestimmte Repositories
|
||||||
|
teams.specific_repositories_helper=Mitglieder haben nur Zugriff auf Repositories, die explizit dem Team hinzugefügt wurden. Wenn Du diese Option wählst, werden Repositories, die bereits mit <i>Alle Repositories</i> hinzugefügt wurden, <strong>nicht</strong> automatisch entfernt.
|
||||||
|
teams.all_repositories=Alle Repositories
|
||||||
|
teams.all_repositories_helper=Team hat Zugriff auf alle Repositorys. Wenn dies ausgewählt wird, werden <strong>alle vorhandenen</strong> Repositories zum Team hinzugefügt.
|
||||||
|
teams.all_repositories_read_permission_desc=Dieses Team gewährt <strong>Lese</strong>-Zugriff auf <strong>Repositories</strong>: Mitglieder können Repositories ansehen und klonen.
|
||||||
|
teams.all_repositories_write_permission_desc=Dieses Team gewährt <strong>Schreib</strong>-Zugriff auf <strong>alle Repositories</strong>: Mitglieder können Repositories lesen und auf sie pushen.
|
||||||
|
teams.all_repositories_admin_permission_desc=Dieses Team gewährt <strong>Administrator</strong>-Zugriff auf <strong> alle Repositories </strong>: Mitglieder können Repositories lesen, auf sie pushen und Mitwirkende zu Repositories hinzufügen.
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=Dashboard
|
dashboard=Dashboard
|
||||||
|
|
|
@ -1515,6 +1515,7 @@ team_name = Team Name
|
||||||
team_desc = Description
|
team_desc = Description
|
||||||
team_name_helper = Team names should be short and memorable.
|
team_name_helper = Team names should be short and memorable.
|
||||||
team_desc_helper = Describe the purpose or role of the team.
|
team_desc_helper = Describe the purpose or role of the team.
|
||||||
|
team_access_desc = Repository access
|
||||||
team_permission_desc = Permission
|
team_permission_desc = Permission
|
||||||
team_unit_desc = Allow Access to Repository Sections
|
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.add_duplicate_users = User is already a team member.
|
||||||
teams.repos.none = No repositories could be accessed by this team.
|
teams.repos.none = No repositories could be accessed by this team.
|
||||||
teams.members.none = No members on 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 <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
|
||||||
|
teams.all_repositories = All repositories
|
||||||
|
teams.all_repositories_helper = Team has access to all repositories. Selecting this will <strong>add all existing</strong> repositories to the team.
|
||||||
|
teams.all_repositories_read_permission_desc = This team grants <strong>Read</strong> access to <strong>all repositories</strong>: members can view and clone repositories.
|
||||||
|
teams.all_repositories_write_permission_desc = This team grants <strong>Write</strong> access to <strong>all repositories</strong>: members can read from and push to repositories.
|
||||||
|
teams.all_repositories_admin_permission_desc = This team grants <strong>Admin</strong> access to <strong>all repositories</strong>: members can read from, push to and add collaborators to repositories.
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard = Dashboard
|
dashboard = Dashboard
|
||||||
|
|
|
@ -586,6 +586,8 @@ fork_visibility_helper=La visibilité d'un dépôt bifurqué ne peut pas être m
|
||||||
repo_desc=Description
|
repo_desc=Description
|
||||||
repo_lang=Langue
|
repo_lang=Langue
|
||||||
repo_gitignore_helper=Choisissez un modèle de fichier .gitignore.
|
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=Licence
|
||||||
license_helper=Sélectionner un fichier de licence.
|
license_helper=Sélectionner un fichier de licence.
|
||||||
readme=LISEZMOI
|
readme=LISEZMOI
|
||||||
|
@ -846,6 +848,8 @@ issues.create_comment=Créer un commentaire
|
||||||
issues.closed_at=`a fermé <a id="%[1]s"href="#%[1]s"> %[2]s</a>`
|
issues.closed_at=`a fermé <a id="%[1]s"href="#%[1]s"> %[2]s</a>`
|
||||||
issues.reopened_at=`réouvert à <a id="%[1]s" href="#%[1]s"> %[2]s</a>`
|
issues.reopened_at=`réouvert à <a id="%[1]s" href="#%[1]s"> %[2]s</a>`
|
||||||
issues.commit_ref_at=`a référencé ce ticket depuis une révision <a id="%[1]s" href="#%[1]s"> %[2]s</a>`
|
issues.commit_ref_at=`a référencé ce ticket depuis une révision <a id="%[1]s" href="#%[1]s"> %[2]s</a>`
|
||||||
|
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.poster=Publier
|
||||||
issues.collaborator=Collaborateur
|
issues.collaborator=Collaborateur
|
||||||
issues.owner=Propriétaire
|
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_users=Utilisateurs en liste blanche de fusion :
|
||||||
settings.protect_merge_whitelist_teams=Équipes 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=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=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_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 :
|
settings.protect_approvals_whitelist_users=Réviseurs sur liste blanche :
|
||||||
|
@ -1366,6 +1372,10 @@ diff.parent=Parent
|
||||||
diff.commit=révision
|
diff.commit=révision
|
||||||
diff.git-notes=Notes
|
diff.git-notes=Notes
|
||||||
diff.data_not_available=Contenu de la comparaison indisponible
|
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_split_view=Vue séparée
|
||||||
diff.show_unified_view=Vue unifiée
|
diff.show_unified_view=Vue unifiée
|
||||||
diff.whitespace_button=Espace
|
diff.whitespace_button=Espace
|
||||||
|
@ -1376,6 +1386,11 @@ diff.whitespace_ignore_at_eol=Ignorer les changements quand ce sont des espaces
|
||||||
diff.stats_desc=<strong> %d fichiers modifiés</strong> avec <strong>%d ajouts</strong> et <strong>%d suppressions</strong>
|
diff.stats_desc=<strong> %d fichiers modifiés</strong> avec <strong>%d ajouts</strong> et <strong>%d suppressions</strong>
|
||||||
diff.bin=BIN
|
diff.bin=BIN
|
||||||
diff.view_file=Voir le fichier
|
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.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.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
|
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.protected_deletion_failed=La branche '%s' est protégé. Il ne peut pas être supprimé.
|
||||||
branch.restore=Restaurer la branche '%s'
|
branch.restore=Restaurer la branche '%s'
|
||||||
branch.download=Télécharger 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.manage_topics=Gérer les sujets
|
||||||
topic.done=Terminé
|
topic.done=Terminé
|
||||||
|
@ -1478,6 +1495,8 @@ settings.options=Organisation
|
||||||
settings.full_name=Non Complet
|
settings.full_name=Non Complet
|
||||||
settings.website=Site Web
|
settings.website=Site Web
|
||||||
settings.location=Localisation
|
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=Visibilité
|
||||||
settings.visibility.public=Public
|
settings.visibility.public=Public
|
||||||
settings.visibility.limited=Limité (Visible uniquement aux utilisateurs connectés)
|
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 (<server>/.well-known/openid-configuration) pour spécifier les points d'accès
|
auths.tip.openid_connect=Utilisez l'URL de découvert OpenID (<server>/.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.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.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.edit=Mettre à jour la source d'authentification
|
||||||
auths.activated=Cette source d'authentification est activée
|
auths.activated=Cette source d'authentification est activée
|
||||||
auths.new_success=L'authentification "%s" a été ajouté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
|
mark_all_as_read=Tout marquer comme lu
|
||||||
|
|
||||||
[gpg]
|
[gpg]
|
||||||
|
default_key=Signé avec la clé par défaut
|
||||||
error.extract_sign=Impossible d'extraire la signature
|
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.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
|
error.no_committer_account=Aucun compte lié à l'adresse e-mail de l'auteur
|
||||||
|
|
|
@ -74,6 +74,7 @@ preview=Podgląd
|
||||||
loading=Ładowanie…
|
loading=Ładowanie…
|
||||||
|
|
||||||
[startpage]
|
[startpage]
|
||||||
|
app_desc=Bezbolesna usługa Git na własnym serwerze
|
||||||
|
|
||||||
[install]
|
[install]
|
||||||
install=Instalacja
|
install=Instalacja
|
||||||
|
@ -282,9 +283,9 @@ AuthName=Nazwa autoryzacji
|
||||||
AdminEmail=E-mail administratora
|
AdminEmail=E-mail administratora
|
||||||
|
|
||||||
NewBranchName=Nazwa nowej gałęzi
|
NewBranchName=Nazwa nowej gałęzi
|
||||||
CommitSummary=Podsumowanie commitu
|
CommitSummary=Podsumowanie commita
|
||||||
CommitMessage=Wiadomość commitu
|
CommitMessage=Wiadomość commita
|
||||||
CommitChoice=Wybór commitu
|
CommitChoice=Wybór commita
|
||||||
TreeName=Ścieżka pliku
|
TreeName=Ścieżka pliku
|
||||||
Content=Treść
|
Content=Treść
|
||||||
|
|
||||||
|
|
|
@ -1514,6 +1514,7 @@ team_name=Nome da equipe
|
||||||
team_desc=Descrição
|
team_desc=Descrição
|
||||||
team_name_helper=Nomes de equipe devem ser curtos e memoráveis.
|
team_name_helper=Nomes de equipe devem ser curtos e memoráveis.
|
||||||
team_desc_helper=Descreva a finalidade ou o papel da equipe.
|
team_desc_helper=Descreva a finalidade ou o papel da equipe.
|
||||||
|
team_access_desc=Acesso ao repositório
|
||||||
team_permission_desc=Permissão
|
team_permission_desc=Permissão
|
||||||
team_unit_desc=Permitir o acesso a seções de repositório
|
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.add_duplicate_users=Usuário já é um membro da equipe.
|
||||||
teams.repos.none=Nenhum repositório pode ser acessado por essa equipe.
|
teams.repos.none=Nenhum repositório pode ser acessado por essa equipe.
|
||||||
teams.members.none=Nenhum membro nesta 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 <strong>não</strong> removerá automaticamente os repositórios já adicionados com <i>Todos os repositórios</i>.
|
||||||
|
teams.all_repositories=Todos os repositórios
|
||||||
|
teams.all_repositories_helper=A equipe tem acesso a todos os repositórios. Selecionar isto irá <strong>adicionar todos os repositórios existentes</strong> à equipe.
|
||||||
|
teams.all_repositories_read_permission_desc=Esta equipe concede acesso <strong>Leitura</strong> a <strong>todos os repositórios</strong>: membros podem ver e clonar repositórios.
|
||||||
|
teams.all_repositories_write_permission_desc=Esta equipe concede acesso <strong>Escrita</strong> a <strong>todos os repositórios</strong>: os membros podem ler de e fazer push para os repositórios.
|
||||||
|
teams.all_repositories_admin_permission_desc=Esta equipe concede acesso <strong>Administrativo</strong> a <strong>todos os repositórios</strong>: os membros podem ler, fazer push e adicionar colaboradores aos repositórios.
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=Painel
|
dashboard=Painel
|
||||||
|
|
|
@ -818,6 +818,7 @@ i.icon.centerlock{top:1.5em}
|
||||||
.issue.list>.item .desc .checklist{padding-left:5px}
|
.issue.list>.item .desc .checklist{padding-left:5px}
|
||||||
.issue.list>.item .desc .checklist .progress-bar{margin-left:2px;width:80px;height:6px;display:inline-block;background-color:#eee;overflow:hidden;border-radius:3px;vertical-align:2px!important}
|
.issue.list>.item .desc .checklist .progress-bar{margin-left:2px;width:80px;height:6px;display:inline-block;background-color:#eee;overflow:hidden;border-radius:3px;vertical-align:2px!important}
|
||||||
.issue.list>.item .desc .checklist .progress-bar .progress{background-color:#ccc;display:block;height:100%}
|
.issue.list>.item .desc .checklist .progress-bar .progress{background-color:#ccc;display:block;height:100%}
|
||||||
|
.issue.list>.item .desc .due-date{padding-left:5px}
|
||||||
.issue.list>.item .desc a.milestone{margin-left:5px;color:#999!important}
|
.issue.list>.item .desc a.milestone{margin-left:5px;color:#999!important}
|
||||||
.issue.list>.item .desc a.milestone:hover{color:#000!important}
|
.issue.list>.item .desc a.milestone:hover{color:#000!important}
|
||||||
.issue.list>.item .desc a.ref{margin-left:8px;color:#999!important}
|
.issue.list>.item .desc a.ref{margin-left:8px;color:#999!important}
|
||||||
|
|
|
@ -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_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}
|
.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-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{color:#9daccc;background-color:#2b2b2b;border-top:0}
|
||||||
.CodeMirror div.CodeMirror-cursor{border-left:1px solid #9e9e9e}
|
.CodeMirror div.CodeMirror-cursor{border-left:1px solid #9e9e9e}
|
||||||
.CodeMirror .CodeMirror-gutters{background-color:#2b2b2b}
|
.CodeMirror .CodeMirror-gutters{background-color:#2b2b2b}
|
||||||
|
|
|
@ -2073,6 +2073,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.due-date {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
a.milestone {
|
a.milestone {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
color: #999999 !important;
|
color: #999999 !important;
|
||||||
|
|
|
@ -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 {
|
.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 */
|
/* code mirror dark theme */
|
||||||
|
|
|
@ -227,11 +227,12 @@ func ToOrganization(org *models.User) *api.Organization {
|
||||||
// ToTeam convert models.Team to api.Team
|
// ToTeam convert models.Team to api.Team
|
||||||
func ToTeam(team *models.Team) *api.Team {
|
func ToTeam(team *models.Team) *api.Team {
|
||||||
return &api.Team{
|
return &api.Team{
|
||||||
ID: team.ID,
|
ID: team.ID,
|
||||||
Name: team.Name,
|
Name: team.Name,
|
||||||
Description: team.Description,
|
Description: team.Description,
|
||||||
Permission: team.Authorize.String(),
|
IncludesAllRepositories: team.IncludesAllRepositories,
|
||||||
Units: team.GetUnitNames(),
|
Permission: team.Authorize.String(),
|
||||||
|
Units: team.GetUnitNames(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,10 +128,11 @@ func CreateTeam(ctx *context.APIContext, form api.CreateTeamOption) {
|
||||||
// "201":
|
// "201":
|
||||||
// "$ref": "#/responses/Team"
|
// "$ref": "#/responses/Team"
|
||||||
team := &models.Team{
|
team := &models.Team{
|
||||||
OrgID: ctx.Org.Organization.ID,
|
OrgID: ctx.Org.Organization.ID,
|
||||||
Name: form.Name,
|
Name: form.Name,
|
||||||
Description: form.Description,
|
Description: form.Description,
|
||||||
Authorize: models.ParseAccessMode(form.Permission),
|
IncludesAllRepositories: form.IncludesAllRepositories,
|
||||||
|
Authorize: models.ParseAccessMode(form.Permission),
|
||||||
}
|
}
|
||||||
|
|
||||||
unitTypes := models.FindUnitTypes(form.Units...)
|
unitTypes := models.FindUnitTypes(form.Units...)
|
||||||
|
@ -182,11 +183,27 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) {
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/Team"
|
// "$ref": "#/responses/Team"
|
||||||
team := ctx.Org.Team
|
team := ctx.Org.Team
|
||||||
team.Name = form.Name
|
|
||||||
team.Description = form.Description
|
team.Description = form.Description
|
||||||
team.Authorize = models.ParseAccessMode(form.Permission)
|
|
||||||
unitTypes := models.FindUnitTypes(form.Units...)
|
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 {
|
if team.Authorize < models.AccessModeOwner {
|
||||||
var units = make([]*models.TeamUnit, 0, len(form.Units))
|
var units = make([]*models.TeamUnit, 0, len(form.Units))
|
||||||
for _, tp := range unitTypes {
|
for _, tp := range unitTypes {
|
||||||
|
@ -198,7 +215,7 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) {
|
||||||
team.Units = units
|
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)
|
ctx.Error(500, "EditTeam", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// 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
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// 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["PageIsOrgTeams"] = true
|
||||||
ctx.Data["PageIsOrgTeamsNew"] = true
|
ctx.Data["PageIsOrgTeamsNew"] = true
|
||||||
ctx.Data["Units"] = models.Units
|
ctx.Data["Units"] = models.Units
|
||||||
|
var includesAllRepositories = (form.RepoAccess == "all")
|
||||||
|
|
||||||
t := &models.Team{
|
t := &models.Team{
|
||||||
OrgID: ctx.Org.Organization.ID,
|
OrgID: ctx.Org.Organization.ID,
|
||||||
Name: form.TeamName,
|
Name: form.TeamName,
|
||||||
Description: form.Description,
|
Description: form.Description,
|
||||||
Authorize: models.ParseAccessMode(form.Permission),
|
Authorize: models.ParseAccessMode(form.Permission),
|
||||||
|
IncludesAllRepositories: includesAllRepositories,
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Authorize < models.AccessModeOwner {
|
if t.Authorize < models.AccessModeOwner {
|
||||||
|
@ -268,6 +271,8 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) {
|
||||||
ctx.Data["Units"] = models.Units
|
ctx.Data["Units"] = models.Units
|
||||||
|
|
||||||
isAuthChanged := false
|
isAuthChanged := false
|
||||||
|
isIncludeAllChanged := false
|
||||||
|
var includesAllRepositories = (form.RepoAccess == "all")
|
||||||
if !t.IsOwnerTeam() {
|
if !t.IsOwnerTeam() {
|
||||||
// Validate permission level.
|
// Validate permission level.
|
||||||
auth := models.ParseAccessMode(form.Permission)
|
auth := models.ParseAccessMode(form.Permission)
|
||||||
|
@ -277,6 +282,11 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) {
|
||||||
isAuthChanged = true
|
isAuthChanged = true
|
||||||
t.Authorize = auth
|
t.Authorize = auth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.IncludesAllRepositories != includesAllRepositories {
|
||||||
|
isIncludeAllChanged = true
|
||||||
|
t.IncludesAllRepositories = includesAllRepositories
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.Description = form.Description
|
t.Description = form.Description
|
||||||
if t.Authorize < models.AccessModeOwner {
|
if t.Authorize < models.AccessModeOwner {
|
||||||
|
@ -305,7 +315,7 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := models.UpdateTeam(t, isAuthChanged); err != nil {
|
if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil {
|
||||||
ctx.Data["Err_TeamName"] = true
|
ctx.Data["Err_TeamName"] = true
|
||||||
switch {
|
switch {
|
||||||
case models.IsErrTeamAlreadyExist(err):
|
case models.IsErrTeamAlreadyExist(err):
|
||||||
|
|
|
@ -91,6 +91,12 @@ func Graph(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allCommitsCount, err := ctx.Repo.GitRepo.GetAllCommitsCount()
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetAllCommitsCount", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
page := ctx.QueryInt("page")
|
page := ctx.QueryInt("page")
|
||||||
|
|
||||||
graph, err := models.GetCommitGraph(ctx.Repo.GitRepo, page)
|
graph, err := models.GetCommitGraph(ctx.Repo.GitRepo, page)
|
||||||
|
@ -105,7 +111,7 @@ func Graph(ctx *context.Context) {
|
||||||
ctx.Data["CommitCount"] = commitsCount
|
ctx.Data["CommitCount"] = commitsCount
|
||||||
ctx.Data["Branch"] = ctx.Repo.BranchName
|
ctx.Data["Branch"] = ctx.Repo.BranchName
|
||||||
ctx.Data["RequireGitGraph"] = true
|
ctx.Data["RequireGitGraph"] = true
|
||||||
ctx.Data["Page"] = context.NewPagination(int(commitsCount), setting.UI.GraphMaxCommitNum, page, 5)
|
ctx.Data["Page"] = context.NewPagination(int(allCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
|
||||||
ctx.HTML(200, tplGraph)
|
ctx.HTML(200, tplGraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,11 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"mime"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
texttmpl "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
@ -28,18 +32,22 @@ const (
|
||||||
mailAuthResetPassword base.TplName = "auth/reset_passwd"
|
mailAuthResetPassword base.TplName = "auth/reset_passwd"
|
||||||
mailAuthRegisterNotify base.TplName = "auth/register_notify"
|
mailAuthRegisterNotify base.TplName = "auth/register_notify"
|
||||||
|
|
||||||
mailIssueComment base.TplName = "issue/comment"
|
|
||||||
mailIssueMention base.TplName = "issue/mention"
|
|
||||||
mailIssueAssigned base.TplName = "issue/assigned"
|
|
||||||
|
|
||||||
mailNotifyCollaborator base.TplName = "notify/collaborator"
|
mailNotifyCollaborator base.TplName = "notify/collaborator"
|
||||||
|
|
||||||
|
// There's no actual limit for subject in RFC 5322
|
||||||
|
mailMaxSubjectRunes = 256
|
||||||
)
|
)
|
||||||
|
|
||||||
var templates *template.Template
|
var (
|
||||||
|
bodyTemplates *template.Template
|
||||||
|
subjectTemplates *texttmpl.Template
|
||||||
|
subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
|
||||||
|
)
|
||||||
|
|
||||||
// InitMailRender initializes the mail renderer
|
// InitMailRender initializes the mail renderer
|
||||||
func InitMailRender(tmpls *template.Template) {
|
func InitMailRender(subjectTpl *texttmpl.Template, bodyTpl *template.Template) {
|
||||||
templates = tmpls
|
subjectTemplates = subjectTpl
|
||||||
|
bodyTemplates = bodyTpl
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTestMail sends a test mail
|
// SendTestMail sends a test mail
|
||||||
|
@ -58,7 +66,7 @@ func SendUserMail(language string, u *models.User, tpl base.TplName, code, subje
|
||||||
|
|
||||||
var content bytes.Buffer
|
var content bytes.Buffer
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||||
log.Error("Template: %v", err)
|
log.Error("Template: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -96,7 +104,7 @@ func SendActivateEmailMail(locale Locale, u *models.User, email *models.EmailAdd
|
||||||
|
|
||||||
var content bytes.Buffer
|
var content bytes.Buffer
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||||
log.Error("Template: %v", err)
|
log.Error("Template: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -121,7 +129,7 @@ func SendRegisterNotifyMail(locale Locale, u *models.User) {
|
||||||
|
|
||||||
var content bytes.Buffer
|
var content bytes.Buffer
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
||||||
log.Error("Template: %v", err)
|
log.Error("Template: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -145,7 +153,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
|
||||||
|
|
||||||
var content bytes.Buffer
|
var content bytes.Buffer
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
|
if err := bodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
|
||||||
log.Error("Template: %v", err)
|
log.Error("Template: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -156,40 +164,70 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
|
||||||
SendAsync(msg)
|
SendAsync(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeTplData(subject, body, link string) map[string]interface{} {
|
func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionType models.ActionType, fromMention bool,
|
||||||
data := make(map[string]interface{}, 10)
|
content string, comment *models.Comment, tos []string, info string) *Message {
|
||||||
data["Subject"] = subject
|
|
||||||
data["Body"] = body
|
|
||||||
data["Link"] = link
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
func composeIssueCommentMessage(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tplName base.TplName, tos []string, info string) *Message {
|
if err := issue.LoadPullRequest(); err != nil {
|
||||||
var subject string
|
log.Error("LoadPullRequest: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
subject string
|
||||||
|
link string
|
||||||
|
prefix string
|
||||||
|
// Fall back subject for bad templates, make sure subject is never empty
|
||||||
|
fallback string
|
||||||
|
)
|
||||||
|
|
||||||
|
commentType := models.CommentTypeComment
|
||||||
if comment != nil {
|
if comment != nil {
|
||||||
subject = "Re: " + mailSubject(issue)
|
prefix = "Re: "
|
||||||
|
commentType = comment.Type
|
||||||
|
link = issue.HTMLURL() + "#" + comment.HashTag()
|
||||||
} else {
|
} else {
|
||||||
subject = mailSubject(issue)
|
link = issue.HTMLURL()
|
||||||
}
|
|
||||||
err := issue.LoadRepo()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("LoadRepo: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fallback = prefix + fallbackMailSubject(issue)
|
||||||
|
|
||||||
|
// This is the body of the new issue or comment, not the mail body
|
||||||
body := string(markup.RenderByType(markdown.MarkupName, []byte(content), issue.Repo.HTMLURL(), issue.Repo.ComposeMetas()))
|
body := string(markup.RenderByType(markdown.MarkupName, []byte(content), issue.Repo.HTMLURL(), issue.Repo.ComposeMetas()))
|
||||||
|
|
||||||
var data = make(map[string]interface{}, 10)
|
actType, actName, tplName := actionToTemplate(issue, actionType, commentType)
|
||||||
if comment != nil {
|
|
||||||
data = composeTplData(subject, body, issue.HTMLURL()+"#"+comment.HashTag())
|
mailMeta := map[string]interface{}{
|
||||||
} else {
|
"FallbackSubject": fallback,
|
||||||
data = composeTplData(subject, body, issue.HTMLURL())
|
"Body": body,
|
||||||
|
"Link": link,
|
||||||
|
"Issue": issue,
|
||||||
|
"Comment": comment,
|
||||||
|
"IsPull": issue.IsPull,
|
||||||
|
"User": issue.Repo.MustOwner(),
|
||||||
|
"Repo": issue.Repo.FullName(),
|
||||||
|
"Doer": doer,
|
||||||
|
"IsMention": fromMention,
|
||||||
|
"SubjectPrefix": prefix,
|
||||||
|
"ActionType": actType,
|
||||||
|
"ActionName": actName,
|
||||||
}
|
}
|
||||||
data["Doer"] = doer
|
|
||||||
data["Issue"] = issue
|
var mailSubject bytes.Buffer
|
||||||
|
if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil {
|
||||||
|
subject = sanitizeSubject(mailSubject.String())
|
||||||
|
} else {
|
||||||
|
log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/subject", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if subject == "" {
|
||||||
|
subject = fallback
|
||||||
|
}
|
||||||
|
mailMeta["Subject"] = subject
|
||||||
|
|
||||||
var mailBody bytes.Buffer
|
var mailBody bytes.Buffer
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&mailBody, string(tplName), data); err != nil {
|
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplName), mailMeta); err != nil {
|
||||||
log.Error("Template: %v", err)
|
log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
|
msg := NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
|
||||||
|
@ -206,24 +244,81 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, content
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeSubject(subject string) string {
|
||||||
|
runes := []rune(strings.TrimSpace(subjectRemoveSpaces.ReplaceAllLiteralString(subject, " ")))
|
||||||
|
if len(runes) > mailMaxSubjectRunes {
|
||||||
|
runes = runes[:mailMaxSubjectRunes]
|
||||||
|
}
|
||||||
|
// Encode non-ASCII characters
|
||||||
|
return mime.QEncoding.Encode("utf-8", string(runes))
|
||||||
|
}
|
||||||
|
|
||||||
// SendIssueCommentMail composes and sends issue comment emails to target receivers.
|
// SendIssueCommentMail composes and sends issue comment emails to target receivers.
|
||||||
func SendIssueCommentMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
|
func SendIssueCommentMail(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
|
||||||
if len(tos) == 0 {
|
if len(tos) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SendAsync(composeIssueCommentMessage(issue, doer, content, comment, mailIssueComment, tos, "issue comment"))
|
SendAsync(composeIssueCommentMessage(issue, doer, actionType, false, content, comment, tos, "issue comment"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendIssueMentionMail composes and sends issue mention emails to target receivers.
|
// SendIssueMentionMail composes and sends issue mention emails to target receivers.
|
||||||
func SendIssueMentionMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
|
func SendIssueMentionMail(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
|
||||||
if len(tos) == 0 {
|
if len(tos) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SendAsync(composeIssueCommentMessage(issue, doer, content, comment, mailIssueMention, tos, "issue mention"))
|
SendAsync(composeIssueCommentMessage(issue, doer, actionType, true, content, comment, tos, "issue mention"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// actionToTemplate returns the type and name of the action facing the user
|
||||||
|
// (slightly different from models.ActionType) and the name of the template to use (based on availability)
|
||||||
|
func actionToTemplate(issue *models.Issue, actionType models.ActionType, commentType models.CommentType) (typeName, name, template string) {
|
||||||
|
if issue.IsPull {
|
||||||
|
typeName = "pull"
|
||||||
|
} else {
|
||||||
|
typeName = "issue"
|
||||||
|
}
|
||||||
|
switch actionType {
|
||||||
|
case models.ActionCreateIssue, models.ActionCreatePullRequest:
|
||||||
|
name = "new"
|
||||||
|
case models.ActionCommentIssue:
|
||||||
|
name = "comment"
|
||||||
|
case models.ActionCloseIssue, models.ActionClosePullRequest:
|
||||||
|
name = "close"
|
||||||
|
case models.ActionReopenIssue, models.ActionReopenPullRequest:
|
||||||
|
name = "reopen"
|
||||||
|
case models.ActionMergePullRequest:
|
||||||
|
name = "merge"
|
||||||
|
default:
|
||||||
|
switch commentType {
|
||||||
|
case models.CommentTypeReview:
|
||||||
|
name = "review"
|
||||||
|
case models.CommentTypeCode:
|
||||||
|
name = "code"
|
||||||
|
case models.CommentTypeAssignees:
|
||||||
|
name = "assigned"
|
||||||
|
default:
|
||||||
|
name = "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template = typeName + "/" + name
|
||||||
|
ok := bodyTemplates.Lookup(template) != nil
|
||||||
|
if !ok && typeName != "issue" {
|
||||||
|
template = "issue/" + name
|
||||||
|
ok = bodyTemplates.Lookup(template) != nil
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
template = typeName + "/default"
|
||||||
|
ok = bodyTemplates.Lookup(template) != nil
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
template = "issue/default"
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendIssueAssignedMail composes and sends issue assigned email
|
// SendIssueAssignedMail composes and sends issue assigned email
|
||||||
func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
|
func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
|
||||||
SendAsync(composeIssueCommentMessage(issue, doer, content, comment, mailIssueAssigned, tos, "issue assigned"))
|
SendAsync(composeIssueCommentMessage(issue, doer, models.ActionType(0), false, content, comment, tos, "issue assigned"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,24 +31,8 @@ func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType mod
|
||||||
for i, u := range userMentions {
|
for i, u := range userMentions {
|
||||||
mentions[i] = u.LowerName
|
mentions[i] = u.LowerName
|
||||||
}
|
}
|
||||||
if len(c.Content) > 0 {
|
if err = mailIssueCommentToParticipants(issue, c.Poster, opType, c.Content, c, mentions); err != nil {
|
||||||
if err = mailIssueCommentToParticipants(issue, c.Poster, c.Content, c, mentions); err != nil {
|
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch opType {
|
|
||||||
case models.ActionCloseIssue:
|
|
||||||
ct := fmt.Sprintf("Closed #%d.", issue.Index)
|
|
||||||
if err = mailIssueCommentToParticipants(issue, c.Poster, ct, c, mentions); err != nil {
|
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
|
||||||
}
|
|
||||||
case models.ActionReopenIssue:
|
|
||||||
ct := fmt.Sprintf("Reopened #%d.", issue.Index)
|
|
||||||
if err = mailIssueCommentToParticipants(issue, c.Poster, ct, c, mentions); err != nil {
|
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/unknwon/com"
|
"github.com/unknwon/com"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mailSubject(issue *models.Issue) string {
|
func fallbackMailSubject(issue *models.Issue) string {
|
||||||
return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
|
return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ func mailSubject(issue *models.Issue) string {
|
||||||
// This function sends two list of emails:
|
// This function sends two list of emails:
|
||||||
// 1. Repository watchers and users who are participated in comments.
|
// 1. Repository watchers and users who are participated in comments.
|
||||||
// 2. Users who are not in 1. but get mentioned in current issue/comment.
|
// 2. Users who are not in 1. but get mentioned in current issue/comment.
|
||||||
func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, content string, comment *models.Comment, mentions []string) error {
|
func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, mentions []string) error {
|
||||||
|
|
||||||
watchers, err := models.GetWatchers(issue.RepoID)
|
watchers, err := models.GetWatchers(issue.RepoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -89,7 +89,7 @@ func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, cont
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, to := range tos {
|
for _, to := range tos {
|
||||||
SendIssueCommentMail(issue, doer, content, comment, []string{to})
|
SendIssueCommentMail(issue, doer, actionType, content, comment, []string{to})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mail mentioned people and exclude watchers.
|
// Mail mentioned people and exclude watchers.
|
||||||
|
@ -106,7 +106,7 @@ func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, cont
|
||||||
emails := models.GetUserEmailsByNames(tos)
|
emails := models.GetUserEmailsByNames(tos)
|
||||||
|
|
||||||
for _, to := range emails {
|
for _, to := range emails {
|
||||||
SendIssueMentionMail(issue, doer, content, comment, []string{to})
|
SendIssueMentionMail(issue, doer, actionType, content, comment, []string{to})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -131,32 +131,8 @@ func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.Us
|
||||||
for i, u := range userMentions {
|
for i, u := range userMentions {
|
||||||
mentions[i] = u.LowerName
|
mentions[i] = u.LowerName
|
||||||
}
|
}
|
||||||
|
if err = mailIssueCommentToParticipants(issue, doer, opType, issue.Content, nil, mentions); err != nil {
|
||||||
if len(issue.Content) > 0 {
|
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||||
if err = mailIssueCommentToParticipants(issue, doer, issue.Content, nil, mentions); err != nil {
|
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch opType {
|
|
||||||
case models.ActionCreateIssue, models.ActionCreatePullRequest:
|
|
||||||
if len(issue.Content) == 0 {
|
|
||||||
ct := fmt.Sprintf("Created #%d.", issue.Index)
|
|
||||||
if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
|
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case models.ActionCloseIssue, models.ActionClosePullRequest:
|
|
||||||
ct := fmt.Sprintf("Closed #%d.", issue.Index)
|
|
||||||
if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
|
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
|
||||||
}
|
|
||||||
case models.ActionReopenIssue, models.ActionReopenPullRequest:
|
|
||||||
ct := fmt.Sprintf("Reopened #%d.", issue.Index)
|
|
||||||
if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
|
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
package mailer
|
package mailer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"html/template"
|
"html/template"
|
||||||
"testing"
|
"testing"
|
||||||
|
texttmpl "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -14,7 +16,11 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tmpl = `
|
const subjectTpl = `
|
||||||
|
{{.SubjectPrefix}}[{{.Repo}}] @{{.Doer.Name}} #{{.Issue.Index}} - {{.Issue.Title}}
|
||||||
|
`
|
||||||
|
|
||||||
|
const bodyTpl = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -47,17 +53,19 @@ func TestComposeIssueCommentMessage(t *testing.T) {
|
||||||
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
||||||
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
|
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
|
||||||
|
|
||||||
email := template.Must(template.New("issue/comment").Parse(tmpl))
|
stpl := texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
|
||||||
InitMailRender(email)
|
btpl := template.Must(template.New("issue/comment").Parse(bodyTpl))
|
||||||
|
InitMailRender(stpl, btpl)
|
||||||
|
|
||||||
tos := []string{"test@gitea.com", "test2@gitea.com"}
|
tos := []string{"test@gitea.com", "test2@gitea.com"}
|
||||||
msg := composeIssueCommentMessage(issue, doer, "test body", comment, mailIssueComment, tos, "issue comment")
|
msg := composeIssueCommentMessage(issue, doer, models.ActionCommentIssue, false, "test body", comment, tos, "issue comment")
|
||||||
|
|
||||||
subject := msg.GetHeader("Subject")
|
subject := msg.GetHeader("Subject")
|
||||||
inreplyTo := msg.GetHeader("In-Reply-To")
|
inreplyTo := msg.GetHeader("In-Reply-To")
|
||||||
references := msg.GetHeader("References")
|
references := msg.GetHeader("References")
|
||||||
|
|
||||||
assert.Equal(t, subject[0], "Re: "+mailSubject(issue), "Comment reply subject should contain Re:")
|
assert.Equal(t, "Re: ", subject[0][:4], "Comment reply subject should contain Re:")
|
||||||
|
assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject[0])
|
||||||
assert.Equal(t, inreplyTo[0], "<user2/repo1/issues/1@localhost>", "In-Reply-To header doesn't match")
|
assert.Equal(t, inreplyTo[0], "<user2/repo1/issues/1@localhost>", "In-Reply-To header doesn't match")
|
||||||
assert.Equal(t, references[0], "<user2/repo1/issues/1@localhost>", "References header doesn't match")
|
assert.Equal(t, references[0], "<user2/repo1/issues/1@localhost>", "References header doesn't match")
|
||||||
}
|
}
|
||||||
|
@ -75,17 +83,122 @@ func TestComposeIssueMessage(t *testing.T) {
|
||||||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
|
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
|
||||||
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
||||||
|
|
||||||
email := template.Must(template.New("issue/comment").Parse(tmpl))
|
stpl := texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
|
||||||
InitMailRender(email)
|
btpl := template.Must(template.New("issue/new").Parse(bodyTpl))
|
||||||
|
InitMailRender(stpl, btpl)
|
||||||
|
|
||||||
tos := []string{"test@gitea.com", "test2@gitea.com"}
|
tos := []string{"test@gitea.com", "test2@gitea.com"}
|
||||||
msg := composeIssueCommentMessage(issue, doer, "test body", nil, mailIssueComment, tos, "issue create")
|
msg := composeIssueCommentMessage(issue, doer, models.ActionCreateIssue, false, "test body", nil, tos, "issue create")
|
||||||
|
|
||||||
subject := msg.GetHeader("Subject")
|
subject := msg.GetHeader("Subject")
|
||||||
messageID := msg.GetHeader("Message-ID")
|
messageID := msg.GetHeader("Message-ID")
|
||||||
|
|
||||||
assert.Equal(t, subject[0], mailSubject(issue), "Subject not equal to issue.mailSubject()")
|
assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0])
|
||||||
assert.Nil(t, msg.GetHeader("In-Reply-To"))
|
assert.Nil(t, msg.GetHeader("In-Reply-To"))
|
||||||
assert.Nil(t, msg.GetHeader("References"))
|
assert.Nil(t, msg.GetHeader("References"))
|
||||||
assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match")
|
assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTemplateSelection(t *testing.T) {
|
||||||
|
assert.NoError(t, models.PrepareTestDatabase())
|
||||||
|
var mailService = setting.Mailer{
|
||||||
|
From: "test@gitea.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.MailService = &mailService
|
||||||
|
setting.Domain = "localhost"
|
||||||
|
|
||||||
|
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||||
|
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
|
||||||
|
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
||||||
|
tos := []string{"test@gitea.com"}
|
||||||
|
|
||||||
|
stpl := texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject"))
|
||||||
|
texttmpl.Must(stpl.New("issue/new").Parse("issue/new/subject"))
|
||||||
|
texttmpl.Must(stpl.New("pull/comment").Parse("pull/comment/subject"))
|
||||||
|
texttmpl.Must(stpl.New("issue/close").Parse("")) // Must default to fallback subject
|
||||||
|
|
||||||
|
btpl := template.Must(template.New("issue/default").Parse("issue/default/body"))
|
||||||
|
template.Must(btpl.New("issue/new").Parse("issue/new/body"))
|
||||||
|
template.Must(btpl.New("pull/comment").Parse("pull/comment/body"))
|
||||||
|
template.Must(btpl.New("issue/close").Parse("issue/close/body"))
|
||||||
|
|
||||||
|
InitMailRender(stpl, btpl)
|
||||||
|
|
||||||
|
expect := func(t *testing.T, msg *Message, expSubject, expBody string) {
|
||||||
|
subject := msg.GetHeader("Subject")
|
||||||
|
msgbuf := new(bytes.Buffer)
|
||||||
|
_, _ = msg.WriteTo(msgbuf)
|
||||||
|
wholemsg := msgbuf.String()
|
||||||
|
assert.Equal(t, []string{expSubject}, subject)
|
||||||
|
assert.Contains(t, wholemsg, expBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := composeIssueCommentMessage(issue, doer, models.ActionCreateIssue, false, "test body", nil, tos, "TestTemplateSelection")
|
||||||
|
expect(t, msg, "issue/new/subject", "issue/new/body")
|
||||||
|
|
||||||
|
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
|
||||||
|
msg = composeIssueCommentMessage(issue, doer, models.ActionCommentIssue, false, "test body", comment, tos, "TestTemplateSelection")
|
||||||
|
expect(t, msg, "issue/default/subject", "issue/default/body")
|
||||||
|
|
||||||
|
pull := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2, Repo: repo, Poster: doer}).(*models.Issue)
|
||||||
|
comment = models.AssertExistsAndLoadBean(t, &models.Comment{ID: 4, Issue: pull}).(*models.Comment)
|
||||||
|
msg = composeIssueCommentMessage(pull, doer, models.ActionCommentIssue, false, "test body", comment, tos, "TestTemplateSelection")
|
||||||
|
expect(t, msg, "pull/comment/subject", "pull/comment/body")
|
||||||
|
|
||||||
|
msg = composeIssueCommentMessage(issue, doer, models.ActionCloseIssue, false, "test body", nil, tos, "TestTemplateSelection")
|
||||||
|
expect(t, msg, "[user2/repo1] issue1 (#1)", "issue/close/body")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateServices(t *testing.T) {
|
||||||
|
assert.NoError(t, models.PrepareTestDatabase())
|
||||||
|
var mailService = setting.Mailer{
|
||||||
|
From: "test@gitea.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.MailService = &mailService
|
||||||
|
setting.Domain = "localhost"
|
||||||
|
|
||||||
|
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||||
|
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
|
||||||
|
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
|
||||||
|
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
|
||||||
|
assert.NoError(t, issue.LoadRepo())
|
||||||
|
|
||||||
|
expect := func(t *testing.T, issue *models.Issue, comment *models.Comment, doer *models.User,
|
||||||
|
actionType models.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string) {
|
||||||
|
|
||||||
|
stpl := texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject))
|
||||||
|
btpl := template.Must(template.New("issue/default").Parse(tplBody))
|
||||||
|
InitMailRender(stpl, btpl)
|
||||||
|
|
||||||
|
tos := []string{"test@gitea.com"}
|
||||||
|
msg := composeIssueCommentMessage(issue, doer, actionType, fromMention, "test body", comment, tos, "TestTemplateServices")
|
||||||
|
|
||||||
|
subject := msg.GetHeader("Subject")
|
||||||
|
msgbuf := new(bytes.Buffer)
|
||||||
|
_, _ = msg.WriteTo(msgbuf)
|
||||||
|
wholemsg := msgbuf.String()
|
||||||
|
|
||||||
|
assert.Equal(t, []string{expSubject}, subject)
|
||||||
|
assert.Contains(t, wholemsg, "\r\n"+expBody+"\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(t, issue, comment, doer, models.ActionCommentIssue, false,
|
||||||
|
"{{.SubjectPrefix}}[{{.Repo}}]: @{{.Doer.Name}} commented on #{{.Issue.Index}} - {{.Issue.Title}}",
|
||||||
|
"//{{.ActionType}},{{.ActionName}},{{if .IsMention}}norender{{end}}//",
|
||||||
|
"Re: [user2/repo1]: @user2 commented on #1 - issue1",
|
||||||
|
"//issue,comment,//")
|
||||||
|
|
||||||
|
expect(t, issue, comment, doer, models.ActionCommentIssue, true,
|
||||||
|
"{{if .IsMention}}must render{{end}}",
|
||||||
|
"//subject is: {{.Subject}}//",
|
||||||
|
"must render",
|
||||||
|
"//subject is: must render//")
|
||||||
|
|
||||||
|
expect(t, issue, comment, doer, models.ActionCommentIssue, true,
|
||||||
|
"{{.FallbackSubject}}",
|
||||||
|
"//{{.SubjectPrefix}}//",
|
||||||
|
"Re: [user2/repo1] issue1 (#1)",
|
||||||
|
"//Re: //")
|
||||||
|
}
|
||||||
|
|
|
@ -12,10 +12,9 @@ import (
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/notification"
|
||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/webhook"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func createTag(gitRepo *git.Repository, rel *models.Release) error {
|
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 !rel.IsDraft {
|
||||||
if err := rel.LoadAttributes(); err != nil {
|
notification.NotifyNewRelease(rel)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -114,20 +101,7 @@ func UpdateRelease(doer *models.User, gitRepo *git.Repository, rel *models.Relea
|
||||||
log.Error("AddReleaseAttachments: %v", err)
|
log.Error("AddReleaseAttachments: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rel.LoadAttributes(); err != nil {
|
notification.NotifyUpdateRelease(doer, rel)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -183,15 +157,7 @@ func DeleteReleaseByID(id int64, doer *models.User, delTag bool) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mode, _ := models.AccessLevel(doer, rel.Repo)
|
notification.NotifyDeleteRelease(doer, rel)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<p>@{{.Doer.Name}} assigned you to the {{if eq .Issue.IsPull true}}pull request{{else}}issue{{end}} <a href="{{.Link}}">#{{.Issue.Index}}</a> in repository {{.Issue.Repo.FullName}}.</p>
|
<p>@{{.Doer.Name}} assigned you to the {{if .IsPull}}pull request{{else}}issue{{end}} <a href="{{.Link}}">#{{.Issue.Index}}</a> in repository {{.Repo}}.</p>
|
||||||
<p>
|
<p>
|
||||||
---
|
---
|
||||||
<br>
|
<br>
|
||||||
<a href="{{.Link}}">View it on Gitea</a>.
|
<a href="{{.Link}}">View it on {{AppName}}</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<title>{{.Subject}}</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<p>{{.Body | Str2html}}</p>
|
|
||||||
<p>
|
|
||||||
---
|
|
||||||
<br>
|
|
||||||
<a href="{{.Link}}">View it on Gitea</a>.
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<title>{{.Subject}}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{if .IsMention}}<p>@{{.Doer.Name}} mentioned you:</p>{{end}}
|
||||||
|
<p>
|
||||||
|
{{- if eq .Body ""}}
|
||||||
|
{{if eq .ActionName "new"}}
|
||||||
|
Created #{{.Issue.Index}}.
|
||||||
|
{{else if eq .ActionName "close"}}
|
||||||
|
Closed #{{.Issue.Index}}.
|
||||||
|
{{else if eq .ActionName "reopen"}}
|
||||||
|
Reopened #{{.Issue.Index}}.
|
||||||
|
{{else}}
|
||||||
|
Empty comment on #{{.Issue.Index}}.
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
{{.Body | Str2html}}
|
||||||
|
{{end -}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
---
|
||||||
|
<br>
|
||||||
|
<a href="{{.Link}}">View it on {{AppName}}</a>.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,17 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<title>{{.Subject}}</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<p>@{{.Doer.Name}} mentioned you:</p>
|
|
||||||
<p>{{.Body | Str2html}}</p>
|
|
||||||
<p>
|
|
||||||
---
|
|
||||||
<br>
|
|
||||||
<a href="{{.Link}}">View it on Gitea</a>.
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -24,6 +24,24 @@
|
||||||
<span class="help">{{.i18n.Tr "org.team_desc_helper"}}</span>
|
<span class="help">{{.i18n.Tr "org.team_desc_helper"}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if not (eq .Team.LowerName "owners")}}
|
{{if not (eq .Team.LowerName "owners")}}
|
||||||
|
<div class="grouped field">
|
||||||
|
<label>{{.i18n.Tr "org.team_access_desc"}}</label>
|
||||||
|
<br>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio checkbox">
|
||||||
|
<input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "org.teams.specific_repositories"}}</label>
|
||||||
|
<span class="help">{{.i18n.Tr "org.teams.specific_repositories_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio checkbox">
|
||||||
|
<input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "org.teams.all_repositories"}}</label>
|
||||||
|
<span class="help">{{.i18n.Tr "org.teams.all_repositories_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="grouped field">
|
<div class="grouped field">
|
||||||
<label>{{.i18n.Tr "org.team_permission_desc"}}</label>
|
<label>{{.i18n.Tr "org.team_permission_desc"}}</label>
|
||||||
<br>
|
<br>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
{{template "org/team/sidebar" .}}
|
{{template "org/team/sidebar" .}}
|
||||||
<div class="ui ten wide column">
|
<div class="ui ten wide column">
|
||||||
{{template "org/team/navbar" .}}
|
{{template "org/team/navbar" .}}
|
||||||
{{$canAddRemove := and $.IsOrganizationOwner (not (eq $.Team.LowerName "owners"))}}
|
{{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}}
|
||||||
{{if $canAddRemove}}
|
{{if $canAddRemove}}
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<form class="ui form" id="add-repo-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/add" method="post">
|
<form class="ui form" id="add-repo-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/add" method="post">
|
||||||
|
|
|
@ -22,11 +22,23 @@
|
||||||
{{if eq .Team.LowerName "owners"}}
|
{{if eq .Team.LowerName "owners"}}
|
||||||
{{.i18n.Tr "org.teams.owners_permission_desc" | Str2html}}
|
{{.i18n.Tr "org.teams.owners_permission_desc" | Str2html}}
|
||||||
{{else if (eq .Team.Authorize 1)}}
|
{{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)}}
|
{{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)}}
|
{{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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -258,8 +258,9 @@
|
||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if ne .DeadlineUnix 0}}
|
{{if ne .DeadlineUnix 0}}
|
||||||
<span class="octicon octicon-calendar"></span>
|
<span class="due-date poping up" data-content="{{$.i18n.Tr "repo.issues.due_date"}}" data-variation="tiny inverted" data-position="right center">
|
||||||
<span{{if .IsOverdue}} class="overdue"{{end}}>{{.DeadlineUnix.FormatShort}}</span>
|
<span class="octicon octicon-calendar"></span><span{{if .IsOverdue}} class="overdue"{{end}}>{{.DeadlineUnix.FormatShort}}</span>
|
||||||
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{range .Assignees}}
|
{{range .Assignees}}
|
||||||
<a class="ui right assignee poping up" href="{{.HomeLink}}" data-content="{{.Name}}" data-variation="inverted" data-position="left center">
|
<a class="ui right assignee poping up" href="{{.HomeLink}}" data-content="{{.Name}}" data-variation="inverted" data-position="left center">
|
||||||
|
|
|
@ -8242,6 +8242,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Description"
|
"x-go-name": "Description"
|
||||||
},
|
},
|
||||||
|
"includes_all_repositories": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "IncludesAllRepositories"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Name"
|
"x-go-name": "Name"
|
||||||
|
@ -8801,6 +8805,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Description"
|
"x-go-name": "Description"
|
||||||
},
|
},
|
||||||
|
"includes_all_repositories": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "IncludesAllRepositories"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Name"
|
"x-go-name": "Name"
|
||||||
|
@ -10457,6 +10465,10 @@
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "ID"
|
"x-go-name": "ID"
|
||||||
},
|
},
|
||||||
|
"includes_all_repositories": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "IncludesAllRepositories"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Name"
|
"x-go-name": "Name"
|
||||||
|
|
|
@ -126,6 +126,11 @@
|
||||||
<span class="octicon octicon-checklist"></span> {{$tasksDone}} / {{$tasks}} <span class="progress-bar"><span class="progress" style="width:calc(100% * {{$tasksDone}} / {{$tasks}});"></span></span>
|
<span class="octicon octicon-checklist"></span> {{$tasksDone}} / {{$tasks}} <span class="progress-bar"><span class="progress" style="width:calc(100% * {{$tasksDone}} / {{$tasks}});"></span></span>
|
||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if ne .DeadlineUnix 0}}
|
||||||
|
<span class="due-date poping up" data-content="{{$.i18n.Tr "repo.issues.due_date"}}" data-variation="tiny inverted" data-position="right center">
|
||||||
|
<span class="octicon octicon-calendar"></span><span{{if .IsOverdue}} class="overdue"{{end}}>{{.DeadlineUnix.FormatShort}}</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in New Issue