1
0
mirror of https://github.com/go-gitea/gitea.git synced 2024-11-04 08:17:24 -05:00

merge upstream, resolve migration conflicts

This commit is contained in:
Manush Dodunekov 2020-01-09 07:49:34 +01:00
commit 3459547fca
38 changed files with 626 additions and 141 deletions

View File

@ -13,16 +13,13 @@ menu:
identifier: "linux-service" identifier: "linux-service"
--- ---
### Run as service in Ubuntu 16.04 LTS ### Run Gitea as Linux service
You can run Gitea as service, using either systemd or supervisor. The steps below tested on Ubuntu 16.04, but those should work on any Linux distributions (with little modification).
#### Using systemd #### Using systemd
Run the below command in a terminal: Copy the sample [gitea.service](https://github.com/go-gitea/gitea/blob/master/contrib/systemd/gitea.service) to `/etc/systemd/system/gitea.service`, then edit the file with your favorite editor.
```
sudo vim /etc/systemd/system/gitea.service
```
Copy the sample [gitea.service](https://github.com/go-gitea/gitea/blob/master/contrib/systemd/gitea.service).
Uncomment any service that needs to be enabled on this host, such as MySQL. Uncomment any service that needs to be enabled on this host, such as MySQL.
@ -35,6 +32,10 @@ sudo systemctl enable gitea
sudo systemctl start gitea sudo systemctl start gitea
``` ```
If you have systemd version 220 or later, you can enable and immediately start Gitea at once by:
```
sudo systemctl enable gitea --now
```
#### Using supervisor #### Using supervisor
@ -49,19 +50,20 @@ Create a log dir for the supervisor logs:
mkdir /home/git/gitea/log/supervisor mkdir /home/git/gitea/log/supervisor
``` ```
Open supervisor config file in a file editor:
```
sudo vim /etc/supervisor/supervisord.conf
```
Append the configuration from the sample Append the configuration from the sample
[supervisord config](https://github.com/go-gitea/gitea/blob/master/contrib/supervisor/gitea). [supervisord config](https://github.com/go-gitea/gitea/blob/master/contrib/supervisor/gitea) to `/etc/supervisor/supervisord.conf`.
Change the user (git) and home (/home/git) settings to match the deployment Using your favorite editor, change the user (git) and home
environment. Change the PORT or remove the -p flag if default port is used. (/home/git) settings to match the deployment environment. Change the PORT
or remove the -p flag if default port is used.
Lastly enable and start supervisor at boot: Lastly enable and start supervisor at boot:
``` ```
sudo systemctl enable supervisor sudo systemctl enable supervisor
sudo systemctl start supervisor sudo systemctl start supervisor
``` ```
If you have systemd version 220 or later, you can enable and immediately start supervisor by:
```
sudo systemctl enable supervisor --now
```

View File

@ -44,6 +44,18 @@ func TestAPIGetTrackedTimes(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, user.Name, apiTimes[i].UserName) assert.Equal(t, user.Name, apiTimes[i].UserName)
} }
// test filter
since := "2000-01-01T00%3A00%3A02%2B00%3A00" //946684802
before := "2000-01-01T00%3A00%3A12%2B00%3A00" //946684812
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/times?since=%s&before=%s&token=%s", user2.Name, issue2.Repo.Name, issue2.Index, since, before, token)
resp = session.MakeRequest(t, req, http.StatusOK)
var filterAPITimes api.TrackedTimeList
DecodeJSON(t, resp, &filterAPITimes)
assert.Len(t, filterAPITimes, 2)
assert.Equal(t, int64(3), filterAPITimes[0].ID)
assert.Equal(t, int64(6), filterAPITimes[1].ID)
} }
func TestAPIDeleteTrackedTime(t *testing.T) { func TestAPIDeleteTrackedTime(t *testing.T) {

View File

@ -196,7 +196,7 @@ func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]*userAcces
if ua.Mode < minMode && !ua.User.IsRestricted { if ua.Mode < minMode && !ua.User.IsRestricted {
continue continue
} }
newAccesses = append(newAccesses, Access{ newAccesses = append(newAccesses, Access{
UserID: userID, UserID: userID,
RepoID: repo.ID, RepoID: repo.ID,

View File

@ -32,21 +32,23 @@ type ProtectedBranch struct {
BranchName string `xorm:"UNIQUE(s)"` BranchName string `xorm:"UNIQUE(s)"`
CanPush bool `xorm:"NOT NULL DEFAULT false"` CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableWhitelist bool EnableWhitelist bool
WhitelistUserIDs []int64 `xorm:"JSON TEXT"` WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"` EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"` MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
StatusCheckContexts []string `xorm:"JSON TEXT"` StatusCheckContexts []string `xorm:"JSON TEXT"`
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"` EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"` ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"` BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"` DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
} }
// IsProtected returns if the branch is protected // IsProtected returns if the branch is protected
@ -155,10 +157,13 @@ func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist. // GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 { func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
approvals, err := x.Where("issue_id = ?", pr.IssueID). sess := x.Where("issue_id = ?", pr.IssueID).
And("type = ?", ReviewTypeApprove). And("type = ?", ReviewTypeApprove).
And("official = ?", true). And("official = ?", true)
Count(new(Review)) if protectBranch.DismissStaleApprovals {
sess = sess.And("stale = ?", false)
}
approvals, err := sess.Count(new(Review))
if err != nil { if err != nil {
log.Error("GetGrantedApprovalsCount: %v", err) log.Error("GetGrantedApprovalsCount: %v", err)
return 0 return 0

View File

@ -381,6 +381,7 @@ func (issue *Issue) apiFormat(e Engine) *api.Issue {
apiIssue := &api.Issue{ apiIssue := &api.Issue{
ID: issue.ID, ID: issue.ID,
URL: issue.APIURL(), URL: issue.APIURL(),
HTMLURL: issue.HTMLURL(),
Index: issue.Index, Index: issue.Index,
Poster: issue.Poster.APIFormat(), Poster: issue.Poster.APIFormat(),
Title: issue.Title, Title: issue.Title,

View File

@ -100,10 +100,12 @@ func (tl TrackedTimeList) APIFormat() api.TrackedTimeList {
// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored. // FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
type FindTrackedTimesOptions struct { type FindTrackedTimesOptions struct {
IssueID int64 IssueID int64
UserID int64 UserID int64
RepositoryID int64 RepositoryID int64
MilestoneID int64 MilestoneID int64
CreatedAfterUnix int64
CreatedBeforeUnix int64
} }
// ToCond will convert each condition into a xorm-Cond // ToCond will convert each condition into a xorm-Cond
@ -121,6 +123,12 @@ func (opts *FindTrackedTimesOptions) ToCond() builder.Cond {
if opts.MilestoneID != 0 { if opts.MilestoneID != 0 {
cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID}) cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID})
} }
if opts.CreatedAfterUnix != 0 {
cond = cond.And(builder.Gte{"tracked_time.created_unix": opts.CreatedAfterUnix})
}
if opts.CreatedBeforeUnix != 0 {
cond = cond.And(builder.Lte{"tracked_time.created_unix": opts.CreatedBeforeUnix})
}
return cond return cond
} }

View File

@ -291,6 +291,8 @@ var migrations = []Migration{
// v117 -> v118 // v117 -> v118
NewMigration("Add block on rejected reviews branch protection", addBlockOnRejectedReviews), NewMigration("Add block on rejected reviews branch protection", addBlockOnRejectedReviews),
// v118 -> v119 // v118 -> v119
NewMigration("Add commit id and stale to reviews", addReviewCommitAndStale),
// v119 -> v120
NewMigration("add is_restricted column for users table", addIsRestricted), NewMigration("add is_restricted column for users table", addIsRestricted),
} }

View File

@ -4,14 +4,23 @@
package migrations package migrations
import "xorm.io/xorm" import (
"xorm.io/xorm"
)
func addIsRestricted(x *xorm.Engine) error { func addReviewCommitAndStale(x *xorm.Engine) error {
// User see models/user.go type Review struct {
type User struct { CommitID string `xorm:"VARCHAR(40)"`
ID int64 `xorm:"pk autoincr"` Stale bool `xorm:"NOT NULL DEFAULT false"`
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
} }
return x.Sync2(new(User)) type ProtectedBranch struct {
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
}
// Old reviews will have commit ID set to "" and not stale
if err := x.Sync2(new(Review)); err != nil {
return err
}
return x.Sync2(new(ProtectedBranch))
} }

17
models/migrations/v119.go Normal file
View File

@ -0,0 +1,17 @@
// Copyright 2020 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 addIsRestricted(x *xorm.Engine) error {
// User see models/user.go
type User struct {
ID int64 `xorm:"pk autoincr"`
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync2(new(User))
}

View File

@ -175,7 +175,11 @@ func (pr *PullRequest) GetDefaultMergeMessage() string {
return "" return ""
} }
} }
return fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.MustHeadUserName(), pr.HeadRepo.Name, pr.BaseBranch) if err := pr.LoadIssue(); err != nil {
log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
return ""
}
return fmt.Sprintf("Merge pull request '%s' (#%d) from %s/%s into %s", pr.Issue.Title, pr.Issue.Index, pr.MustHeadUserName(), pr.HeadBranch, pr.BaseBranch)
} }
// GetCommitMessages returns the commit messages between head and merge base (if there is one) // GetCommitMessages returns the commit messages between head and merge base (if there is one)

