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:
commit
3459547fca
@ -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
|
||||||
|
```
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
17
models/migrations/v119.go
Normal 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))
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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"`
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
|
@ -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}}">
|
||||||
|
@ -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"}}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user