mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-08 05:02:38 +09:00
Compare commits
18 Commits
v1.5.0-rc2
...
v1.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d8b8cf49 | ||
|
|
0358a40625 | ||
|
|
99ce0bfcd7 | ||
|
|
3fbcdd9e25 | ||
|
|
e9def84bf2 | ||
|
|
066515429f | ||
|
|
12c04a85f2 | ||
|
|
a345023d0a | ||
|
|
052aa54b2b | ||
|
|
cbe8a1f0e6 | ||
|
|
cfe6941905 | ||
|
|
eb8c611b1d | ||
|
|
b1eaeeb0cd | ||
|
|
15a403bf97 | ||
|
|
099028681e | ||
|
|
940e30bcd4 | ||
|
|
5a7830e0e8 | ||
|
|
dae065ea68 |
29
CHANGELOG.md
29
CHANGELOG.md
@@ -4,9 +4,25 @@ This changelog goes through all the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
|
||||
## [1.5.0-RC2](https://github.com/go-gitea/gitea/releases/tag/v1.5.0-rc2) - 2018-07-21
|
||||
## [1.5.1](https://github.com/go-gitea/gitea/releases/tag/v1.5.1) - 2018-09-03
|
||||
* SECURITY
|
||||
* Don't disclose emails of all users when sending out emails (#4784)
|
||||
* Improve URL validation for external wiki and external issues (#4710) (#4740)
|
||||
* Make cookies HttpOnly and obey COOKIE_SECURE flag (#4706) (#4707)
|
||||
* BUGFIXES
|
||||
* Fix missing release title in webhook (#4783) (#4800)
|
||||
* Make sure to reset commit count in the cache on mirror syncing (#4770)
|
||||
* Fixed bug where team with admin privelege type doesn't get any unit (#4759)
|
||||
* Fix failure on creating pull request with assignees (#4583) (#4727)
|
||||
* Hide org/create menu item in Dashboard if user has no rights (#4678) (#4686)
|
||||
* TRANSLATION
|
||||
* Fix incorrect caption of webhook setting (#4701) (#4718)
|
||||
|
||||
## [1.5.0](https://github.com/go-gitea/gitea/releases/tag/v1.5.0) - 2018-08-10
|
||||
* SECURITY
|
||||
* Check that repositories can only be migrated to own user or organizations (#4366) (#4370)
|
||||
* Limit uploaded avatar image-size to 4096px x 3072px by default (#4353)
|
||||
* Do not allow to reuse TOTP passcode (#3878)
|
||||
* BUGFIXES
|
||||
* Fix column droping for MSSQL that need new transaction for that (#4440) (#4484)
|
||||
* Redirect to correct page after using scratch token (#4458) (#4472)
|
||||
@@ -15,11 +31,12 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
* Add default merge options when adding new repository (#4369) (#4373)
|
||||
* Fix repository last updated time update when delete a user who watched the repo (#4363) (#4371)
|
||||
* Fix html entity escaping in branch deletion message (#4471) (#4485)
|
||||
|
||||
## [1.5.0-RC1](https://github.com/go-gitea/gitea/releases/tag/v1.5.0-rc1) - 2018-07-04
|
||||
* SECURITY
|
||||
* Limit uploaded avatar image-size to 4096px x 3072px by default (#4353)
|
||||
* Do not allow to reuse TOTP passcode (#3878)
|
||||
* Fix out-of-transaction query in removeOrgUser (#4521) (#4524)
|
||||
* Fix incorrect MergeWhitelistTeamIDs check in CanUserMerge function (#4519)
|
||||
* Fix panic issue on update avatar email (#4580) (#4590)
|
||||
* Fix bugs when too many IN variables (#4594) (#4597)
|
||||
* Push whitelist now doesn't apply to branch deletion (#4601) (#4640)
|
||||
* Site admin could create repos even MAX_CREATION_LIMIT=0 (#4645) (#4650)
|
||||
* FEATURE
|
||||
* Add cli commands to regen hooks & keys (#3979)
|
||||
* Add support for FIDO U2F (#3971)
|
||||
|
||||
@@ -74,7 +74,7 @@ func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(protectBranch.WhitelistTeamIDs) == 0 {
|
||||
if len(protectBranch.MergeWhitelistTeamIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -184,6 +184,24 @@ func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool,
|
||||
BranchName: branchName,
|
||||
}
|
||||
|
||||
has, err := x.Exist(protectedBranch)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return has, nil
|
||||
}
|
||||
|
||||
// IsProtectedBranchForPush checks if branch is protected for push
|
||||
func (repo *Repository) IsProtectedBranchForPush(branchName string, doer *User) (bool, error) {
|
||||
if doer == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
protectedBranch := &ProtectedBranch{
|
||||
RepoID: repo.ID,
|
||||
BranchName: branchName,
|
||||
}
|
||||
|
||||
has, err := x.Get(protectedBranch)
|
||||
if err != nil {
|
||||
return true, err
|
||||
|
||||
@@ -950,7 +950,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
||||
|
||||
// Insert the assignees
|
||||
for _, assigneeID := range opts.AssigneeIDs {
|
||||
err = opts.Issue.changeAssignee(e, doer, assigneeID)
|
||||
err = opts.Issue.changeAssignee(e, doer, assigneeID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -134,14 +134,14 @@ func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := issue.changeAssignee(sess, doer, assigneeID); err != nil {
|
||||
if err := issue.changeAssignee(sess, doer, assigneeID, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64) (err error) {
|
||||
func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (err error) {
|
||||
|
||||
// Update the assignee
|
||||
removed, err := updateIssueAssignee(sess, issue, assigneeID)
|
||||
@@ -161,6 +161,10 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in
|
||||
|
||||
mode, _ := accessLevel(sess, doer.ID, issue.Repo)
|
||||
if issue.IsPull {
|
||||
// if pull request is in the middle of creation - don't call webhook
|
||||
if isCreate {
|
||||
return nil
|
||||
}
|
||||
if err = issue.loadPullRequest(sess); err != nil {
|
||||
return fmt.Errorf("loadPullRequest: %v", err)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ import "fmt"
|
||||
// IssueList defines a list of issues
|
||||
type IssueList []*Issue
|
||||
|
||||
const (
|
||||
// default variables number on IN () in SQL
|
||||
defaultMaxInSize = 50
|
||||
)
|
||||
|
||||
func (issues IssueList) getRepoIDs() []int64 {
|
||||
repoIDs := make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
@@ -26,11 +31,20 @@ func (issues IssueList) loadRepositories(e Engine) ([]*Repository, error) {
|
||||
|
||||
repoIDs := issues.getRepoIDs()
|
||||
repoMaps := make(map[int64]*Repository, len(repoIDs))
|
||||
err := e.
|
||||
In("id", repoIDs).
|
||||
Find(&repoMaps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find repository: %v", err)
|
||||
var left = len(repoIDs)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := e.
|
||||
In("id", repoIDs[:limit]).
|
||||
Find(&repoMaps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find repository: %v", err)
|
||||
}
|
||||
left = left - limit
|
||||
repoIDs = repoIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
@@ -61,11 +75,20 @@ func (issues IssueList) loadPosters(e Engine) error {
|
||||
|
||||
posterIDs := issues.getPosterIDs()
|
||||
posterMaps := make(map[int64]*User, len(posterIDs))
|
||||
err := e.
|
||||
In("id", posterIDs).
|
||||
Find(&posterMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
var left = len(posterIDs)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := e.
|
||||
In("id", posterIDs[:limit]).
|
||||
Find(&posterMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left = left - limit
|
||||
posterIDs = posterIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
@@ -99,23 +122,34 @@ func (issues IssueList) loadLabels(e Engine) error {
|
||||
}
|
||||
|
||||
var issueLabels = make(map[int64][]*Label, len(issues)*3)
|
||||
rows, err := e.Table("label").
|
||||
Join("LEFT", "issue_label", "issue_label.label_id = label.id").
|
||||
In("issue_label.issue_id", issues.getIssueIDs()).
|
||||
Asc("label.name").
|
||||
Rows(new(LabelIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var labelIssue LabelIssue
|
||||
err = rows.Scan(&labelIssue)
|
||||
var issueIDs = issues.getIssueIDs()
|
||||
var left = len(issueIDs)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := e.Table("label").
|
||||
Join("LEFT", "issue_label", "issue_label.label_id = label.id").
|
||||
In("issue_label.issue_id", issueIDs[:limit]).
|
||||
Asc("label.name").
|
||||
Rows(new(LabelIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label)
|
||||
|
||||
for rows.Next() {
|
||||
var labelIssue LabelIssue
|
||||
err = rows.Scan(&labelIssue)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label)
|
||||
}
|
||||
rows.Close()
|
||||
left = left - limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
@@ -141,11 +175,20 @@ func (issues IssueList) loadMilestones(e Engine) error {
|
||||
}
|
||||
|
||||
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
err := e.
|
||||
In("id", milestoneIDs).
|
||||
Find(&milestoneMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
var left = len(milestoneIDs)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := e.
|
||||
In("id", milestoneIDs[:limit]).
|
||||
Find(&milestoneMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left = left - limit
|
||||
milestoneIDs = milestoneIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
@@ -165,23 +208,35 @@ func (issues IssueList) loadAssignees(e Engine) error {
|
||||
}
|
||||
|
||||
var assignees = make(map[int64][]*User, len(issues))
|
||||
rows, err := e.Table("issue_assignees").
|
||||
Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
|
||||
In("`issue_assignees`.issue_id", issues.getIssueIDs()).
|
||||
Rows(new(AssigneeIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var assigneeIssue AssigneeIssue
|
||||
err = rows.Scan(&assigneeIssue)
|
||||
var issueIDs = issues.getIssueIDs()
|
||||
var left = len(issueIDs)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := e.Table("issue_assignees").
|
||||
Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
|
||||
In("`issue_assignees`.issue_id", issueIDs[:limit]).
|
||||
Rows(new(AssigneeIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
|
||||
for rows.Next() {
|
||||
var assigneeIssue AssigneeIssue
|
||||
err = rows.Scan(&assigneeIssue)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
left = left - limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
@@ -207,21 +262,32 @@ func (issues IssueList) loadPullRequests(e Engine) error {
|
||||
}
|
||||
|
||||
pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
|
||||
rows, err := e.
|
||||
In("issue_id", issuesIDs).
|
||||
Rows(new(PullRequest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var pr PullRequest
|
||||
err = rows.Scan(&pr)
|
||||
var left = len(issuesIDs)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := e.
|
||||
In("issue_id", issuesIDs[:limit]).
|
||||
Rows(new(PullRequest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pullRequestMaps[pr.IssueID] = &pr
|
||||
|
||||
for rows.Next() {
|
||||
var pr PullRequest
|
||||
err = rows.Scan(&pr)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
pullRequestMaps[pr.IssueID] = &pr
|
||||
}
|
||||
|
||||
rows.Close()
|
||||
left = left - limit
|
||||
issuesIDs = issuesIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
@@ -236,22 +302,34 @@ func (issues IssueList) loadAttachments(e Engine) (err error) {
|
||||
}
|
||||
|
||||
var attachments = make(map[int64][]*Attachment, len(issues))
|
||||
rows, err := e.Table("attachment").
|
||||
Join("INNER", "issue", "issue.id = attachment.issue_id").
|
||||
In("issue.id", issues.getIssueIDs()).
|
||||
Rows(new(Attachment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var attachment Attachment
|
||||
err = rows.Scan(&attachment)
|
||||
var issuesIDs = issues.getIssueIDs()
|
||||
var left = len(issuesIDs)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := e.Table("attachment").
|
||||
Join("INNER", "issue", "issue.id = attachment.issue_id").
|
||||
In("issue.id", issuesIDs[:limit]).
|
||||
Rows(new(Attachment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
|
||||
|
||||
for rows.Next() {
|
||||
var attachment Attachment
|
||||
err = rows.Scan(&attachment)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
|
||||
}
|
||||
|
||||
rows.Close()
|
||||
left = left - limit
|
||||
issuesIDs = issuesIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
@@ -266,22 +344,33 @@ func (issues IssueList) loadComments(e Engine) (err error) {
|
||||
}
|
||||
|
||||
var comments = make(map[int64][]*Comment, len(issues))
|
||||
rows, err := e.Table("comment").
|
||||
Join("INNER", "issue", "issue.id = comment.issue_id").
|
||||
In("issue.id", issues.getIssueIDs()).
|
||||
Rows(new(Comment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var comment Comment
|
||||
err = rows.Scan(&comment)
|
||||
var issuesIDs = issues.getIssueIDs()
|
||||
var left = len(issuesIDs)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := e.Table("comment").
|
||||
Join("INNER", "issue", "issue.id = comment.issue_id").
|
||||
In("issue.id", issuesIDs[:limit]).
|
||||
Rows(new(Comment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
|
||||
|
||||
for rows.Next() {
|
||||
var comment Comment
|
||||
err = rows.Scan(&comment)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
|
||||
}
|
||||
rows.Close()
|
||||
left = left - limit
|
||||
issuesIDs = issuesIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
@@ -307,25 +396,35 @@ func (issues IssueList) loadTotalTrackedTimes(e Engine) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
|
||||
rows, err := e.Table("tracked_time").
|
||||
Select("issue_id, sum(time) as time").
|
||||
In("issue_id", ids).
|
||||
GroupBy("issue_id").
|
||||
Rows(new(totalTimesByIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var left = len(ids)
|
||||
for left > 0 {
|
||||
var limit = defaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var totalTime totalTimesByIssue
|
||||
err = rows.Scan(&totalTime)
|
||||
// select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
|
||||
rows, err := e.Table("tracked_time").
|
||||
Select("issue_id, sum(time) as time").
|
||||
In("issue_id", ids[:limit]).
|
||||
GroupBy("issue_id").
|
||||
Rows(new(totalTimesByIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trackedTimes[totalTime.IssueID] = totalTime.Time
|
||||
|
||||
for rows.Next() {
|
||||
var totalTime totalTimesByIssue
|
||||
err = rows.Scan(&totalTime)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
trackedTimes[totalTime.IssueID] = totalTime.Time
|
||||
}
|
||||
rows.Close()
|
||||
left = left - limit
|
||||
ids = ids[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 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.
|
||||
|
||||
@@ -87,7 +88,9 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, content
|
||||
names = append(names, participants[i].Name)
|
||||
}
|
||||
|
||||
SendIssueCommentMail(issue, doer, content, comment, tos)
|
||||
for _, to := range tos {
|
||||
SendIssueCommentMail(issue, doer, content, comment, []string{to})
|
||||
}
|
||||
|
||||
// Mail mentioned people and exclude watchers.
|
||||
names = append(names, doer.Name)
|
||||
@@ -99,7 +102,12 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, content
|
||||
|
||||
tos = append(tos, mentions[i])
|
||||
}
|
||||
SendIssueMentionMail(issue, doer, content, comment, getUserEmailsByNames(e, tos))
|
||||
|
||||
emails := getUserEmailsByNames(e, tos)
|
||||
|
||||
for _, to := range emails {
|
||||
SendIssueMentionMail(issue, doer, content, comment, []string{to})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ func AddOrgUser(orgID, uid int64) error {
|
||||
func removeOrgUser(sess *xorm.Session, orgID, userID int64) error {
|
||||
ou := new(OrgUser)
|
||||
|
||||
has, err := x.
|
||||
has, err := sess.
|
||||
Where("uid=?", userID).
|
||||
And("org_id=?", orgID).
|
||||
Get(ou)
|
||||
|
||||
@@ -88,6 +88,7 @@ func (r *Release) APIFormat() *api.Release {
|
||||
ID: r.ID,
|
||||
TagName: r.TagName,
|
||||
Target: r.Target,
|
||||
Title: r.Title,
|
||||
Note: r.Note,
|
||||
URL: r.APIURL(),
|
||||
TarURL: r.TarURL(),
|
||||
|
||||
@@ -1407,7 +1407,7 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err
|
||||
|
||||
// CreateRepository creates a repository for the user/organization u.
|
||||
func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err error) {
|
||||
if !u.CanCreateRepo() {
|
||||
if !doer.IsAdmin && !u.CanCreateRepo() {
|
||||
return nil, ErrReachLimitOfRepo{u.MaxRepoCreation}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/git"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -180,6 +181,16 @@ func (m *Mirror) runSync() bool {
|
||||
}
|
||||
}
|
||||
|
||||
branches, err := m.Repo.GetBranches()
|
||||
if err != nil {
|
||||
log.Error(4, "GetBranches: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range branches {
|
||||
cache.Remove(m.Repo.GetCommitsCountCacheKey(branches[i].Name, true))
|
||||
}
|
||||
|
||||
m.UpdatedUnix = util.TimeStampNow()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ func (r *Repository) CanCreateBranch() bool {
|
||||
}
|
||||
|
||||
// CanCommitToBranch returns true if repository is editable and user has proper access level
|
||||
// and branch is not protected
|
||||
// and branch is not protected for push
|
||||
func (r *Repository) CanCommitToBranch(doer *models.User) (bool, error) {
|
||||
protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName, doer)
|
||||
protectedBranch, err := r.Repository.IsProtectedBranchForPush(r.BranchName, doer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -70,13 +69,9 @@ func addValidURLBindingRule() {
|
||||
},
|
||||
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
|
||||
str := fmt.Sprintf("%v", val)
|
||||
if len(str) != 0 {
|
||||
if u, err := url.ParseRequestURI(str); err != nil ||
|
||||
(u.Scheme != "http" && u.Scheme != "https") ||
|
||||
!validPort(portOnly(u.Host)) {
|
||||
errs.Add([]string{name}, binding.ERR_URL, "Url")
|
||||
return false, errs
|
||||
}
|
||||
if len(str) != 0 && !IsValidURL(str) {
|
||||
errs.Add([]string{name}, binding.ERR_URL, "Url")
|
||||
return false, errs
|
||||
}
|
||||
|
||||
return true, errs
|
||||
|
||||
77
modules/validation/helpers.go
Normal file
77
modules/validation/helpers.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2018 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 validation
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
var loopbackIPBlocks []*net.IPNet
|
||||
|
||||
func init() {
|
||||
for _, cidr := range []string{
|
||||
"127.0.0.0/8", // IPv4 loopback
|
||||
"::1/128", // IPv6 loopback
|
||||
} {
|
||||
if _, block, err := net.ParseCIDR(cidr); err == nil {
|
||||
loopbackIPBlocks = append(loopbackIPBlocks, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isLoopbackIP(ip string) bool {
|
||||
pip := net.ParseIP(ip)
|
||||
if pip == nil {
|
||||
return false
|
||||
}
|
||||
for _, block := range loopbackIPBlocks {
|
||||
if block.Contains(pip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidURL checks if URL is valid
|
||||
func IsValidURL(uri string) bool {
|
||||
if u, err := url.ParseRequestURI(uri); err != nil ||
|
||||
(u.Scheme != "http" && u.Scheme != "https") ||
|
||||
!validPort(portOnly(u.Host)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsAPIURL checks if URL is current Gitea instance API URL
|
||||
func IsAPIURL(uri string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api"))
|
||||
}
|
||||
|
||||
// IsValidExternalURL checks if URL is valid external URL
|
||||
func IsValidExternalURL(uri string) bool {
|
||||
if !IsValidURL(uri) || IsAPIURL(uri) {
|
||||
return false
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(uri)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Currently check only if not loopback IP is provided to keep compatibility
|
||||
if isLoopbackIP(u.Hostname()) || strings.ToLower(u.Hostname()) == "localhost" {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: Later it should be added to allow local network IP addreses
|
||||
// only if allowed by special setting
|
||||
|
||||
return true
|
||||
}
|
||||
90
modules/validation/helpers_test.go
Normal file
90
modules/validation/helpers_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2018 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 validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func Test_IsValidURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
url string
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
description: "Empty URL",
|
||||
url: "",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
description: "Loobpack IPv4 URL",
|
||||
url: "http://127.0.1.1:5678/",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Loobpack IPv6 URL",
|
||||
url: "https://[::1]/",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Missing semicolon after schema",
|
||||
url: "http//meh/",
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.valid, IsValidURL(testCase.url))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IsValidExternalURL(t *testing.T) {
|
||||
setting.AppURL = "https://try.gitea.io/"
|
||||
|
||||
cases := []struct {
|
||||
description string
|
||||
url string
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
description: "Current instance URL",
|
||||
url: "https://try.gitea.io/test",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Loobpack IPv4 URL",
|
||||
url: "http://127.0.1.1:5678/",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
description: "Current instance API URL",
|
||||
url: "https://try.gitea.io/api/v1/user/follow",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
description: "Local network URL",
|
||||
url: "http://192.168.1.2/api/v1/user/follow",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Local URL",
|
||||
url: "http://LOCALHOST:1234/whatever",
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.valid, IsValidExternalURL(testCase.url))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -943,6 +943,7 @@ settings.external_tracker_url = External Issue Tracker URL
|
||||
settings.external_tracker_url_error = The external issue tracker URL is not a valid URL.
|
||||
settings.external_tracker_url_desc = Visitors are redirected to the external issue tracker URL when clicking on the issues tab.
|
||||
settings.tracker_url_format = External Issue Tracker URL Format
|
||||
settings.tracker_url_format_error = The external issue tracker URL format is not a valid URL.
|
||||
settings.tracker_issue_style = External Issue Tracker Number Format
|
||||
settings.tracker_issue_style.numeric = Numeric
|
||||
settings.tracker_issue_style.alphanumeric = Alphanumeric
|
||||
@@ -1042,8 +1043,8 @@ settings.event_push = Push
|
||||
settings.event_push_desc = Git push to a repository.
|
||||
settings.event_repository = Repository
|
||||
settings.event_repository_desc = Repository created or deleted.
|
||||
settings.active = Include Event Details
|
||||
settings.active_helper = Add information about the triggering event to requests.
|
||||
settings.active = Active
|
||||
settings.active_helper = Information about triggered events will be sent to this webhook URL.
|
||||
settings.add_hook_success = The webhook has been added.
|
||||
settings.update_webhook = Update Webhook
|
||||
settings.update_hook_success = The webhook has been updated.
|
||||
|
||||
@@ -181,7 +181,8 @@ func NewTeamPost(ctx *context.Context, form auth.CreateTeamForm) {
|
||||
Description: form.Description,
|
||||
Authorize: models.ParseAccessMode(form.Permission),
|
||||
}
|
||||
if t.Authorize < models.AccessModeAdmin {
|
||||
|
||||
if t.Authorize < models.AccessModeOwner {
|
||||
var units = make([]*models.TeamUnit, 0, len(form.Units))
|
||||
for _, tp := range form.Units {
|
||||
units = append(units, &models.TeamUnit{
|
||||
@@ -270,7 +271,7 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) {
|
||||
}
|
||||
}
|
||||
t.Description = form.Description
|
||||
if t.Authorize < models.AccessModeAdmin {
|
||||
if t.Authorize < models.AccessModeOwner {
|
||||
var units = make([]models.TeamUnit, 0, len(form.Units))
|
||||
for _, tp := range form.Units {
|
||||
units = append(units, models.TeamUnit{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 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.
|
||||
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
)
|
||||
|
||||
@@ -157,7 +159,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
|
||||
|
||||
if form.EnableWiki {
|
||||
if form.EnableExternalWiki {
|
||||
if !strings.HasPrefix(form.ExternalWikiURL, "http://") && !strings.HasPrefix(form.ExternalWikiURL, "https://") {
|
||||
if !validation.IsValidExternalURL(form.ExternalWikiURL) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
|
||||
ctx.Redirect(repo.Link() + "/settings")
|
||||
return
|
||||
@@ -181,11 +183,16 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
|
||||
|
||||
if form.EnableIssues {
|
||||
if form.EnableExternalTracker {
|
||||
if !strings.HasPrefix(form.ExternalTrackerURL, "http://") && !strings.HasPrefix(form.ExternalTrackerURL, "https://") {
|
||||
if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
|
||||
ctx.Redirect(repo.Link() + "/settings")
|
||||
return
|
||||
}
|
||||
if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalURL(form.TrackerURLFormat) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
|
||||
ctx.Redirect(repo.Link() + "/settings")
|
||||
return
|
||||
}
|
||||
units = append(units, models.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: models.UnitTypeExternalTracker,
|
||||
|
||||
@@ -116,12 +116,13 @@ func NewMacaron() *macaron.Macaron {
|
||||
}))
|
||||
m.Use(session.Sessioner(setting.SessionConfig))
|
||||
m.Use(csrf.Csrfer(csrf.Options{
|
||||
Secret: setting.SecretKey,
|
||||
Cookie: setting.CSRFCookieName,
|
||||
SetCookie: true,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
Header: "X-Csrf-Token",
|
||||
CookiePath: setting.AppSubURL,
|
||||
Secret: setting.SecretKey,
|
||||
Cookie: setting.CSRFCookieName,
|
||||
SetCookie: true,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
CookieHttpOnly: true,
|
||||
Header: "X-Csrf-Token",
|
||||
CookiePath: setting.AppSubURL,
|
||||
}))
|
||||
m.Use(toolbox.Toolboxer(m, toolbox.Options{
|
||||
HealthCheckFuncs: []*toolbox.HealthCheckFuncDesc{
|
||||
|
||||
@@ -56,8 +56,8 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
|
||||
defer func() {
|
||||
if !isSucceed {
|
||||
log.Trace("auto-login cookie cleared: %s", uname)
|
||||
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -77,7 +77,7 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
|
||||
isSucceed = true
|
||||
ctx.Session.Set("uid", u.ID)
|
||||
ctx.Session.Set("uname", u.Name)
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -91,13 +91,13 @@ func checkAutoLogin(ctx *context.Context) bool {
|
||||
|
||||
redirectTo := ctx.Query("redirect_to")
|
||||
if len(redirectTo) > 0 {
|
||||
ctx.SetCookie("redirect_to", redirectTo, 0, setting.AppSubURL)
|
||||
ctx.SetCookie("redirect_to", redirectTo, 0, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
} else {
|
||||
redirectTo, _ = url.QueryUnescape(ctx.GetCookie("redirect_to"))
|
||||
}
|
||||
|
||||
if isSucceed {
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
ctx.RedirectToFirst(redirectTo, setting.AppSubURL+string(setting.LandingPageURL))
|
||||
return true
|
||||
}
|
||||
@@ -438,9 +438,9 @@ func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
|
||||
func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) string {
|
||||
if remember {
|
||||
days := 86400 * setting.LogInRememberDays
|
||||
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL)
|
||||
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
|
||||
setting.CookieRememberName, u.Name, days, setting.AppSubURL)
|
||||
setting.CookieRememberName, u.Name, days, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
}
|
||||
|
||||
ctx.Session.Delete("openid_verified_uri")
|
||||
@@ -464,10 +464,10 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetCookie("lang", u.Language, nil, setting.AppSubURL)
|
||||
ctx.SetCookie("lang", u.Language, nil, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
|
||||
// Clear whatever CSRF has right now, force to generate a new one
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
|
||||
// Register last login
|
||||
u.SetLastLogin()
|
||||
@@ -477,7 +477,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
|
||||
}
|
||||
|
||||
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 && !util.IsExternalURL(redirectTo) {
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
if obeyRedirect {
|
||||
ctx.RedirectToFirst(redirectTo)
|
||||
}
|
||||
@@ -558,7 +558,7 @@ func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context
|
||||
ctx.Session.Set("uname", u.Name)
|
||||
|
||||
// Clear whatever CSRF has right now, force to generate a new one
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
|
||||
// Register last login
|
||||
u.SetLastLogin()
|
||||
@@ -568,7 +568,7 @@ func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context
|
||||
}
|
||||
|
||||
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
ctx.RedirectToFirst(redirectTo)
|
||||
return
|
||||
}
|
||||
@@ -844,10 +844,10 @@ func SignOut(ctx *context.Context) {
|
||||
ctx.Session.Delete("socialId")
|
||||
ctx.Session.Delete("socialName")
|
||||
ctx.Session.Delete("socialEmail")
|
||||
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie("lang", "", -1, setting.AppSubURL) // Setting the lang cookie will trigger the middleware to reset the language ot previous state.
|
||||
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
ctx.SetCookie("lang", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true) // Setting the lang cookie will trigger the middleware to reset the language ot previous state.
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
}
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ func SignInOpenID(ctx *context.Context) {
|
||||
|
||||
redirectTo := ctx.Query("redirect_to")
|
||||
if len(redirectTo) > 0 {
|
||||
ctx.SetCookie("redirect_to", redirectTo, 0, setting.AppSubURL)
|
||||
ctx.SetCookie("redirect_to", redirectTo, 0, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
} else {
|
||||
redirectTo, _ = url.QueryUnescape(ctx.GetCookie("redirect_to"))
|
||||
}
|
||||
|
||||
if isSucceed {
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
ctx.RedirectToFirst(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) {
|
||||
}
|
||||
|
||||
// Update the language to the one we just set
|
||||
ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL)
|
||||
ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
|
||||
|
||||
log.Trace("User settings updated: %s", ctx.User.Name)
|
||||
ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success"))
|
||||
@@ -119,7 +119,7 @@ func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *mo
|
||||
ctxUser.AvatarEmail = form.Gravatar
|
||||
}
|
||||
|
||||
if form.Avatar.Filename != "" {
|
||||
if form.Avatar != nil && form.Avatar.Filename != "" {
|
||||
fr, err := form.Avatar.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Avatar.Open: %v", err)
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
<div class="ui divider"></div>
|
||||
<br>
|
||||
{{if .ShowFooterVersion}}<p>Application Version: {{AppVer}}</p>{{end}}
|
||||
<p>If you think this is an error, please open an issue on <a href="https://github.com/go-gitea/gitea/issues/new">GitHub</a>.</p>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
|
||||
@@ -23,9 +23,11 @@
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .SignedUser.CanCreateOrganization}}
|
||||
<a class="item" href="{{AppSubUrl}}/org/create">
|
||||
<i class="octicon octicon-plus"></i> {{.i18n.Tr "new_org"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user