View File

@ -124,41 +124,43 @@ func generateRepoCommit(e Engine, repo, templateRepo, generateRepo *Repository,
return fmt.Errorf("checkGiteaTemplate: %v", err) return fmt.Errorf("checkGiteaTemplate: %v", err)
} }
if err := os.Remove(gt.Path); err != nil { if gt != nil {
return fmt.Errorf("remove .giteatemplate: %v", err) if err := os.Remove(gt.Path); err != nil {
} return fmt.Errorf("remove .giteatemplate: %v", err)
}
// Avoid walking tree if there are no globs // Avoid walking tree if there are no globs
if len(gt.Globs()) > 0 { if len(gt.Globs()) > 0 {
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
if err := filepath.Walk(tmpDirSlash, func(path string, info os.FileInfo, walkErr error) error { if err := filepath.Walk(tmpDirSlash, func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil { if walkErr != nil {
return walkErr return walkErr
}
if info.IsDir() {
return nil
}
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
for _, g := range gt.Globs() {
if g.Match(base) {
content, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if err := ioutil.WriteFile(path,
[]byte(generateExpansion(string(content), templateRepo, generateRepo)),
0644); err != nil {
return err
}
break
} }
if info.IsDir() {
return nil
}
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
for _, g := range gt.Globs() {
if g.Match(base) {
content, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if err := ioutil.WriteFile(path,
[]byte(generateExpansion(string(content), templateRepo, generateRepo)),
0644); err != nil {
return err
}
break
}
}
return nil
}); err != nil {
return err
} }
return nil
}); err != nil {
return err
} }
} }

View File

@ -53,7 +53,9 @@ type Review struct {
IssueID int64 `xorm:"index"` IssueID int64 `xorm:"index"`
Content string `xorm:"TEXT"` Content string `xorm:"TEXT"`
// Official is a review made by an assigned approver (counts towards approval) // Official is a review made by an assigned approver (counts towards approval)
Official bool `xorm:"NOT NULL DEFAULT false"` Official bool `xorm:"NOT NULL DEFAULT false"`
CommitID string `xorm:"VARCHAR(40)"`
Stale bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
@ -169,6 +171,8 @@ type CreateReviewOptions struct {
Issue *Issue Issue *Issue
Reviewer *User Reviewer *User
Official bool Official bool
CommitID string
Stale bool
} }
// IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals) // IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals)
@ -200,6 +204,8 @@ func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
ReviewerID: opts.Reviewer.ID, ReviewerID: opts.Reviewer.ID,
Content: opts.Content, Content: opts.Content,
Official: opts.Official, Official: opts.Official,
CommitID: opts.CommitID,
Stale: opts.Stale,
} }
if _, err := e.Insert(review); err != nil { if _, err := e.Insert(review); err != nil {
return nil, err return nil, err
@ -258,7 +264,7 @@ func IsContentEmptyErr(err error) bool {
} }
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content string) (*Review, *Comment, error) { func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) {
sess := x.NewSession() sess := x.NewSession()
defer sess.Close() defer sess.Close()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
@ -295,6 +301,8 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
Reviewer: doer, Reviewer: doer,
Content: content, Content: content,
Official: official, Official: official,
CommitID: commitID,
Stale: stale,
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -322,8 +330,10 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
review.Issue = issue review.Issue = issue
review.Content = content review.Content = content
review.Type = reviewType review.Type = reviewType
review.CommitID = commitID
review.Stale = stale
if _, err := sess.ID(review.ID).Cols("content, type, official").Update(review); err != nil { if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
return nil, nil, err return nil, nil, err
} }
} }
@ -374,3 +384,17 @@ func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
return reviews, nil return reviews, nil
} }
// MarkReviewsAsStale marks existing reviews as stale
func MarkReviewsAsStale(issueID int64) (err error) {
_, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
return
}
// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
_, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
return
}

View File

