if err = issue.loadRepo(ctx.e); err != nil {
return
}
- resolved := make(map[string]bool, 20)
- names := make([]string, 0, 20)
+
+ resolved := make(map[string]bool, 10)
+ var mentionTeams []string
+
+ if err := issue.Repo.getOwner(ctx.e); err != nil {
+ return nil, err
+ }
+
+ repoOwnerIsOrg := issue.Repo.Owner.IsOrganization()
+ if repoOwnerIsOrg {
+ mentionTeams = make([]string, 0, 5)
+ }
+
resolved[doer.LowerName] = true
for _, name := range mentions {
name := strings.ToLower(name)
if _, ok := resolved[name]; ok {
continue
}
- resolved[name] = false
- names = append(names, name)
- }
-
- if err := issue.Repo.getOwner(ctx.e); err != nil {
- return nil, err
+ if repoOwnerIsOrg && strings.Contains(name, "/") {
+ names := strings.Split(name, "/")
+ if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName {
+ continue
+ }
+ mentionTeams = append(mentionTeams, names[1])
+ resolved[name] = true
+ } else {
+ resolved[name] = false
+ }
}
- if issue.Repo.Owner.IsOrganization() {
- // Since there can be users with names that match the name of a team,
- // if the team exists and can read the issue, the team takes precedence.
- teams := make([]*Team, 0, len(names))
+ if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
+ teams := make([]*Team, 0, len(mentionTeams))
if err := ctx.e.
Join("INNER", "team_repo", "team_repo.team_id = team.id").
Where("team_repo.repo_id=?", issue.Repo.ID).
- In("team.lower_name", names).
+ In("team.lower_name", mentionTeams).
Find(&teams); err != nil {
return nil, fmt.Errorf("find mentioned teams: %v", err)
}
for _, team := range teams {
if team.Authorize >= AccessModeOwner {
checked = append(checked, team.ID)
- resolved[team.LowerName] = true
+ resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
continue
}
has, err := ctx.e.Get(&TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
}
if has {
checked = append(checked, team.ID)
- resolved[team.LowerName] = true
+ resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
}
}
if len(checked) != 0 {
}
}
}
+ }
- // Remove names already in the list to avoid querying the database if pending names remain
- names = make([]string, 0, len(resolved))
- for name, already := range resolved {
- if !already {
- names = append(names, name)
- }
- }
- if len(names) == 0 {
- return
+ // Remove names already in the list to avoid querying the database if pending names remain
+ mentionUsers := make([]string, 0, len(resolved))
+ for name, already := range resolved {
+ if !already {
+ mentionUsers = append(mentionUsers, name)
}
}
+ if len(mentionUsers) == 0 {
+ return
+ }
+
+ if users == nil {
+ users = make([]*User, 0, len(mentionUsers))
+ }
- unchecked := make([]*User, 0, len(names))
+ unchecked := make([]*User, 0, len(mentionUsers))
if err := ctx.e.
Where("`user`.is_active = ?", true).
And("`user`.prohibit_login = ?", false).
- In("`user`.lower_name", names).
+ In("`user`.lower_name", mentionUsers).
Find(&unchecked); err != nil {
return nil, fmt.Errorf("find mentioned users: %v", err)
}
// Private repo, not a team member
testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{})
// Private repo, whole team
- testSuccess("user17", "big_test_private_4", "user15", []string{"owners"}, []int64{18})
+ testSuccess("user17", "big_test_private_4", "user15", []string{"user17/owners"}, []int64{18})
}
mention := node.Data[loc.Start:loc.End]
var teams string
teams, ok := ctx.metas["teams"]
- if ok && strings.Contains(teams, ","+strings.ToLower(mention[1:])+",") {
- replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.metas["org"], "teams", mention[1:]), mention, "mention"))
- } else {
- replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention"))
+ // team mention should follow @orgName/teamName style
+ if ok && strings.Contains(mention, "/") {
+ mentionOrgAndTeam := strings.Split(mention, "/")
+ if mentionOrgAndTeam[0][1:] == ctx.metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
+ replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
+ }
+ return
}
+ replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention"))
}
func shortLinkProcessor(ctx *postProcessCtx, node *html.Node) {
// While fast, this is also incorrect and lead to false positives.
// TODO: fix invalid linking issue
- // mentionPattern matches all mentions in the form of "@user"
- mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
+ // mentionPattern matches all mentions in the form of "@user" or "@org/team"
+ mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
{"@gitea.", "@gitea"},
{"@gitea,", "@gitea"},
{"@gitea;", "@gitea"},
+ {"@gitea/team1;", "@gitea/team1"},
}
falseTestCases := []string{
"@ 0",
"@gitea?this",
"@gitea,this",
"@gitea;this",
+ "@gitea/team1/more",
}
for _, testCase := range trueTestCases {
return
}
+ handleTeamMentions(ctx)
+ if ctx.Written() {
+ return
+ }
+
labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
ctx.ServerError("GetAssignees", err)
return
}
+
+ handleTeamMentions(ctx)
+ if ctx.Written() {
+ return
+ }
}
func retrieveProjects(ctx *context.Context, repo *models.Repository) {
i--
}
}
+
+// get all teams that current user can mention
+func handleTeamMentions(ctx *context.Context) {
+ if ctx.User == nil || !ctx.Repo.Owner.IsOrganization() {
+ return
+ }
+
+ isAdmin := false
+ var err error
+ // Admin has super access.
+ if ctx.User.IsAdmin {
+ isAdmin = true
+ } else {
+ isAdmin, err = ctx.Repo.Owner.IsOwnedBy(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("IsOwnedBy", err)
+ return
+ }
+ }
+
+ if isAdmin {
+ if err := ctx.Repo.Owner.GetTeams(&models.SearchTeamOptions{}); err != nil {
+ ctx.ServerError("GetTeams", err)
+ return
+ }
+ } else {
+ ctx.Repo.Owner.Teams, err = ctx.Repo.Owner.GetUserTeams(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("GetUserTeams", err)
+ return
+ }
+ }
+
+ ctx.Data["MentionableTeams"] = ctx.Repo.Owner.Teams
+ ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
+ ctx.Data["MentionableTeamsOrgAvator"] = ctx.Repo.Owner.RelAvatarLink()
+}
ctx.ServerError("GetAssignees", err)
return
}
+ handleTeamMentions(ctx)
+ if ctx.Written() {
+ return
+ }
ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.User, issue)
if err != nil && !models.IsErrReviewNotExist(err) {
ctx.ServerError("GetCurrentReview", err)
EventSourceUpdateTime: {{NotificationSettings.EventSourceUpdateTime}},
},
PageIsProjects: {{if .PageIsProjects }}true{{else}}false{{end}},
- {{if .RequireTribute}}
+ {{if .RequireTribute}}
tributeValues: Array.from(new Map([
{{ range .Participants }}
['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.RelAvatarLink}}'}],
{{ end }}
+ {{ range .MentionableTeams }}
+ ['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}',
+ name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvator}}'}],
+ {{ end }}
]).values()),
{{end}}
};