// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
org_model "code.gitea.io/gitea/models/organization"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
10 months ago
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
// ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error.
type ErrPullRequestNotExist struct {
ID int64
IssueID int64
HeadRepoID int64
BaseRepoID int64
HeadBranch string
BaseBranch string
}
// IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist.
func IsErrPullRequestNotExist ( err error ) bool {
_ , ok := err . ( ErrPullRequestNotExist )
return ok
}
func ( err ErrPullRequestNotExist ) Error ( ) string {
return fmt . Sprintf ( "pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]" ,
err . ID , err . IssueID , err . HeadRepoID , err . BaseRepoID , err . HeadBranch , err . BaseBranch )
}
func ( err ErrPullRequestNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error
type ErrPullRequestAlreadyExists struct {
ID int64
IssueID int64
HeadRepoID int64
BaseRepoID int64
HeadBranch string
BaseBranch string
}
// IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists.
func IsErrPullRequestAlreadyExists ( err error ) bool {
_ , ok := err . ( ErrPullRequestAlreadyExists )
return ok
}
// Error does pretty-printing :D
func ( err ErrPullRequestAlreadyExists ) Error ( ) string {
return fmt . Sprintf ( "pull request already exists for these targets [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]" ,
err . ID , err . IssueID , err . HeadRepoID , err . BaseRepoID , err . HeadBranch , err . BaseBranch )
}
func ( err ErrPullRequestAlreadyExists ) Unwrap ( ) error {
return util . ErrAlreadyExist
}
// ErrPullWasClosed is used close a closed pull request
type ErrPullWasClosed struct {
ID int64
Index int64
}
// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed.
func IsErrPullWasClosed ( err error ) bool {
_ , ok := err . ( ErrPullWasClosed )
return ok
}
func ( err ErrPullWasClosed ) Error ( ) string {
return fmt . Sprintf ( "Pull request [%d] %d was already closed" , err . ID , err . Index )
}
// PullRequestType defines pull request type
type PullRequestType int
// Enumerate all the pull request types
const (
PullRequestGitea PullRequestType = iota
PullRequestGit
)
// PullRequestStatus defines pull request status
type PullRequestStatus int
// Enumerate all the pull request status
const (
PullRequestStatusConflict PullRequestStatus = iota
PullRequestStatusChecking
PullRequestStatusMergeable
PullRequestStatusManuallyMerged
PullRequestStatusError
PullRequestStatusEmpty
PullRequestStatusAncestor
)
func ( status PullRequestStatus ) String ( ) string {
switch status {
case PullRequestStatusConflict :
return "CONFLICT"
case PullRequestStatusChecking :
return "CHECKING"
case PullRequestStatusMergeable :
return "MERGEABLE"
case PullRequestStatusManuallyMerged :
return "MANUALLY_MERGED"
case PullRequestStatusError :
return "ERROR"
case PullRequestStatusEmpty :
return "EMPTY"
case PullRequestStatusAncestor :
return "ANCESTOR"
default :
return strconv . Itoa ( int ( status ) )
}
}
// PullRequestFlow the flow of pull request
type PullRequestFlow int
const (
// PullRequestFlowGithub github flow from head branch to base branch
PullRequestFlowGithub PullRequestFlow = iota
// PullRequestFlowAGit Agit flow pull request, head branch is not exist
PullRequestFlowAGit
)
// PullRequest represents relation between pull request and repositories.
type PullRequest struct {
ID int64 ` xorm:"pk autoincr" `
Type PullRequestType
Status PullRequestStatus
ConflictedFiles [ ] string ` xorm:"TEXT JSON" `
CommitsAhead int
CommitsBehind int
ChangedProtectedFiles [ ] string ` xorm:"TEXT JSON" `
IssueID int64 ` xorm:"INDEX" `
Issue * Issue ` xorm:"-" `
Index int64
RequestedReviewers [ ] * user_model . User ` xorm:"-" `
HeadRepoID int64 ` xorm:"INDEX" `
HeadRepo * repo_model . Repository ` xorm:"-" `
BaseRepoID int64 ` xorm:"INDEX" `
BaseRepo * repo_model . Repository ` xorm:"-" `
HeadBranch string
HeadCommitID string ` xorm:"-" `
BaseBranch string
MergeBase string ` xorm:"VARCHAR(64)" `
AllowMaintainerEdit bool ` xorm:"NOT NULL DEFAULT false" `
HasMerged bool ` xorm:"INDEX" `
MergedCommitID string ` xorm:"VARCHAR(64)" `
MergerID int64 ` xorm:"INDEX" `
Merger * user_model . User ` xorm:"-" `
MergedUnix timeutil . TimeStamp ` xorm:"updated INDEX" `
isHeadRepoLoaded bool ` xorm:"-" `
Flow PullRequestFlow ` xorm:"NOT NULL DEFAULT 0" `
}
func init ( ) {
db . RegisterModel ( new ( PullRequest ) )
}
// DeletePullsByBaseRepoID deletes all pull requests by the base repository ID
func DeletePullsByBaseRepoID ( ctx context . Context , repoID int64 ) error {
deleteCond := builder . Select ( "id" ) . From ( "pull_request" ) . Where ( builder . Eq { "pull_request.base_repo_id" : repoID } )
// Delete scheduled auto merges
if _ , err := db . GetEngine ( ctx ) . In ( "pull_id" , deleteCond ) .
Delete ( & pull_model . AutoMerge { } ) ; err != nil {
return err
}
// Delete review states
if _ , err := db . GetEngine ( ctx ) . In ( "pull_id" , deleteCond ) .
Delete ( & pull_model . ReviewState { } ) ; err != nil {
return err
}
_ , err := db . DeleteByBean ( ctx , & PullRequest { BaseRepoID : repoID } )
return err
}
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
func ( pr * PullRequest ) String ( ) string {
if pr == nil {
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
return "<PullRequest nil>"
}
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
s := new ( strings . Builder )
fmt . Fprintf ( s , "<PullRequest [%d]" , pr . ID )
if pr . BaseRepo != nil {
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
fmt . Fprintf ( s , "%s#%d[%s..." , pr . BaseRepo . FullName ( ) , pr . Index , pr . BaseBranch )
} else {
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
fmt . Fprintf ( s , "Repo[%d]#%d[%s..." , pr . BaseRepoID , pr . Index , pr . BaseBranch )
}
if pr . HeadRepoID == pr . BaseRepoID {
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
fmt . Fprintf ( s , "%s]" , pr . HeadBranch )
} else if pr . HeadRepo != nil {
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
fmt . Fprintf ( s , "%s:%s]" , pr . HeadRepo . FullName ( ) , pr . HeadBranch )
} else {
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
fmt . Fprintf ( s , "Repo[%d]:%s]" , pr . HeadRepoID , pr . HeadBranch )
}
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
s . WriteByte ( '>' )
return s . String ( )
}
// MustHeadUserName returns the HeadRepo's username if failed return blank
func ( pr * PullRequest ) MustHeadUserName ( ctx context . Context ) string {
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
if ! repo_model . IsErrRepoNotExist ( err ) {
log . Error ( "LoadHeadRepo: %v" , err )
} else {
log . Warn ( "LoadHeadRepo %d but repository does not exist: %v" , pr . HeadRepoID , err )
}
return ""
}
if pr . HeadRepo == nil {
return ""
}
return pr . HeadRepo . OwnerName
}
// LoadAttributes loads pull request attributes from database
// Note: don't try to get Issue because will end up recursive querying.
func ( pr * PullRequest ) LoadAttributes ( ctx context . Context ) ( err error ) {
if pr . HasMerged && pr . Merger == nil {
pr . Merger , err = user_model . GetUserByID ( ctx , pr . MergerID )
if user_model . IsErrUserNotExist ( err ) {
pr . MergerID = user_model . GhostUserID
pr . Merger = user_model . NewGhostUser ( )
} else if err != nil {
return fmt . Errorf ( "getUserByID [%d]: %w" , pr . MergerID , err )
}
}
return nil
}
// LoadHeadRepo loads the head repository, pr.HeadRepo will remain nil if it does not exist
// and thus ErrRepoNotExist will never be returned
func ( pr * PullRequest ) LoadHeadRepo ( ctx context . Context ) ( err error ) {
if ! pr . isHeadRepoLoaded && pr . HeadRepo == nil && pr . HeadRepoID > 0 {
if pr . HeadRepoID == pr . BaseRepoID {
if pr . BaseRepo != nil {
pr . HeadRepo = pr . BaseRepo
return nil
} else if pr . Issue != nil && pr . Issue . Repo != nil {
pr . HeadRepo = pr . Issue . Repo
return nil
}
}
pr . HeadRepo , err = repo_model . GetRepositoryByID ( ctx , pr . HeadRepoID )
if err != nil && ! repo_model . IsErrRepoNotExist ( err ) { // Head repo maybe deleted, but it should still work
return fmt . Errorf ( "pr[%d].LoadHeadRepo[%d]: %w" , pr . ID , pr . HeadRepoID , err )
}
pr . isHeadRepoLoaded = true
}
return nil
}
// LoadRequestedReviewers loads the requested reviewers.
func ( pr * PullRequest ) LoadRequestedReviewers ( ctx context . Context ) error {
if len ( pr . RequestedReviewers ) > 0 {
return nil
}
reviews , err := GetReviewsByIssueID ( ctx , pr . Issue . ID )
if err != nil {
return err
}
if err = reviews . LoadReviewers ( ctx ) ; err != nil {
return err
}
for _ , review := range reviews {
pr . RequestedReviewers = append ( pr . RequestedReviewers , review . Reviewer )
}
return nil
}
// LoadBaseRepo loads the target repository. ErrRepoNotExist may be returned.
func ( pr * PullRequest ) LoadBaseRepo ( ctx context . Context ) ( err error ) {
if pr . BaseRepo != nil {
return nil
}
if pr . HeadRepoID == pr . BaseRepoID && pr . HeadRepo != nil {
pr . BaseRepo = pr . HeadRepo
return nil
}
if pr . Issue != nil && pr . Issue . Repo != nil {
pr . BaseRepo = pr . Issue . Repo
return nil
}
pr . BaseRepo , err = repo_model . GetRepositoryByID ( ctx , pr . BaseRepoID )
if err != nil {
return fmt . Errorf ( "pr[%d].LoadBaseRepo[%d]: %w" , pr . ID , pr . BaseRepoID , err )
}
return nil
}
// LoadIssue loads issue information from database
func ( pr * PullRequest ) LoadIssue ( ctx context . Context ) ( err error ) {
if pr . Issue != nil {
return nil
}
pr . Issue , err = GetIssueByID ( ctx , pr . IssueID )
if err == nil {
pr . Issue . PullRequest = pr
}
return err
}
// ReviewCount represents a count of Reviews
type ReviewCount struct {
IssueID int64
Type ReviewType
Count int64
}
// GetApprovalCounts returns the approval counts by type
// FIXME: Only returns official counts due to double counting of non-official counts
func ( pr * PullRequest ) GetApprovalCounts ( ctx context . Context ) ( [ ] * ReviewCount , error ) {
rCounts := make ( [ ] * ReviewCount , 0 , 6 )
sess := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID )
return rCounts , sess . Select ( "issue_id, type, count(id) as `count`" ) . Where ( "official = ? AND dismissed = ?" , true , false ) . GroupBy ( "issue_id, type" ) . Table ( "review" ) . Find ( & rCounts )
}
// GetApprovers returns the approvers of the pull request
func ( pr * PullRequest ) GetApprovers ( ctx context . Context ) string {
stringBuilder := strings . Builder { }
if err := pr . getReviewedByLines ( ctx , & stringBuilder ) ; err != nil {
log . Error ( "Unable to getReviewedByLines: Error: %v" , err )
return ""
}
return stringBuilder . String ( )
}
func ( pr * PullRequest ) getReviewedByLines ( ctx context . Context , writer io . Writer ) error {
maxReviewers := setting . Repository . PullRequest . DefaultMergeMessageMaxApprovers
if maxReviewers == 0 {
return nil
}
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
// Note: This doesn't page as we only expect a very limited number of reviews
reviews , err := FindLatestReviews ( ctx , FindReviewOptions {
Type : ReviewTypeApprove ,
IssueID : pr . IssueID ,
OfficialOnly : setting . Repository . PullRequest . DefaultMergeMessageOfficialApproversOnly ,
} )
if err != nil {
log . Error ( "Unable to FindReviews for PR ID %d: %v" , pr . ID , err )
return err
}
reviewersWritten := 0
for _ , review := range reviews {
if maxReviewers > 0 && reviewersWritten > maxReviewers {
break
}
if err := review . LoadReviewer ( ctx ) ; err != nil && ! user_model . IsErrUserNotExist ( err ) {
log . Error ( "Unable to LoadReviewer[%d] for PR ID %d : %v" , review . ReviewerID , pr . ID , err )
return err
} else if review . Reviewer == nil {
continue
}
if _ , err := writer . Write ( [ ] byte ( "Reviewed-by: " ) ) ; err != nil {
return err
}
if _ , err := writer . Write ( [ ] byte ( review . Reviewer . NewGitSig ( ) . String ( ) ) ) ; err != nil {
return err
}
if _ , err := writer . Write ( [ ] byte { '\n' } ) ; err != nil {
return err
}
reviewersWritten ++
}
return committer . Commit ( )
}
// GetGitRefName returns git ref for hidden pull request branch
func ( pr * PullRequest ) GetGitRefName ( ) string {
return fmt . Sprintf ( "%s%d/head" , git . PullPrefix , pr . Index )
}
func ( pr * PullRequest ) GetGitHeadBranchRefName ( ) string {
return fmt . Sprintf ( "%s%s" , git . BranchPrefix , pr . HeadBranch )
}
// IsChecking returns true if this pull request is still checking conflict.
func ( pr * PullRequest ) IsChecking ( ) bool {
return pr . Status == PullRequestStatusChecking
}
// CanAutoMerge returns true if this pull request can be merged automatically.
func ( pr * PullRequest ) CanAutoMerge ( ) bool {
return pr . Status == PullRequestStatusMergeable
}
// IsEmpty returns true if this pull request is empty.
func ( pr * PullRequest ) IsEmpty ( ) bool {
return pr . Status == PullRequestStatusEmpty
}
// IsAncestor returns true if the Head Commit of this PR is an ancestor of the Base Commit
func ( pr * PullRequest ) IsAncestor ( ) bool {
return pr . Status == PullRequestStatusAncestor
}
Implement actions (#21937)
Close #13539.
Co-authored by: @lunny @appleboy @fuxiaohei and others.
Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner
### Summary
The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:
- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.
**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.
### ⚠️ Breaking
`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.
### Some important reviews
- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868
### What it looks like
<details>
#### Manage runners
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">
#### List runs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">
#### View logs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">
</details>
### How to try it
<details>
#### 1. Start Gitea
Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).
Add additional configurations in `app.ini` to enable Actions:
```ini
[actions]
ENABLED = true
```
Start it.
If all is well, you'll see the management page of runners:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">
#### 2. Start runner
Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.
If all is well, you'll see a new runner has been added:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">
#### 3. Enable actions for a repo
Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">
If all is well, you'll see a new tab "Actions":
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">
#### 4. Upload workflow files
Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
If all is well, you'll see a new run in `Actions` tab:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">
#### 5. Check the logs of jobs
Click a run and you'll see the logs:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">
#### 6. Go on
You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.
Come on, PRs are welcome.
</details>
See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)
---------
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2 years ago
// IsFromFork return true if this PR is from a fork.
func ( pr * PullRequest ) IsFromFork ( ) bool {
return pr . HeadRepoID != pr . BaseRepoID
}
// SetMerged sets a pull request to merged and closes the corresponding issue
func ( pr * PullRequest ) SetMerged ( ctx context . Context ) ( bool , error ) {
if pr . HasMerged {
return false , fmt . Errorf ( "PullRequest[%d] already merged" , pr . Index )
}
if pr . MergedCommitID == "" || pr . MergedUnix == 0 || pr . Merger == nil {
return false , fmt . Errorf ( "Unable to merge PullRequest[%d], some required fields are empty" , pr . Index )
}
pr . HasMerged = true
sess := db . GetEngine ( ctx )
if _ , err := sess . Exec ( "UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?" , pr . IssueID ) ; err != nil {
return false , err
}
if _ , err := sess . Exec ( "UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?" , pr . ID ) ; err != nil {
return false , err
}
pr . Issue = nil
if err := pr . LoadIssue ( ctx ) ; err != nil {
return false , err
}
if tmpPr , err := GetPullRequestByID ( ctx , pr . ID ) ; err != nil {
return false , err
} else if tmpPr . HasMerged {
if pr . Issue . IsClosed {
return false , nil
}
return false , fmt . Errorf ( "PullRequest[%d] already merged but it's associated issue [%d] is not closed" , pr . Index , pr . IssueID )
} else if pr . Issue . IsClosed {
return false , fmt . Errorf ( "PullRequest[%d] already closed" , pr . Index )
}
if err := pr . Issue . LoadRepo ( ctx ) ; err != nil {
return false , err
}
if err := pr . Issue . Repo . LoadOwner ( ctx ) ; err != nil {
return false , err
}
if _ , err := changeIssueStatus ( ctx , pr . Issue , pr . Merger , true , true ) ; err != nil {
return false , fmt . Errorf ( "Issue.changeStatus: %w" , err )
}
// reset the conflicted files as there cannot be any if we're merged
pr . ConflictedFiles = [ ] string { }
// We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging.
if _ , err := sess . Where ( "id = ?" , pr . ID ) . Cols ( "has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files" ) . Update ( pr ) ; err != nil {
return false , fmt . Errorf ( "Failed to update pr[%d]: %w" , pr . ID , err )
}
return true , nil
}
// NewPullRequest creates new pull request with labels for repository.
func NewPullRequest ( ctx context . Context , repo * repo_model . Repository , issue * Issue , labelIDs [ ] int64 , uuids [ ] string , pr * PullRequest ) ( err error ) {
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
idx , err := db . GetNextResourceIndex ( ctx , "issue_index" , repo . ID )
if err != nil {
return fmt . Errorf ( "generate pull request index failed: %w" , err )
}
issue . Index = idx
if err = NewIssueWithIndex ( ctx , issue . Poster , NewIssueOptions {
Repo : repo ,
Issue : issue ,
LabelIDs : labelIDs ,
Attachments : uuids ,
IsPull : true ,
} ) ; err != nil {
if repo_model . IsErrUserDoesNotHaveAccessToRepo ( err ) || IsErrNewIssueInsert ( err ) {
return err
}
return fmt . Errorf ( "newIssue: %w" , err )
}
pr . Index = issue . Index
pr . BaseRepo = repo
pr . IssueID = issue . ID
if err = db . Insert ( ctx , pr ) ; err != nil {
return fmt . Errorf ( "insert pull repo: %w" , err )
}
if err = committer . Commit ( ) ; err != nil {
return fmt . Errorf ( "Commit: %w" , err )
}
return nil
}
// GetUnmergedPullRequest returns a pull request that is open and has not been merged
// by given head/base and repo/branch.
func GetUnmergedPullRequest ( ctx context . Context , headRepoID , baseRepoID int64 , headBranch , baseBranch string , flow PullRequestFlow ) ( * PullRequest , error ) {
pr := new ( PullRequest )
has , err := db . GetEngine ( ctx ) .
Where ( "head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?" ,
headRepoID , headBranch , baseRepoID , baseBranch , false , flow , false ) .
Join ( "INNER" , "issue" , "issue.id=pull_request.issue_id" ) .
Get ( pr )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrPullRequestNotExist { 0 , 0 , headRepoID , baseRepoID , headBranch , baseBranch }
}
return pr , nil
}
// GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status)
// by given head information (repo and branch).
func GetLatestPullRequestByHeadInfo ( ctx context . Context , repoID int64 , branch string ) ( * PullRequest , error ) {
pr := new ( PullRequest )
has , err := db . GetEngine ( ctx ) .
Where ( "head_repo_id = ? AND head_branch = ? AND flow = ?" , repoID , branch , PullRequestFlowGithub ) .
OrderBy ( "id DESC" ) .
Get ( pr )
if ! has {
return nil , err
}
return pr , err
}
// GetPullRequestByIndex returns a pull request by the given index
func GetPullRequestByIndex ( ctx context . Context , repoID , index int64 ) ( * PullRequest , error ) {
if index < 1 {
return nil , ErrPullRequestNotExist { }
}
pr := & PullRequest {
BaseRepoID : repoID ,
Index : index ,
}
has , err := db . GetEngine ( ctx ) . Get ( pr )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrPullRequestNotExist { 0 , 0 , 0 , repoID , "" , "" }
}
if err = pr . LoadAttributes ( ctx ) ; err != nil {
return nil , err
}
if err = pr . LoadIssue ( ctx ) ; err != nil {
return nil , err
}
return pr , nil
}
// GetPullRequestByID returns a pull request by given ID.
func GetPullRequestByID ( ctx context . Context , id int64 ) ( * PullRequest , error ) {
pr := new ( PullRequest )
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( pr )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrPullRequestNotExist { id , 0 , 0 , 0 , "" , "" }
}
return pr , pr . LoadAttributes ( ctx )
}
// GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID.
func GetPullRequestByIssueIDWithNoAttributes ( ctx context . Context , issueID int64 ) ( * PullRequest , error ) {
var pr PullRequest
has , err := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , issueID ) . Get ( & pr )
if err != nil {
return nil , err
}
if ! has {
return nil , ErrPullRequestNotExist { 0 , issueID , 0 , 0 , "" , "" }
}
return & pr , nil
}
// GetPullRequestByIssueID returns pull request by given issue ID.
func GetPullRequestByIssueID ( ctx context . Context , issueID int64 ) ( * PullRequest , error ) {
pr , exist , err := db . Get [ PullRequest ] ( ctx , builder . Eq { "issue_id" : issueID } )
if err != nil {
return nil , err
} else if ! exist {
return nil , ErrPullRequestNotExist { 0 , issueID , 0 , 0 , "" , "" }
}
return pr , pr . LoadAttributes ( ctx )
}
// GetPullRequestsByBaseHeadInfo returns the pull request by given base and head
func GetPullRequestByBaseHeadInfo ( ctx context . Context , baseID , headID int64 , base , head string ) ( * PullRequest , error ) {
pr := & PullRequest { }
sess := db . GetEngine ( ctx ) .
Join ( "INNER" , "issue" , "issue.id = pull_request.issue_id" ) .
Where ( "base_repo_id = ? AND base_branch = ? AND head_repo_id = ? AND head_branch = ?" , baseID , base , headID , head )
has , err := sess . Get ( pr )
if err != nil {
return nil , err
}
if ! has {
return nil , ErrPullRequestNotExist {
HeadRepoID : headID ,
BaseRepoID : baseID ,
HeadBranch : head ,
BaseBranch : base ,
}
}
if err = pr . LoadAttributes ( ctx ) ; err != nil {
return nil , err
}
if err = pr . LoadIssue ( ctx ) ; err != nil {
return nil , err
}
return pr , nil
}
// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
// By poster id.
func GetAllUnmergedAgitPullRequestByPoster ( ctx context . Context , uid int64 ) ( [ ] * PullRequest , error ) {
pulls := make ( [ ] * PullRequest , 0 , 10 )
err := db . GetEngine ( ctx ) .
Where ( "has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?" ,
false , PullRequestFlowAGit , false , uid ) .
Join ( "INNER" , "issue" , "issue.id=pull_request.issue_id" ) .
Find ( & pulls )
return pulls , err
}
// Update updates all fields of pull request.
func ( pr * PullRequest ) Update ( ctx context . Context ) error {
_ , err := db . GetEngine ( ctx ) . ID ( pr . ID ) . AllCols ( ) . Update ( pr )
return err
}
// UpdateCols updates specific fields of pull request.
func ( pr * PullRequest ) UpdateCols ( ctx context . Context , cols ... string ) error {
_ , err := db . GetEngine ( ctx ) . ID ( pr . ID ) . Cols ( cols ... ) . Update ( pr )
return err
}
// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged
func ( pr * PullRequest ) UpdateColsIfNotMerged ( ctx context . Context , cols ... string ) error {
_ , err := db . GetEngine ( ctx ) . Where ( "id = ? AND has_merged = ?" , pr . ID , false ) . Cols ( cols ... ) . Update ( pr )
return err
}
// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
// Issue must be set before this method can be called.
func ( pr * PullRequest ) IsWorkInProgress ( ctx context . Context ) bool {
if err := pr . LoadIssue ( ctx ) ; err != nil {
log . Error ( "LoadIssue: %v" , err )
return false
}
return HasWorkInProgressPrefix ( pr . Issue . Title )
}
// HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix
func HasWorkInProgressPrefix ( title string ) bool {
for _ , prefix := range setting . Repository . PullRequest . WorkInProgressPrefixes {
Make WIP prefixes case insensitive, e.g. allow `Draft` as a WIP prefix (#19780)
The issue was that only the actual title was converted to uppercase, but
not the prefix as specified in `WORK_IN_PROGRESS_PREFIXES`. As a result,
the following did not work:
WORK_IN_PROGRESS_PREFIXES=Draft:,[Draft],WIP:,[WIP]
One possible workaround was:
WORK_IN_PROGRESS_PREFIXES=DRAFT:,[DRAFT],WIP:,[WIP]
Then indeed one could use `Draft` (as well as `DRAFT`) in the title.
However, the link `Start the title with DRAFT: to prevent the pull request
from being merged accidentally.` showed the suggestion in uppercase; so
it is not possible to show it as `Draft`. This PR fixes it, and allows
to use `Draft` in `WORK_IN_PROGRESS_PREFIXES`.
Fixes #19779.
Co-authored-by: zeripath <art27@cantab.net>
3 years ago
if strings . HasPrefix ( strings . ToUpper ( title ) , strings . ToUpper ( prefix ) ) {
return true
}
}
return false
}
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
func ( pr * PullRequest ) IsFilesConflicted ( ) bool {
return len ( pr . ConflictedFiles ) > 0
}
// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
// It returns an empty string when none were found
func ( pr * PullRequest ) GetWorkInProgressPrefix ( ctx context . Context ) string {
if err := pr . LoadIssue ( ctx ) ; err != nil {
log . Error ( "LoadIssue: %v" , err )
return ""
}
for _ , prefix := range setting . Repository . PullRequest . WorkInProgressPrefixes {
Make WIP prefixes case insensitive, e.g. allow `Draft` as a WIP prefix (#19780)
The issue was that only the actual title was converted to uppercase, but
not the prefix as specified in `WORK_IN_PROGRESS_PREFIXES`. As a result,
the following did not work:
WORK_IN_PROGRESS_PREFIXES=Draft:,[Draft],WIP:,[WIP]
One possible workaround was:
WORK_IN_PROGRESS_PREFIXES=DRAFT:,[DRAFT],WIP:,[WIP]
Then indeed one could use `Draft` (as well as `DRAFT`) in the title.
However, the link `Start the title with DRAFT: to prevent the pull request
from being merged accidentally.` showed the suggestion in uppercase; so
it is not possible to show it as `Draft`. This PR fixes it, and allows
to use `Draft` in `WORK_IN_PROGRESS_PREFIXES`.
Fixes #19779.
Co-authored-by: zeripath <art27@cantab.net>
3 years ago
if strings . HasPrefix ( strings . ToUpper ( pr . Issue . Title ) , strings . ToUpper ( prefix ) ) {
return pr . Issue . Title [ 0 : len ( prefix ) ]
}
}
return ""
}
// UpdateCommitDivergence update Divergence of a pull request
func ( pr * PullRequest ) UpdateCommitDivergence ( ctx context . Context , ahead , behind int ) error {
if pr . ID == 0 {
return fmt . Errorf ( "pull ID is 0" )
}
pr . CommitsAhead = ahead
pr . CommitsBehind = behind
_ , err := db . GetEngine ( ctx ) . ID ( pr . ID ) . Cols ( "commits_ahead" , "commits_behind" ) . Update ( pr )
return err
}
// IsSameRepo returns true if base repo and head repo is the same
func ( pr * PullRequest ) IsSameRepo ( ) bool {
return pr . BaseRepoID == pr . HeadRepoID
}
// GetBaseBranchLink returns the relative URL of the base branch
func ( pr * PullRequest ) GetBaseBranchLink ( ctx context . Context ) string {
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
log . Error ( "LoadBaseRepo: %v" , err )
return ""
}
if pr . BaseRepo == nil {
return ""
}
return pr . BaseRepo . Link ( ) + "/src/branch/" + util . PathEscapeSegments ( pr . BaseBranch )
}
// GetHeadBranchLink returns the relative URL of the head branch
func ( pr * PullRequest ) GetHeadBranchLink ( ctx context . Context ) string {
if pr . Flow == PullRequestFlowAGit {
return ""
}
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
log . Error ( "LoadHeadRepo: %v" , err )
return ""
}
if pr . HeadRepo == nil {
return ""
}
return pr . HeadRepo . Link ( ) + "/src/branch/" + util . PathEscapeSegments ( pr . HeadBranch )
}
// UpdateAllowEdits update if PR can be edited from maintainers
func UpdateAllowEdits ( ctx context . Context , pr * PullRequest ) error {
if _ , err := db . GetEngine ( ctx ) . ID ( pr . ID ) . Cols ( "allow_maintainer_edit" ) . Update ( pr ) ; err != nil {
return err
}
return nil
}
// Mergeable returns if the pullrequest is mergeable.
func ( pr * PullRequest ) Mergeable ( ctx context . Context ) bool {
// If a pull request isn't mergable if it's:
// - Being conflict checked.
// - Has a conflict.
// - Received a error while being conflict checked.
// - Is a work-in-progress pull request.
return pr . Status != PullRequestStatusChecking && pr . Status != PullRequestStatusConflict &&
pr . Status != PullRequestStatusError && ! pr . IsWorkInProgress ( ctx )
}
// HasEnoughApprovals returns true if pr has enough granted approvals.
func HasEnoughApprovals ( ctx context . Context , protectBranch * git_model . ProtectedBranch , pr * PullRequest ) bool {
if protectBranch . RequiredApprovals == 0 {
return true
}
return GetGrantedApprovalsCount ( ctx , protectBranch , pr ) >= protectBranch . RequiredApprovals
}
// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
func GetGrantedApprovalsCount ( ctx context . Context , protectBranch * git_model . ProtectedBranch , pr * PullRequest ) int64 {
sess := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID ) .
And ( "type = ?" , ReviewTypeApprove ) .
And ( "official = ?" , true ) .
And ( "dismissed = ?" , false )
if protectBranch . IgnoreStaleApprovals {
sess = sess . And ( "stale = ?" , false )
}
approvals , err := sess . Count ( new ( Review ) )
if err != nil {
log . Error ( "GetGrantedApprovalsCount: %v" , err )
return 0
}
return approvals
}
// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
func MergeBlockedByRejectedReview ( ctx context . Context , protectBranch * git_model . ProtectedBranch , pr * PullRequest ) bool {
if ! protectBranch . BlockOnRejectedReviews {
return false
}
rejectExist , err := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID ) .
And ( "type = ?" , ReviewTypeReject ) .
And ( "official = ?" , true ) .
And ( "dismissed = ?" , false ) .
Exist ( new ( Review ) )
if err != nil {
log . Error ( "MergeBlockedByRejectedReview: %v" , err )
return true
}
return rejectExist
}
// MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer
// of from official review
func MergeBlockedByOfficialReviewRequests ( ctx context . Context , protectBranch * git_model . ProtectedBranch , pr * PullRequest ) bool {
if ! protectBranch . BlockOnOfficialReviewRequests {
return false
}
has , err := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID ) .
And ( "type = ?" , ReviewTypeRequest ) .
And ( "official = ?" , true ) .
Exist ( new ( Review ) )
if err != nil {
log . Error ( "MergeBlockedByOfficialReviewRequests: %v" , err )
return true
}
return has
}
// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch
func MergeBlockedByOutdatedBranch ( protectBranch * git_model . ProtectedBranch , pr * PullRequest ) bool {
return protectBranch . BlockOnOutdatedBranch && pr . CommitsBehind > 0
}
func PullRequestCodeOwnersReview ( ctx context . Context , pull * Issue , pr * PullRequest ) error {
files := [ ] string { "CODEOWNERS" , "docs/CODEOWNERS" , ".gitea/CODEOWNERS" }
if pr . IsWorkInProgress ( ctx ) {
return nil
}
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
return err
}
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
10 months ago
repo , err := gitrepo . OpenRepository ( ctx , pr . BaseRepo )
if err != nil {
return err
}
defer repo . Close ( )
branch , err := repo . GetDefaultBranch ( )
if err != nil {
return err
}
commit , err := repo . GetBranchCommit ( branch )
if err != nil {
return err
}
var data string
for _ , file := range files {
if blob , err := commit . GetBlobByPath ( file ) ; err == nil {
data , err = blob . GetBlobContent ( setting . UI . MaxDisplayFileSize )
if err == nil {
break
}
}
}
rules , _ := GetCodeOwnersFromContent ( ctx , data )
changedFiles , err := repo . GetFilesChangedBetween ( git . BranchPrefix + pr . BaseBranch , pr . GetGitRefName ( ) )
if err != nil {
return err
}
uniqUsers := make ( map [ int64 ] * user_model . User )
uniqTeams := make ( map [ string ] * org_model . Team )
for _ , rule := range rules {
for _ , f := range changedFiles {
if ( rule . Rule . MatchString ( f ) && ! rule . Negative ) || ( ! rule . Rule . MatchString ( f ) && rule . Negative ) {
for _ , u := range rule . Users {
uniqUsers [ u . ID ] = u
}
for _ , t := range rule . Teams {
uniqTeams [ fmt . Sprintf ( "%d/%d" , t . OrgID , t . ID ) ] = t
}
}
}
}
for _ , u := range uniqUsers {
if u . ID != pull . Poster . ID {
if _ , err := AddReviewRequest ( ctx , pull , u , pull . Poster ) ; err != nil {
log . Warn ( "Failed add assignee user: %s to PR review: %s#%d, error: %s" , u . Name , pr . BaseRepo . Name , pr . ID , err )
return err
}
}
}
for _ , t := range uniqTeams {
if _ , err := AddTeamReviewRequest ( ctx , pull , t , pull . Poster ) ; err != nil {
log . Warn ( "Failed add assignee team: %s to PR review: %s#%d, error: %s" , t . Name , pr . BaseRepo . Name , pr . ID , err )
return err
}
}
return nil
}
// GetCodeOwnersFromContent returns the code owners configuration
// Return empty slice if files missing
// Return warning messages on parsing errors
// We're trying to do the best we can when parsing a file.
// Invalid lines are skipped. Non-existent users and teams too.
func GetCodeOwnersFromContent ( ctx context . Context , data string ) ( [ ] * CodeOwnerRule , [ ] string ) {
if len ( data ) == 0 {
return nil , nil
}
rules := make ( [ ] * CodeOwnerRule , 0 )
lines := strings . Split ( data , "\n" )
warnings := make ( [ ] string , 0 )
for i , line := range lines {
tokens := TokenizeCodeOwnersLine ( line )
if len ( tokens ) == 0 {
continue
} else if len ( tokens ) < 2 {
warnings = append ( warnings , fmt . Sprintf ( "Line: %d: incorrect format" , i + 1 ) )
continue
}
rule , wr := ParseCodeOwnersLine ( ctx , tokens )
for _ , w := range wr {
warnings = append ( warnings , fmt . Sprintf ( "Line: %d: %s" , i + 1 , w ) )
}
if rule == nil {
continue
}
rules = append ( rules , rule )
}
return rules , warnings
}
type CodeOwnerRule struct {
Rule * regexp . Regexp
Negative bool
Users [ ] * user_model . User
Teams [ ] * org_model . Team
}
func ParseCodeOwnersLine ( ctx context . Context , tokens [ ] string ) ( * CodeOwnerRule , [ ] string ) {
var err error
rule := & CodeOwnerRule {
Users : make ( [ ] * user_model . User , 0 ) ,
Teams : make ( [ ] * org_model . Team , 0 ) ,
Negative : strings . HasPrefix ( tokens [ 0 ] , "!" ) ,
}
warnings := make ( [ ] string , 0 )
rule . Rule , err = regexp . Compile ( fmt . Sprintf ( "^%s$" , strings . TrimPrefix ( tokens [ 0 ] , "!" ) ) )
if err != nil {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner regexp: %s" , err ) )
return nil , warnings
}
for _ , user := range tokens [ 1 : ] {
user = strings . TrimPrefix ( user , "@" )
// Only @org/team can contain slashes
if strings . Contains ( user , "/" ) {
s := strings . Split ( user , "/" )
if len ( s ) != 2 {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner group: %s" , user ) )
continue
}
orgName := s [ 0 ]
teamName := s [ 1 ]
org , err := org_model . GetOrgByName ( ctx , orgName )
if err != nil {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner organization: %s" , user ) )
continue
}
teams , err := org . LoadTeams ( ctx )
if err != nil {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner team: %s" , user ) )
continue
}
for _ , team := range teams {
if team . Name == teamName {
rule . Teams = append ( rule . Teams , team )
}
}
} else {
u , err := user_model . GetUserByName ( ctx , user )
if err != nil {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner user: %s" , user ) )
continue
}
rule . Users = append ( rule . Users , u )
}
}
if ( len ( rule . Users ) == 0 ) && ( len ( rule . Teams ) == 0 ) {
warnings = append ( warnings , "no users/groups matched" )
return nil , warnings
}
return rule , warnings
}
func TokenizeCodeOwnersLine ( line string ) [ ] string {
if len ( line ) == 0 {
return nil
}
line = strings . TrimSpace ( line )
line = strings . ReplaceAll ( line , "\t" , " " )
tokens := make ( [ ] string , 0 )
escape := false
token := ""
for _ , char := range line {
if escape {
token += string ( char )
escape = false
} else if string ( char ) == "\\" {
escape = true
} else if string ( char ) == "#" {
break
} else if string ( char ) == " " {
if len ( token ) > 0 {
tokens = append ( tokens , token )
token = ""
}
} else {
token += string ( char )
}
}
if len ( token ) > 0 {
tokens = append ( tokens , token )
}
return tokens
}
// InsertPullRequests inserted pull requests
func InsertPullRequests ( ctx context . Context , prs ... * PullRequest ) error {
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
sess := db . GetEngine ( ctx )
for _ , pr := range prs {
if err := insertIssue ( ctx , pr . Issue ) ; err != nil {
return err
}
pr . IssueID = pr . Issue . ID
if _ , err := sess . NoAutoTime ( ) . Insert ( pr ) ; err != nil {
return err
}
}
return committer . Commit ( )
}
// GetPullRequestByMergedCommit returns a merged pull request by the given commit
func GetPullRequestByMergedCommit ( ctx context . Context , repoID int64 , sha string ) ( * PullRequest , error ) {
pr := new ( PullRequest )
has , err := db . GetEngine ( ctx ) . Where ( "base_repo_id = ? AND merged_commit_id = ?" , repoID , sha ) . Get ( pr )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrPullRequestNotExist { 0 , 0 , 0 , repoID , "" , "" }
}
if err = pr . LoadAttributes ( ctx ) ; err != nil {
return nil , err
}
if err = pr . LoadIssue ( ctx ) ; err != nil {
return nil , err
}
return pr , nil
}