@ -172,6 +172,7 @@ type ProtectBranchForm struct {
ApprovalsWhitelistUsers string ApprovalsWhitelistUsers string
ApprovalsWhitelistTeams string ApprovalsWhitelistTeams string
BlockOnRejectedReviews bool BlockOnRejectedReviews bool
DismissStaleApprovals bool
} }
// Validate validates the fields // Validate validates the fields
@ -456,12 +457,13 @@ func (f *MergePullRequestForm) Validate(ctx *macaron.Context, errs binding.Error
// CodeCommentForm form for adding code comments for PRs // CodeCommentForm form for adding code comments for PRs
type CodeCommentForm struct { type CodeCommentForm struct {
Content string `binding:"Required"` Content string `binding:"Required"`
Side string `binding:"Required;In(previous,proposed)"` Side string `binding:"Required;In(previous,proposed)"`
Line int64 Line int64
TreePath string `form:"path" binding:"Required"` TreePath string `form:"path" binding:"Required"`
IsReview bool `form:"is_review"` IsReview bool `form:"is_review"`
Reply int64 `form:"reply"` Reply int64 `form:"reply"`
LatestCommitID string
} }
// Validate validates the fields // Validate validates the fields
@ -471,8 +473,9 @@ func (f *CodeCommentForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
// SubmitReviewForm for submitting a finished code review // SubmitReviewForm for submitting a finished code review
type SubmitReviewForm struct { type SubmitReviewForm struct {
Content string Content string
Type string `binding:"Required;In(approve,comment,reject)"` Type string `binding:"Required;In(approve,comment,reject)"`
CommitID string
} }
// Validate validates the fields // Validate validates the fields

View File

@ -112,3 +112,9 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
return NewCommand("format-patch", "--binary", "--stdout", base+"..."+head). return NewCommand("format-patch", "--binary", "--stdout", base+"..."+head).
RunInDirPipeline(repo.Path, w, nil) RunInDirPipeline(repo.Path, w, nil)
} }
// GetDiffFromMergeBase generates and return patch data from merge base to head
func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error {
return NewCommand("diff", "-p", "--binary", base+"..."+head).
RunInDirPipeline(repo.Path, w, nil)
}

View File

@ -477,7 +477,7 @@ func PushUpdate(repo *models.Repository, branch string, opts PushUpdateOptions)
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true) go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)
if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil { if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil {
log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err) log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err)
@ -528,7 +528,7 @@ func PushUpdates(repo *models.Repository, optsList []*PushUpdateOptions) error {
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, opts.Branch, pusher.Name) log.Trace("TriggerTask '%s/%s' by %s", repo.Name, opts.Branch, pusher.Name)
go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true) go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true, opts.OldCommitID, opts.NewCommitID)
if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil { if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil {
log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err) log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err)

View File

@ -7,6 +7,7 @@ package setting
import ( import (
"fmt" "fmt"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -44,7 +45,7 @@ func GetQueueSettings(name string) QueueSettings {
q := QueueSettings{} q := QueueSettings{}
sec := Cfg.Section("queue." + name) sec := Cfg.Section("queue." + name)
// DataDir is not directly inheritable // DataDir is not directly inheritable
q.DataDir = path.Join(Queue.DataDir, name) q.DataDir = filepath.Join(Queue.DataDir, name)
// QueueName is not directly inheritable either // QueueName is not directly inheritable either
q.QueueName = name + Queue.QueueName q.QueueName = name + Queue.QueueName
for _, key := range sec.Keys() { for _, key := range sec.Keys() {
@ -55,8 +56,8 @@ func GetQueueSettings(name string) QueueSettings {
q.QueueName = key.MustString(q.QueueName) q.QueueName = key.MustString(q.QueueName)
} }
} }
if !path.IsAbs(q.DataDir) { if !filepath.IsAbs(q.DataDir) {
q.DataDir = path.Join(AppDataPath, q.DataDir) q.DataDir = filepath.Join(AppDataPath, q.DataDir)
} }
sec.Key("DATADIR").SetValue(q.DataDir) sec.Key("DATADIR").SetValue(q.DataDir)
// The rest are... // The rest are...
@ -82,8 +83,8 @@ func GetQueueSettings(name string) QueueSettings {
func NewQueueService() { func NewQueueService() {
sec := Cfg.Section("queue") sec := Cfg.Section("queue")
Queue.DataDir = sec.Key("DATADIR").MustString("queues/") Queue.DataDir = sec.Key("DATADIR").MustString("queues/")
if !path.IsAbs(Queue.DataDir) { if !filepath.IsAbs(Queue.DataDir) {
Queue.DataDir = path.Join(AppDataPath, Queue.DataDir) Queue.DataDir = filepath.Join(AppDataPath, Queue.DataDir)
} }
Queue.Length = sec.Key("LENGTH").MustInt(20) Queue.Length = sec.Key("LENGTH").MustInt(20)
Queue.BatchLength = sec.Key("BATCH_LENGTH").MustInt(20) Queue.BatchLength = sec.Key("BATCH_LENGTH").MustInt(20)

View File

@ -38,6 +38,7 @@ type RepositoryMeta struct {
type Issue struct { type Issue struct {
ID int64 `json:"id"` ID int64 `json:"id"`
URL string `json:"url"` URL string `json:"url"`
HTMLURL string `json:"html_url"`
Index int64 `json:"number"` Index int64 `json:"number"`
Poster *User `json:"user"` Poster *User `json:"user"`
OriginalAuthor string `json:"original_author"` OriginalAuthor string `json:"original_author"`

View File

@ -142,7 +142,7 @@ func getDingtalkIssuesPayload(p *api.IssuePayload) (*DingtalkPayload, error) {
Title: issueTitle, Title: issueTitle,
HideAvatar: "0", HideAvatar: "0",
SingleTitle: "view issue", SingleTitle: "view issue",
SingleURL: p.Issue.URL, SingleURL: p.Issue.HTMLURL,
}, },
}, nil }, nil
} }

View File

@ -236,7 +236,7 @@ func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPa
{ {
Title: text, Title: text,
Description: attachmentText, Description: attachmentText,
URL: p.Issue.URL, URL: p.Issue.HTMLURL,
Color: color, Color: color,
Author: DiscordEmbedAuthor{ Author: DiscordEmbedAuthor{
Name: p.Sender.UserName, Name: p.Sender.UserName,

View File

@ -299,7 +299,7 @@ func getMSTeamsIssuesPayload(p *api.IssuePayload) (*MSTeamsPayload, error) {
Targets: []MSTeamsActionTarget{ Targets: []MSTeamsActionTarget{
{ {
Os: "default", Os: "default",
URI: p.Issue.URL, URI: p.Issue.HTMLURL,
}, },
}, },
}, },

View File

@ -158,7 +158,7 @@ func getSlackIssuesPayload(p *api.IssuePayload, slack *SlackMeta) (*SlackPayload
pl.Attachments = []SlackAttachment{{ pl.Attachments = []SlackAttachment{{
Color: fmt.Sprintf("%x", color), Color: fmt.Sprintf("%x", color),
Title: issueTitle, Title: issueTitle,
TitleLink: p.Issue.URL, TitleLink: p.Issue.HTMLURL,
Text: attachmentText, Text: attachmentText,
}} }}
} }

View File

@ -148,6 +148,25 @@ func getTelegramPullRequestPayload(p *api.PullRequestPayload) (*TelegramPayload,
}, nil }, nil
} }
func getTelegramPullRequestApprovalPayload(p *api.PullRequestPayload, event models.HookEventType) (*TelegramPayload, error) {
var text, attachmentText string
switch p.Action {
case api.HookIssueSynchronized:
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
}
text = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
attachmentText = p.Review.Content
}
return &TelegramPayload{
Message: text + "\n" + attachmentText,
}, nil
}
func getTelegramRepositoryPayload(p *api.RepositoryPayload) (*TelegramPayload, error) { func getTelegramRepositoryPayload(p *api.RepositoryPayload) (*TelegramPayload, error) {
var title string var title string
switch p.Action { switch p.Action {
@ -192,6 +211,8 @@ func GetTelegramPayload(p api.Payloader, event models.HookEventType, meta string
return getTelegramPushPayload(p.(*api.PushPayload)) return getTelegramPushPayload(p.(*api.PushPayload))
case models.HookEventPullRequest: case models.HookEventPullRequest:
return getTelegramPullRequestPayload(p.(*api.PullRequestPayload)) return getTelegramPullRequestPayload(p.(*api.PullRequestPayload))
case models.HookEventPullRequestRejected, models.HookEventPullRequestApproved, models.HookEventPullRequestComment:
return getTelegramPullRequestApprovalPayload(p.(*api.PullRequestPayload), event)
case models.HookEventRepository: case models.HookEventRepository:
return getTelegramRepositoryPayload(p.(*api.RepositoryPayload)) return getTelegramRepositoryPayload(p.(*api.RepositoryPayload))
case models.HookEventRelease: case models.HookEventRelease:

View File

@ -1413,6 +1413,8 @@ settings.protect_approvals_whitelist_enabled = Restrict approvals to whitelisted
settings.protect_approvals_whitelist_enabled_desc = Only reviews from whitelisted users or teams will count to the required approvals. Without approval whitelist, reviews from anyone with write access count to the required approvals. settings.protect_approvals_whitelist_enabled_desc = Only reviews from whitelisted users or teams will count to the required approvals. Without approval whitelist, reviews from anyone with write access count to the required approvals.
settings.protect_approvals_whitelist_users = Whitelisted reviewers: settings.protect_approvals_whitelist_users = Whitelisted reviewers:
settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews: settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews:
settings.dismiss_stale_approvals = Dismiss stale approvals
settings.dismiss_stale_approvals_desc = When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed.
settings.add_protected_branch = Enable protection settings.add_protected_branch = Enable protection
settings.delete_protected_branch = Disable protection settings.delete_protected_branch = Disable protection
settings.update_protect_branch_success = Branch protection for branch '%s' has been updated. settings.update_protect_branch_success = Branch protection for branch '%s' has been updated.

View File

@ -1596,7 +1596,7 @@ settings.full_name=Pilns vārds, uzvārds
settings.website=Mājas lapa settings.website=Mājas lapa
settings.location=Atrašanās vieta settings.location=Atrašanās vieta
settings.permission=Tiesības settings.permission=Tiesības
settings.repoadminchangeteam=Repozitorija administrators var pievienot vain noņemt piekļuvi komandām settings.repoadminchangeteam=Repozitorija administrators var pievienot vai noņemt piekļuvi komandām
settings.visibility=Redzamība settings.visibility=Redzamība
settings.visibility.public=Publiska settings.visibility.public=Publiska
settings.visibility.limited=Ierobežota (redzama tikai autorizētiem lietotājiem) settings.visibility.limited=Ierobežota (redzama tikai autorizētiem lietotājiem)
@ -2025,8 +2025,54 @@ monitor.execute_time=Izpildes laiks
monitor.process.cancel=Atcelt procesu monitor.process.cancel=Atcelt procesu
monitor.process.cancel_desc=Procesa atcelšana var radīt datu zaudējumus monitor.process.cancel_desc=Procesa atcelšana var radīt datu zaudējumus
monitor.process.cancel_notices=Atcelt: <strong>%s</strong>? monitor.process.cancel_notices=Atcelt: <strong>%s</strong>?
monitor.queues=Rindas
monitor.queue=Rinda: %s
monitor.queue.name=Nosaukums
monitor.queue.type=Veids
monitor.queue.exemplar=Eksemplāra veids
monitor.queue.numberworkers=Strādņu skaits
monitor.queue.maxnumberworkers=Maksimālais strādņu skaits
monitor.queue.review=Pārbaudīt konfigurāciju
monitor.queue.review_add=Pārbaudīt/Pievienot strādņus
monitor.queue.configuration=Sākotnējā konfigurācija
monitor.queue.nopool.title=Nav strādņu pūla
monitor.queue.nopool.desc=Šī rinda apvieno citas rindas un tai nav strādņu pūla.
monitor.queue.wrapped.desc=Apvienojošā rinda apvieno lēni startējošās rindas, uzkrājot sarindotos pieprasījumus kanālā. Tai nav strādņu pūla.
monitor.queue.persistable-channel.desc=Patstāvīgas kanāli apvieno divas rindas, kanāla rindu, kurai ir savs strādņu pūls un līmeņu rindu patstāvīgajiem pieprasījumiem no iepriekšejām izslēgšanām. Tai nav strādņu pūla.
monitor.queue.pool.timeout=Noildze
monitor.queue.pool.addworkers.title=Pievienot strādņus
monitor.queue.pool.addworkers.submit=Pievienot
monitor.queue.pool.addworkers.desc=Pievienot strādņus šim pūlam ar vai bez noildzes. Ja uzstādīsies noildzi, tad šie strādņi tiks noņemti no pūla, kad noildze būs iestājusies.
monitor.queue.pool.addworkers.numberworkers.placeholder=Strādņu skaits
monitor.queue.pool.addworkers.timeout.placeholder=Norādiet 0, lai nebūtu noildzes
monitor.queue.pool.addworkers.mustnumbergreaterzero=Strādņu skaitam, ko pievienot, ir jābūt lielākam par nulli
monitor.queue.pool.addworkers.musttimeoutduration=Noildzei ir jābūt norādītai kā ilgumam, piemēram, 5m vai 0
monitor.queue.settings.title=Pūla iestatījumi
monitor.queue.settings.desc=Pūli var dinamiski augt un paildzinātu atbildi uz strādņu rindas bloķēšanu. Šis izmaiņas ietekmēs pašreizējās strādņu grupas.
monitor.queue.settings.timeout=Pagarināt noildzi
monitor.queue.settings.timeout.placeholder=Pašalaik %[1]v
monitor.queue.settings.timeout.error=Noildzei ir jābūt norādītai kā ilgumam, piemēram, 5m vai 0
monitor.queue.settings.numberworkers=Palielināt strādņu skaitu
monitor.queue.settings.numberworkers.placeholder=Pašalaik %[1]d
monitor.queue.settings.numberworkers.error=Strādņu skaitam ir jābūt lielākam vai vienādam ar nulli
monitor.queue.settings.maxnumberworkers=Maksimālais strādņu skaits
monitor.queue.settings.maxnumberworkers.placeholder=Pašalaik %[1]d
monitor.queue.settings.maxnumberworkers.error=Maksimālajam strādņu skaitam ir jābūt skaitlim
monitor.queue.settings.submit=Saglabāt iestatījumus
monitor.queue.settings.changed=Iestatījumi saglabāti
monitor.queue.settings.blocktimeout=Pašreizējās grupas noildze
monitor.queue.settings.blocktimeout.value=%[1]v
monitor.queue.pool.none=Rindai nav pūla
monitor.queue.pool.added=Strādņu grupa pievienota
monitor.queue.pool.max_changed=Maksimālais strādņu skaits mainīts
monitor.queue.pool.workers.title=Aktīvās strādņu grupas
monitor.queue.pool.workers.none=Nav strādņu grupu.
monitor.queue.pool.cancel=Izslēgt strādņu grupu
monitor.queue.pool.cancelling=Strādņu grupa tiek izslēgta
monitor.queue.pool.cancel_notices=Izslēgt šo grupu ar %s strādņiem?
monitor.queue.pool.cancel_desc=Atstājot rindu bez nevienas strādņu grupas, var radīt pieprasījumu bloķēšanos.
notices.system_notice_list=Sistēmas paziņojumi notices.system_notice_list=Sistēmas paziņojumi
notices.view_detail_header=Skatīt paziņojuma detaļas notices.view_detail_header=Skatīt paziņojuma detaļas

View File

@ -654,7 +654,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/times", func() { m.Group("/times", func() {
m.Combo("").Get(repo.ListTrackedTimesByRepository) m.Combo("").Get(repo.ListTrackedTimesByRepository)
m.Combo("/:timetrackingusername").Get(repo.ListTrackedTimesByUser) m.Combo("/:timetrackingusername").Get(repo.ListTrackedTimesByUser)
}, mustEnableIssues) }, mustEnableIssues, reqToken())
m.Group("/issues", func() { m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues). m.Combo("").Get(repo.ListIssues).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue) Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
@ -688,12 +688,12 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Delete("/:id", reqToken(), repo.DeleteIssueLabel) m.Delete("/:id", reqToken(), repo.DeleteIssueLabel)
}) })
m.Group("/times", func() { m.Group("/times", func() {
m.Combo("", reqToken()). m.Combo("").
Get(repo.ListTrackedTimes). Get(repo.ListTrackedTimes).
Post(bind(api.AddTimeOption{}), repo.AddTime). Post(bind(api.AddTimeOption{}), repo.AddTime).
Delete(repo.ResetIssueTime) Delete(repo.ResetIssueTime)
m.Delete("/:id", reqToken(), repo.DeleteTime) m.Delete("/:id", repo.DeleteTime)
}) }, reqToken())
m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
m.Group("/stopwatch", func() { m.Group("/stopwatch", func() {
m.Post("/start", reqToken(), repo.StartIssueStopwatch) m.Post("/start", reqToken(), repo.StartIssueStopwatch)

View File

@ -5,12 +5,15 @@
package repo package repo
import ( import (
"fmt"
"net/http" "net/http"
"strings"
"time" "time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
) )
// ListTrackedTimes list all the tracked times of an issue // ListTrackedTimes list all the tracked times of an issue
@ -37,6 +40,16 @@ func ListTrackedTimes(ctx *context.APIContext) {
// type: integer // type: integer
// format: int64 // format: int64
// required: true // required: true
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/TrackedTimeList" // "$ref": "#/responses/TrackedTimeList"
@ -62,6 +75,11 @@ func ListTrackedTimes(ctx *context.APIContext) {
IssueID: issue.ID, IssueID: issue.ID,
} }
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
ctx.InternalServerError(err)
return
}
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin { if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin {
opts.UserID = ctx.User.ID opts.UserID = ctx.User.ID
} }
@ -141,7 +159,7 @@ func AddTime(ctx *context.APIContext, form api.AddTimeOption) {
//allow only RepoAdmin, Admin and User to add time //allow only RepoAdmin, Admin and User to add time
user, err = models.GetUserByName(form.User) user, err = models.GetUserByName(form.User)
if err != nil { if err != nil {
ctx.Error(500, "GetUserByName", err) ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
} }
} }
} }
@ -195,33 +213,33 @@ func ResetIssueTime(ctx *context.APIContext) {
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403": // "403":
// "$ref": "#/responses/error" // "$ref": "#/responses/forbidden"
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil { if err != nil {
if models.IsErrIssueNotExist(err) { if models.IsErrIssueNotExist(err) {
ctx.NotFound(err) ctx.NotFound(err)
} else { } else {
ctx.Error(500, "GetIssueByIndex", err) ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
} }
return return
} }
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) { if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
if !ctx.Repo.Repository.IsTimetrackerEnabled() { if !ctx.Repo.Repository.IsTimetrackerEnabled() {
ctx.JSON(400, struct{ Message string }{Message: "time tracking disabled"}) ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
return return
} }
ctx.Status(403) ctx.Status(http.StatusForbidden)
return return
} }
err = models.DeleteIssueUserTimes(issue, ctx.User) err = models.DeleteIssueUserTimes(issue, ctx.User)
if err != nil { if err != nil {
if models.IsErrNotExist(err) { if models.IsErrNotExist(err) {
ctx.Error(404, "DeleteIssueUserTimes", err) ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err)
} else { } else {
ctx.Error(500, "DeleteIssueUserTimes", err) ctx.Error(http.StatusInternalServerError, "DeleteIssueUserTimes", err)
} }
return return
} }
@ -266,52 +284,53 @@ func DeleteTime(ctx *context.APIContext) {
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403": // "403":
// "$ref": "#/responses/error" // "$ref": "#/responses/forbidden"
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil { if err != nil {
if models.IsErrIssueNotExist(err) { if models.IsErrIssueNotExist(err) {
ctx.NotFound(err) ctx.NotFound(err)
} else { } else {
ctx.Error(500, "GetIssueByIndex", err) ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
} }
return return
} }
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) { if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
if !ctx.Repo.Repository.IsTimetrackerEnabled() { if !ctx.Repo.Repository.IsTimetrackerEnabled() {
ctx.JSON(400, struct{ Message string }{Message: "time tracking disabled"}) ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
return return
} }
ctx.Status(403) ctx.Status(http.StatusForbidden)
return return
} }
time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id")) time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
ctx.Error(500, "GetTrackedTimeByID", err) ctx.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err)
return return
} }
if !ctx.User.IsAdmin && time.UserID != ctx.User.ID { if !ctx.User.IsAdmin && time.UserID != ctx.User.ID {
//Only Admin and User itself can delete their time //Only Admin and User itself can delete their time
ctx.Status(403) ctx.Status(http.StatusForbidden)
return return
} }
err = models.DeleteTime(time) err = models.DeleteTime(time)
if err != nil { if err != nil {
ctx.Error(500, "DeleteTime", err) ctx.Error(http.StatusInternalServerError, "DeleteTime", err)
return return
} }
ctx.Status(204) ctx.Status(http.StatusNoContent)
} }
// ListTrackedTimesByUser lists all tracked times of the user // ListTrackedTimesByUser lists all tracked times of the user
func ListTrackedTimesByUser(ctx *context.APIContext) { func ListTrackedTimesByUser(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/times/{user} user userTrackedTimes // swagger:operation GET /repos/{owner}/{repo}/times/{user} repository userTrackedTimes
// --- // ---
// summary: List a user's tracked times in a repo // summary: List a user's tracked times in a repo
// deprecated: true
// produces: // produces:
// - application/json // - application/json
// parameters: // parameters:
@ -335,6 +354,8 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
// "$ref": "#/responses/TrackedTimeList" // "$ref": "#/responses/TrackedTimeList"
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
if !ctx.Repo.Repository.IsTimetrackerEnabled() { if !ctx.Repo.Repository.IsTimetrackerEnabled() {
ctx.Error(http.StatusBadRequest, "", "time tracking disabled") ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
@ -353,9 +374,23 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
ctx.NotFound() ctx.NotFound()
return return
} }
trackedTimes, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin && ctx.User.ID != user.ID {
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query user not allowed not enouth rights"))
return
}
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin && ctx.User.ID != user.ID {
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query user not allowed not enouth rights"))
return
}
opts := models.FindTrackedTimesOptions{
UserID: user.ID, UserID: user.ID,
RepositoryID: ctx.Repo.Repository.ID}) RepositoryID: ctx.Repo.Repository.ID,
}
trackedTimes, err := models.GetTrackedTimes(opts)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
return return
@ -385,11 +420,27 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
// description: name of the repo // description: name of the repo
// type: string // type: string
// required: true // required: true
// - name: user
// in: query
// description: optional filter by user
// type: string
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/TrackedTimeList" // "$ref": "#/responses/TrackedTimeList"
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
if !ctx.Repo.Repository.IsTimetrackerEnabled() { if !ctx.Repo.Repository.IsTimetrackerEnabled() {
ctx.Error(http.StatusBadRequest, "", "time tracking disabled") ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
@ -400,8 +451,30 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
RepositoryID: ctx.Repo.Repository.ID, RepositoryID: ctx.Repo.Repository.ID,
} }
// Filters
qUser := strings.Trim(ctx.Query("user"), " ")
if qUser != "" {
user, err := models.GetUserByName(qUser)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
return
}
opts.UserID = user.ID
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
ctx.InternalServerError(err)
return
}
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin { if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin {
opts.UserID = ctx.User.ID if opts.UserID == 0 {
opts.UserID = ctx.User.ID
} else {
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query user not allowed not enouth rights"))
return
}
} }
trackedTimes, err := models.GetTrackedTimes(opts) trackedTimes, err := models.GetTrackedTimes(opts)
@ -423,18 +496,39 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
// summary: List the current user's tracked times // summary: List the current user's tracked times
// produces: // produces:
// - application/json // - application/json
// parameters:
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/TrackedTimeList" // "$ref": "#/responses/TrackedTimeList"
trackedTimes, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{UserID: ctx.User.ID}) opts := models.FindTrackedTimesOptions{UserID: ctx.User.ID}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
ctx.InternalServerError(err)
return
}
trackedTimes, err := models.GetTrackedTimes(opts)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err) ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err)
return return
} }
if err = trackedTimes.LoadAttributes(); err != nil { if err = trackedTimes.LoadAttributes(); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return return
} }
ctx.JSON(http.StatusOK, trackedTimes.APIFormat()) ctx.JSON(http.StatusOK, trackedTimes.APIFormat())
} }

View File

@ -4,7 +4,12 @@
package utils package utils
import "code.gitea.io/gitea/modules/context" import (
"strings"
"time"
"code.gitea.io/gitea/modules/context"
)
// UserID user ID of authenticated user, or 0 if not authenticated // UserID user ID of authenticated user, or 0 if not authenticated
func UserID(ctx *context.APIContext) int64 { func UserID(ctx *context.APIContext) int64 {
@ -13,3 +18,29 @@ func UserID(ctx *context.APIContext) int64 {
} }
return ctx.User.ID return ctx.User.ID
} }
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
func GetQueryBeforeSince(ctx *context.APIContext) (before, since int64, err error) {
qCreatedBefore := strings.Trim(ctx.Query("before"), " ")
if qCreatedBefore != "" {
createdBefore, err := time.Parse(time.RFC3339, qCreatedBefore)
if err != nil {
return 0, 0, err
}
if !createdBefore.IsZero() {
before = createdBefore.Unix()
}
}
qCreatedAfter := strings.Trim(ctx.Query("since"), " ")
if qCreatedAfter != "" {
createdAfter, err := time.Parse(time.RFC3339, qCreatedAfter)
if err != nil {
return 0, 0, err
}
if !createdAfter.IsZero() {
since = createdAfter.Unix()
}
}
return before, since, nil
}

View File

@ -841,7 +841,7 @@ func TriggerTask(ctx *context.Context) {
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true) go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, "", "")
ctx.Status(202) ctx.Status(202)
} }

View File

@ -37,12 +37,14 @@ func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {
comment, err := pull_service.CreateCodeComment( comment, err := pull_service.CreateCodeComment(
ctx.User, ctx.User,
ctx.Repo.GitRepo,
issue, issue,
signedLine, signedLine,
form.Content, form.Content,
form.TreePath, form.TreePath,
form.IsReview, form.IsReview,
form.Reply, form.Reply,
form.LatestCommitID,
) )
if err != nil { if err != nil {
ctx.ServerError("CreateCodeComment", err) ctx.ServerError("CreateCodeComment", err)
@ -95,7 +97,7 @@ func SubmitReview(ctx *context.Context, form auth.SubmitReviewForm) {
} }
} }
_, comm, err := pull_service.SubmitReview(ctx.User, issue, reviewType, form.Content) _, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID)
if err != nil { if err != nil {
if models.IsContentEmptyErr(err) { if models.IsContentEmptyErr(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty")) ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))

View File

@ -245,6 +245,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
} }
} }
protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{ err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
UserIDs: whitelistUsers, UserIDs: whitelistUsers,

View File

@ -64,7 +64,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
} }
defer func() { defer func() {
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false) go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
}() }()
// Clone base repo. // Clone base repo.

View File

@ -5,10 +5,14 @@
package pull package pull
import ( import (
"bufio"
"bytes"
"context" "context"
"fmt" "fmt"
"os" "os"
"path" "path"
"strings"
"time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -16,6 +20,8 @@ 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"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
"github.com/unknwon/com"
) )
// NewPullRequest creates new pull request with labels for repository. // NewPullRequest creates new pull request with labels for repository.
@ -168,7 +174,7 @@ func addHeadRepoTasks(prs []*models.PullRequest) {
// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch, // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
// and generate new patch for testing as needed. // and generate new patch for testing as needed.
func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSync bool) { func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch) log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
// There is no sensible way to shut this down ":-(" // There is no sensible way to shut this down ":-("
@ -191,6 +197,22 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy
} }
if err == nil { if err == nil {
for _, pr := range prs { for _, pr := range prs {
if newCommitID != "" && newCommitID != git.EmptySHA {
changed, err := checkIfPRContentChanged(pr, oldCommitID, newCommitID)
if err != nil {
log.Error("checkIfPRContentChanged: %v", err)
}
if changed {
// Mark old reviews as stale if diff to mergebase has changed
if err := models.MarkReviewsAsStale(pr.IssueID); err != nil {
log.Error("MarkReviewsAsStale: %v", err)
}
}
if err := models.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil {
log.Error("MarkReviewsAsNotStale: %v", err)
}
}
pr.Issue.PullRequest = pr pr.Issue.PullRequest = pr
notification.NotifyPullRequestSynchronized(doer, pr) notification.NotifyPullRequestSynchronized(doer, pr)
} }
@ -211,6 +233,78 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy
}) })
} }
// checkIfPRContentChanged checks if diff to target branch has changed by push
// A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
func checkIfPRContentChanged(pr *models.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
if err = pr.GetHeadRepo(); err != nil {
return false, fmt.Errorf("GetHeadRepo: %v", err)
} else if pr.HeadRepo == nil {
// corrupt data assumed changed
return true, nil
}
if err = pr.GetBaseRepo(); err != nil {
return false, fmt.Errorf("GetBaseRepo: %v", err)
}
headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
if err != nil {
return false, fmt.Errorf("OpenRepository: %v", err)
}
defer headGitRepo.Close()
// Add a temporary remote.
tmpRemote := "checkIfPRContentChanged-" + com.ToStr(time.Now().UnixNano())
if err = headGitRepo.AddRemote(tmpRemote, models.RepoPath(pr.BaseRepo.MustOwner().Name, pr.BaseRepo.Name), true); err != nil {
return false, fmt.Errorf("AddRemote: %s/%s-%s: %v", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err)
}
defer func() {
if err := headGitRepo.RemoveRemote(tmpRemote); err != nil {
log.Error("checkIfPRContentChanged: RemoveRemote: %s/%s-%s: %v", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err)
}
}()
// To synchronize repo and get a base ref
_, base, err := headGitRepo.GetMergeBase(tmpRemote, pr.BaseBranch, pr.HeadBranch)
if err != nil {
return false, fmt.Errorf("GetMergeBase: %v", err)
}
diffBefore := &bytes.Buffer{}
diffAfter := &bytes.Buffer{}
if err := headGitRepo.GetDiffFromMergeBase(base, oldCommitID, diffBefore); err != nil {
// If old commit not found, assume changed.
log.Debug("GetDiffFromMergeBase: %v", err)
return true, nil
}
if err := headGitRepo.GetDiffFromMergeBase(base, newCommitID, diffAfter); err != nil {
// New commit should be found
return false, fmt.Errorf("GetDiffFromMergeBase: %v", err)
}
diffBeforeLines := bufio.NewScanner(diffBefore)
diffAfterLines := bufio.NewScanner(diffAfter)
for diffBeforeLines.Scan() && diffAfterLines.Scan() {
if strings.HasPrefix(diffBeforeLines.Text(), "index") && strings.HasPrefix(diffAfterLines.Text(), "index") {
// file hashes can change without the diff changing
continue
} else if strings.HasPrefix(diffBeforeLines.Text(), "@@") && strings.HasPrefix(diffAfterLines.Text(), "@@") {
// the location of the difference may change
continue
} else if !bytes.Equal(diffBeforeLines.Bytes(), diffAfterLines.Bytes()) {
return true, nil
}
}
if diffBeforeLines.Scan() || diffAfterLines.Scan() {
// Diffs not of equal length
return true, nil
}
return false, nil
}
// PushToBaseRepo pushes commits from branches of head repository to // PushToBaseRepo pushes commits from branches of head repository to
// corresponding branches of base repository. // corresponding branches of base repository.
// FIXME: Only push branches that are actually updates? // FIXME: Only push branches that are actually updates?

View File

@ -18,7 +18,7 @@ import (
) )
// CreateCodeComment creates a comment on the code line // CreateCodeComment creates a comment on the code line
func CreateCodeComment(doer *models.User, issue *models.Issue, line int64, content string, treePath string, isReview bool, replyReviewID int64) (*models.Comment, error) { func CreateCodeComment(doer *models.User, gitRepo *git.Repository, issue *models.Issue, line int64, content string, treePath string, isReview bool, replyReviewID int64, latestCommitID string) (*models.Comment, error) {
var ( var (
existsReview bool existsReview bool
@ -73,6 +73,7 @@ func CreateCodeComment(doer *models.User, issue *models.Issue, line int64, conte
Reviewer: doer, Reviewer: doer,
Issue: issue, Issue: issue,
Official: false, Official: false,
CommitID: latestCommitID,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -94,7 +95,7 @@ func CreateCodeComment(doer *models.User, issue *models.Issue, line int64, conte
if !isReview && !existsReview { if !isReview && !existsReview {
// Submit the review we've just created so the comment shows up in the issue view // Submit the review we've just created so the comment shows up in the issue view
if _, _, err = SubmitReview(doer, issue, models.ReviewTypeComment, ""); err != nil { if _, _, err = SubmitReview(doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID); err != nil {
return nil, err return nil, err
} }
} }
@ -159,16 +160,36 @@ func createCodeComment(doer *models.User, repo *models.Repository, issue *models
} }
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
func SubmitReview(doer *models.User, issue *models.Issue, reviewType models.ReviewType, content string) (*models.Review, *models.Comment, error) { func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string) (*models.Review, *models.Comment, error) {
review, comm, err := models.SubmitReview(doer, issue, reviewType, content)
if err != nil {
return nil, nil, err
}
pr, err := issue.GetPullRequest() pr, err := issue.GetPullRequest()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
var stale bool
if reviewType != models.ReviewTypeApprove && reviewType != models.ReviewTypeReject {
stale = false
} else {
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
return nil, nil, err
}
if headCommitID == commitID {
stale = false
} else {
stale, err = checkIfPRContentChanged(pr, commitID, headCommitID)
if err != nil {
return nil, nil, err
}
}
}
review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale)
if err != nil {
return nil, nil, err
}
notification.NotifyPullRequestReview(pr, review, comm) notification.NotifyPullRequestReview(pr, review, comm)
return review, comm, nil return review, comm, nil

View File

@ -4,6 +4,7 @@
{{end}} {{end}}
<form class="ui form {{if $.hidden}}hide comment-form comment-form-reply{{end}}" action="{{$.root.Issue.HTMLURL}}/files/reviews/comments" method="post"> <form class="ui form {{if $.hidden}}hide comment-form comment-form-reply{{end}}" action="{{$.root.Issue.HTMLURL}}/files/reviews/comments" method="post">
{{$.root.CsrfTokenHtml}} {{$.root.CsrfTokenHtml}}
<input type="hidden" name="latest_commit_id" value="{{$.root.AfterCommitID}}"/>
<input type="hidden" name="side" value="{{if $.Side}}{{$.Side}}{{end}}"> <input type="hidden" name="side" value="{{if $.Side}}{{$.Side}}{{end}}">
<input type="hidden" name="line" value="{{if $.Line}}{{$.Line}}{{end}}"> <input type="hidden" name="line" value="{{if $.Line}}{{$.Line}}{{end}}">
<input type="hidden" name="path" value="{{if $.File}}{{$.File}}{{end}}"> <input type="hidden" name="path" value="{{if $.File}}{{$.File}}{{end}}">

View File

@ -7,6 +7,7 @@
<div class="ui clearing segment"> <div class="ui clearing segment">
<form class="ui form" action="{{.Link}}/reviews/submit" method="post"> <form class="ui form" action="{{.Link}}/reviews/submit" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input type="hidden" name="commit_id" value="{{.AfterCommitID}}"/>
<i class="ui right floated link icon close"></i> <i class="ui right floated link icon close"></i>
<div class="header"> <div class="header">
{{$.i18n.Tr "repo.diff.review.header"}} {{$.i18n.Tr "repo.diff.review.header"}}

View File

@ -13,6 +13,11 @@
{{else}}grey{{end}}"> {{else}}grey{{end}}">
<span class="octicon octicon-{{.Type.Icon}}"></span> <span class="octicon octicon-{{.Type.Icon}}"></span>
</span> </span>
{{if .Stale}}
<span class="type-icon text grey">
<i class="octicon icon fa-hourglass-end"></i>
</span>
{{end}}
<a class="ui avatar image" href="{{.Reviewer.HomeLink}}"> <a class="ui avatar image" href="{{.Reviewer.HomeLink}}">
<img src="{{.Reviewer.RelAvatarLink}}"> <img src="{{.Reviewer.RelAvatarLink}}">
</a> </a>

View File

@ -211,6 +211,14 @@
<p class="help">{{.i18n.Tr "repo.settings.block_rejected_reviews_desc"}}</p> <p class="help">{{.i18n.Tr "repo.settings.block_rejected_reviews_desc"}}</p>
</div> </div>
</div> </div>
<div class="field">
<div class="ui checkbox">
<input name="dismiss_stale_approvals" type="checkbox" {{if .Branch.DismissStaleApprovals}}checked{{end}}>
<label for="dismiss_stale_approvals">{{.i18n.Tr "repo.settings.dismiss_stale_approvals"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.dismiss_stale_approvals_desc"}}</p>
</div>
</div>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>

View File

@ -4433,6 +4433,20 @@
"name": "index", "name": "index",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated after the given time. This is a timestamp in RFC 3339 format",
"name": "since",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated before the given time. This is a timestamp in RFC 3339 format",
"name": "before",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -4543,7 +4557,7 @@
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"403": { "403": {
"$ref": "#/responses/error" "$ref": "#/responses/forbidden"
} }
} }
} }
@ -4601,7 +4615,7 @@
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"403": { "403": {
"$ref": "#/responses/error" "$ref": "#/responses/forbidden"
} }
} }
} }
@ -6419,6 +6433,26 @@
"name": "repo", "name": "repo",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"description": "optional filter by user",
"name": "user",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated after the given time. This is a timestamp in RFC 3339 format",
"name": "since",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated before the given time. This is a timestamp in RFC 3339 format",
"name": "before",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -6427,6 +6461,9 @@
}, },
"400": { "400": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
},
"403": {
"$ref": "#/responses/forbidden"
} }
} }
} }
@ -6437,10 +6474,11 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"user" "repository"
], ],
"summary": "List a user's tracked times in a repo", "summary": "List a user's tracked times in a repo",
"operationId": "userTrackedTimes", "operationId": "userTrackedTimes",
"deprecated": true,
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
@ -6470,6 +6508,9 @@
}, },
"400": { "400": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
},
"403": {
"$ref": "#/responses/forbidden"
} }
} }
} }
@ -7685,6 +7726,22 @@
], ],
"summary": "List the current user's tracked times", "summary": "List the current user's tracked times",
"operationId": "userCurrentTrackedTimes", "operationId": "userCurrentTrackedTimes",
"parameters": [
{
"type": "string",
"format": "date-time",
"description": "Only show times updated after the given time. This is a timestamp in RFC 3339 format",
"name": "since",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated before the given time. This is a timestamp in RFC 3339 format",
"name": "before",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"$ref": "#/responses/TrackedTimeList" "$ref": "#/responses/TrackedTimeList"
@ -10248,6 +10305,10 @@
"format": "date-time", "format": "date-time",
"x-go-name": "Deadline" "x-go-name": "Deadline"
}, },
"html_url": {
"type": "string",
"x-go-name": "HTMLURL"
},
"id": { "id": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",