Merge branch 'master' of github.com:go-gitea/gitea
This commit is contained in:
+42
-14
@@ -71,9 +71,17 @@ type Access struct {
|
||||
Mode AccessMode
|
||||
}
|
||||
|
||||
func accessLevel(e Engine, userID int64, repo *Repository) (AccessMode, error) {
|
||||
func accessLevel(e Engine, user *User, repo *Repository) (AccessMode, error) {
|
||||
mode := AccessModeNone
|
||||
if !repo.IsPrivate {
|
||||
var userID int64
|
||||
restricted := false
|
||||
|
||||
if user != nil {
|
||||
userID = user.ID
|
||||
restricted = user.IsRestricted
|
||||
}
|
||||
|
||||
if !restricted && !repo.IsPrivate {
|
||||
mode = AccessModeRead
|
||||
}
|
||||
|
||||
@@ -162,42 +170,62 @@ func maxAccessMode(modes ...AccessMode) AccessMode {
|
||||
return max
|
||||
}
|
||||
|
||||
type userAccess struct {
|
||||
User *User
|
||||
Mode AccessMode
|
||||
}
|
||||
|
||||
// updateUserAccess updates an access map so that user has at least mode
|
||||
func updateUserAccess(accessMap map[int64]*userAccess, user *User, mode AccessMode) {
|
||||
if ua, ok := accessMap[user.ID]; ok {
|
||||
ua.Mode = maxAccessMode(ua.Mode, mode)
|
||||
} else {
|
||||
accessMap[user.ID] = &userAccess{User: user, Mode: mode}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
|
||||
func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode) (err error) {
|
||||
func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]*userAccess) (err error) {
|
||||
minMode := AccessModeRead
|
||||
if !repo.IsPrivate {
|
||||
minMode = AccessModeWrite
|
||||
}
|
||||
|
||||
newAccesses := make([]Access, 0, len(accessMap))
|
||||
for userID, mode := range accessMap {
|
||||
if mode < minMode {
|
||||
for userID, ua := range accessMap {
|
||||
if ua.Mode < minMode && !ua.User.IsRestricted {
|
||||
continue
|
||||
}
|
||||
|
||||
newAccesses = append(newAccesses, Access{
|
||||
UserID: userID,
|
||||
RepoID: repo.ID,
|
||||
Mode: mode,
|
||||
Mode: ua.Mode,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete old accesses and insert new ones for repository.
|
||||
if _, err = e.Delete(&Access{RepoID: repo.ID}); err != nil {
|
||||
return fmt.Errorf("delete old accesses: %v", err)
|
||||
} else if _, err = e.Insert(newAccesses); err != nil {
|
||||
}
|
||||
if len(newAccesses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = e.Insert(newAccesses); err != nil {
|
||||
return fmt.Errorf("insert new accesses: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
|
||||
func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error {
|
||||
collaborations, err := repo.getCollaborations(e)
|
||||
func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]*userAccess) error {
|
||||
collaborators, err := repo.getCollaborators(e, ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("getCollaborations: %v", err)
|
||||
}
|
||||
for _, c := range collaborations {
|
||||
accessMap[c.UserID] = c.Mode
|
||||
for _, c := range collaborators {
|
||||
updateUserAccess(accessMap, c.User, c.Collaboration.Mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -206,7 +234,7 @@ func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int6
|
||||
// except the team whose ID is given. It is used to assign a team ID when
|
||||
// remove repository from that team.
|
||||
func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) {
|
||||
accessMap := make(map[int64]AccessMode, 20)
|
||||
accessMap := make(map[int64]*userAccess, 20)
|
||||
|
||||
if err = repo.getOwner(e); err != nil {
|
||||
return err
|
||||
@@ -239,7 +267,7 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err
|
||||
return fmt.Errorf("getMembers '%d': %v", t.ID, err)
|
||||
}
|
||||
for _, m := range t.Members {
|
||||
accessMap[m.ID] = maxAccessMode(accessMap[m.ID], t.Authorize)
|
||||
updateUserAccess(accessMap, m, t.Authorize)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +328,7 @@ func (repo *Repository) recalculateAccesses(e Engine) error {
|
||||
return repo.recalculateTeamAccesses(e, 0)
|
||||
}
|
||||
|
||||
accessMap := make(map[int64]AccessMode, 20)
|
||||
accessMap := make(map[int64]*userAccess, 20)
|
||||
if err := repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
|
||||
return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
|
||||
}
|
||||
|
||||
+51
-1
@@ -15,6 +15,7 @@ func TestAccessLevel(t *testing.T) {
|
||||
|
||||
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User)
|
||||
user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
|
||||
// A public repository owned by User 2
|
||||
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||
assert.False(t, repo1.IsPrivate)
|
||||
@@ -22,6 +23,12 @@ func TestAccessLevel(t *testing.T) {
|
||||
repo3 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
|
||||
assert.True(t, repo3.IsPrivate)
|
||||
|
||||
// Another public repository
|
||||
repo4 := AssertExistsAndLoadBean(t, &Repository{ID: 4}).(*Repository)
|
||||
assert.False(t, repo4.IsPrivate)
|
||||
// org. owned private repo
|
||||
repo24 := AssertExistsAndLoadBean(t, &Repository{ID: 24}).(*Repository)
|
||||
|
||||
level, err := AccessLevel(user2, repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeOwner, level)
|
||||
@@ -37,6 +44,21 @@ func TestAccessLevel(t *testing.T) {
|
||||
level, err = AccessLevel(user5, repo3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeNone, level)
|
||||
|
||||
// restricted user has no access to a public repo
|
||||
level, err = AccessLevel(user29, repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeNone, level)
|
||||
|
||||
// ... unless he's a collaborator
|
||||
level, err = AccessLevel(user29, repo4)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeWrite, level)
|
||||
|
||||
// ... or a team member
|
||||
level, err = AccessLevel(user29, repo24)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeRead, level)
|
||||
}
|
||||
|
||||
func TestHasAccess(t *testing.T) {
|
||||
@@ -72,6 +94,11 @@ func TestUser_GetRepositoryAccesses(t *testing.T) {
|
||||
accesses, err := user1.GetRepositoryAccesses()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, accesses, 0)
|
||||
|
||||
user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
|
||||
accesses, err = user29.GetRepositoryAccesses()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, accesses, 2)
|
||||
}
|
||||
|
||||
func TestUser_GetAccessibleRepositories(t *testing.T) {
|
||||
@@ -85,7 +112,12 @@ func TestUser_GetAccessibleRepositories(t *testing.T) {
|
||||
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repos, err = user2.GetAccessibleRepositories(0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, repos, 1)
|
||||
assert.Len(t, repos, 4)
|
||||
|
||||
user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
|
||||
repos, err = user29.GetAccessibleRepositories(0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, repos, 2)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses(t *testing.T) {
|
||||
@@ -119,3 +151,21 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses3(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
team5 := AssertExistsAndLoadBean(t, &Team{ID: 5}).(*Team)
|
||||
user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
|
||||
|
||||
has, err := x.Get(&Access{UserID: 29, RepoID: 23})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
|
||||
// adding user29 to team5 should add an explicit access row for repo 23
|
||||
// even though repo 23 is public
|
||||
assert.NoError(t, AddTeamMember(team5, user29.ID))
|
||||
|
||||
has, err = x.Get(&Access{UserID: 29, RepoID: 23})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
}
|
||||
|
||||
+50
-360
@@ -7,18 +7,14 @@ package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
@@ -30,26 +26,29 @@ type ActionType int
|
||||
|
||||
// Possible action types.
|
||||
const (
|
||||
ActionCreateRepo ActionType = iota + 1 // 1
|
||||
ActionRenameRepo // 2
|
||||
ActionStarRepo // 3
|
||||
ActionWatchRepo // 4
|
||||
ActionCommitRepo // 5
|
||||
ActionCreateIssue // 6
|
||||
ActionCreatePullRequest // 7
|
||||
ActionTransferRepo // 8
|
||||
ActionPushTag // 9
|
||||
ActionCommentIssue // 10
|
||||
ActionMergePullRequest // 11
|
||||
ActionCloseIssue // 12
|
||||
ActionReopenIssue // 13
|
||||
ActionClosePullRequest // 14
|
||||
ActionReopenPullRequest // 15
|
||||
ActionDeleteTag // 16
|
||||
ActionDeleteBranch // 17
|
||||
ActionMirrorSyncPush // 18
|
||||
ActionMirrorSyncCreate // 19
|
||||
ActionMirrorSyncDelete // 20
|
||||
ActionCreateRepo ActionType = iota + 1 // 1
|
||||
ActionRenameRepo // 2
|
||||
ActionStarRepo // 3
|
||||
ActionWatchRepo // 4
|
||||
ActionCommitRepo // 5
|
||||
ActionCreateIssue // 6
|
||||
ActionCreatePullRequest // 7
|
||||
ActionTransferRepo // 8
|
||||
ActionPushTag // 9
|
||||
ActionCommentIssue // 10
|
||||
ActionMergePullRequest // 11
|
||||
ActionCloseIssue // 12
|
||||
ActionReopenIssue // 13
|
||||
ActionClosePullRequest // 14
|
||||
ActionReopenPullRequest // 15
|
||||
ActionDeleteTag // 16
|
||||
ActionDeleteBranch // 17
|
||||
ActionMirrorSyncPush // 18
|
||||
ActionMirrorSyncCreate // 19
|
||||
ActionMirrorSyncDelete // 20
|
||||
ActionApprovePullRequest // 21
|
||||
ActionRejectPullRequest // 22
|
||||
ActionCommentPull // 23
|
||||
)
|
||||
|
||||
// Action represents user operation type and other information to
|
||||
@@ -121,10 +120,13 @@ func (a *Action) ShortActUserName() string {
|
||||
return base.EllipsisString(a.GetActUserName(), 20)
|
||||
}
|
||||
|
||||
// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME
|
||||
// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
|
||||
func (a *Action) GetDisplayName() string {
|
||||
if setting.UI.DefaultShowFullName {
|
||||
return a.GetActFullName()
|
||||
trimmedFullName := strings.TrimSpace(a.GetActFullName())
|
||||
if len(trimmedFullName) > 0 {
|
||||
return trimmedFullName
|
||||
}
|
||||
}
|
||||
return a.ShortActUserName()
|
||||
}
|
||||
@@ -146,7 +148,7 @@ func (a *Action) GetActAvatar() string {
|
||||
// GetRepoUserName returns the name of the action repository owner.
|
||||
func (a *Action) GetRepoUserName() string {
|
||||
a.loadRepo()
|
||||
return a.Repo.MustOwner().Name
|
||||
return a.Repo.OwnerName
|
||||
}
|
||||
|
||||
// ShortRepoUserName returns the name of the action repository owner
|
||||
@@ -211,7 +213,7 @@ func (a *Action) getCommentLink(e Engine) string {
|
||||
return "#"
|
||||
}
|
||||
if a.Comment == nil && a.CommentID != 0 {
|
||||
a.Comment, _ = GetCommentByID(a.CommentID)
|
||||
a.Comment, _ = getCommentByID(e, a.CommentID)
|
||||
}
|
||||
if a.Comment != nil {
|
||||
return a.Comment.HTMLURL()
|
||||
@@ -283,339 +285,13 @@ func (a *Action) GetIssueContent() string {
|
||||
return issue.Content
|
||||
}
|
||||
|
||||
func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
|
||||
if err = notifyWatchers(e, &Action{
|
||||
ActUserID: u.ID,
|
||||
ActUser: u,
|
||||
OpType: ActionCreateRepo,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("notify watchers '%d/%d': %v", u.ID, repo.ID, err)
|
||||
}
|
||||
|
||||
log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewRepoAction adds new action for creating repository.
|
||||
func NewRepoAction(u *User, repo *Repository) (err error) {
|
||||
return newRepoAction(x, u, repo)
|
||||
}
|
||||
|
||||
func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) {
|
||||
if err = notifyWatchers(e, &Action{
|
||||
ActUserID: actUser.ID,
|
||||
ActUser: actUser,
|
||||
OpType: ActionRenameRepo,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
Content: oldRepoName,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("notify watchers: %v", err)
|
||||
}
|
||||
|
||||
log.Trace("action.renameRepoAction: %s/%s", actUser.Name, repo.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenameRepoAction adds new action for renaming a repository.
|
||||
func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error {
|
||||
return renameRepoAction(x, actUser, oldRepoName, repo)
|
||||
}
|
||||
|
||||
// PushCommit represents a commit in a push operation.
|
||||
type PushCommit struct {
|
||||
Sha1 string
|
||||
Message string
|
||||
AuthorEmail string
|
||||
AuthorName string
|
||||
CommitterEmail string
|
||||
CommitterName string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// PushCommits represents list of commits in a push operation.
|
||||
type PushCommits struct {
|
||||
Len int
|
||||
Commits []*PushCommit
|
||||
CompareURL string
|
||||
|
||||
avatars map[string]string
|
||||
emailUsers map[string]*User
|
||||
}
|
||||
|
||||
// NewPushCommits creates a new PushCommits object.
|
||||
func NewPushCommits() *PushCommits {
|
||||
return &PushCommits{
|
||||
avatars: make(map[string]string),
|
||||
emailUsers: make(map[string]*User),
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPIPayloadCommits converts a PushCommits object to
|
||||
// api.PayloadCommit format.
|
||||
func (pc *PushCommits) ToAPIPayloadCommits(repoPath, repoLink string) ([]*api.PayloadCommit, error) {
|
||||
commits := make([]*api.PayloadCommit, len(pc.Commits))
|
||||
|
||||
if pc.emailUsers == nil {
|
||||
pc.emailUsers = make(map[string]*User)
|
||||
}
|
||||
var err error
|
||||
for i, commit := range pc.Commits {
|
||||
authorUsername := ""
|
||||
author, ok := pc.emailUsers[commit.AuthorEmail]
|
||||
if !ok {
|
||||
author, err = GetUserByEmail(commit.AuthorEmail)
|
||||
if err == nil {
|
||||
authorUsername = author.Name
|
||||
pc.emailUsers[commit.AuthorEmail] = author
|
||||
}
|
||||
} else {
|
||||
authorUsername = author.Name
|
||||
}
|
||||
|
||||
committerUsername := ""
|
||||
committer, ok := pc.emailUsers[commit.CommitterEmail]
|
||||
if !ok {
|
||||
committer, err = GetUserByEmail(commit.CommitterEmail)
|
||||
if err == nil {
|
||||
// TODO: check errors other than email not found.
|
||||
committerUsername = committer.Name
|
||||
pc.emailUsers[commit.CommitterEmail] = committer
|
||||
}
|
||||
} else {
|
||||
committerUsername = committer.Name
|
||||
}
|
||||
|
||||
fileStatus, err := git.GetCommitFileStatus(repoPath, commit.Sha1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FileStatus [commit_sha1: %s]: %v", commit.Sha1, err)
|
||||
}
|
||||
|
||||
commits[i] = &api.PayloadCommit{
|
||||
ID: commit.Sha1,
|
||||
Message: commit.Message,
|
||||
URL: fmt.Sprintf("%s/commit/%s", repoLink, commit.Sha1),
|
||||
Author: &api.PayloadUser{
|
||||
Name: commit.AuthorName,
|
||||
Email: commit.AuthorEmail,
|
||||
UserName: authorUsername,
|
||||
},
|
||||
Committer: &api.PayloadUser{
|
||||
Name: commit.CommitterName,
|
||||
Email: commit.CommitterEmail,
|
||||
UserName: committerUsername,
|
||||
},
|
||||
Added: fileStatus.Added,
|
||||
Removed: fileStatus.Removed,
|
||||
Modified: fileStatus.Modified,
|
||||
Timestamp: commit.Timestamp,
|
||||
}
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// AvatarLink tries to match user in database with e-mail
|
||||
// in order to show custom avatar, and falls back to general avatar link.
|
||||
func (pc *PushCommits) AvatarLink(email string) string {
|
||||
if pc.avatars == nil {
|
||||
pc.avatars = make(map[string]string)
|
||||
}
|
||||
avatar, ok := pc.avatars[email]
|
||||
if ok {
|
||||
return avatar
|
||||
}
|
||||
|
||||
u, ok := pc.emailUsers[email]
|
||||
if !ok {
|
||||
var err error
|
||||
u, err = GetUserByEmail(email)
|
||||
if err != nil {
|
||||
pc.avatars[email] = base.AvatarLink(email)
|
||||
if !IsErrUserNotExist(err) {
|
||||
log.Error("GetUserByEmail: %v", err)
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
pc.emailUsers[email] = u
|
||||
}
|
||||
}
|
||||
if u != nil {
|
||||
pc.avatars[email] = u.RelAvatarLink()
|
||||
}
|
||||
|
||||
return pc.avatars[email]
|
||||
}
|
||||
|
||||
// getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
|
||||
// if the provided ref references a non-existent issue.
|
||||
func getIssueFromRef(repo *Repository, index int64) (*Issue, error) {
|
||||
issue, err := GetIssueByIndex(repo.ID, index)
|
||||
if err != nil {
|
||||
if IsErrIssueNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return issue, nil
|
||||
}
|
||||
|
||||
func changeIssueStatus(repo *Repository, issue *Issue, doer *User, status bool) error {
|
||||
|
||||
stopTimerIfAvailable := func(doer *User, issue *Issue) error {
|
||||
|
||||
if StopwatchExists(doer.ID, issue.ID) {
|
||||
if err := CreateOrStopIssueStopwatch(doer, issue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
issue.Repo = repo
|
||||
if err := issue.ChangeStatus(doer, status); err != nil {
|
||||
// Don't return an error when dependencies are open as this would let the push fail
|
||||
if IsErrDependenciesLeft(err) {
|
||||
return stopTimerIfAvailable(doer, issue)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return stopTimerIfAvailable(doer, issue)
|
||||
}
|
||||
|
||||
// UpdateIssuesCommit checks if issues are manipulated by commit message.
|
||||
func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit, branchName string) error {
|
||||
// Commits are appended in the reverse order.
|
||||
for i := len(commits) - 1; i >= 0; i-- {
|
||||
c := commits[i]
|
||||
|
||||
type markKey struct {
|
||||
ID int64
|
||||
Action references.XRefAction
|
||||
}
|
||||
|
||||
refMarked := make(map[markKey]bool)
|
||||
var refRepo *Repository
|
||||
var refIssue *Issue
|
||||
var err error
|
||||
for _, ref := range references.FindAllIssueReferences(c.Message) {
|
||||
|
||||
// issue is from another repo
|
||||
if len(ref.Owner) > 0 && len(ref.Name) > 0 {
|
||||
refRepo, err = GetRepositoryFromMatch(ref.Owner, ref.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
refRepo = repo
|
||||
}
|
||||
if refIssue, err = getIssueFromRef(refRepo, ref.Index); err != nil {
|
||||
return err
|
||||
}
|
||||
if refIssue == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
perm, err := GetUserRepoPermission(refRepo, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := markKey{ID: refIssue.ID, Action: ref.Action}
|
||||
if refMarked[key] {
|
||||
continue
|
||||
}
|
||||
refMarked[key] = true
|
||||
|
||||
// only create comments for issues if user has permission for it
|
||||
if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeIssues) {
|
||||
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, html.EscapeString(c.Message))
|
||||
if err = CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Process closing/reopening keywords
|
||||
if ref.Action != references.XRefActionCloses && ref.Action != references.XRefActionReopens {
|
||||
continue
|
||||
}
|
||||
|
||||
// Change issue status only if the commit has been pushed to the default branch.
|
||||
// and if the repo is configured to allow only that
|
||||
// FIXME: we should be using Issue.ref if set instead of repo.DefaultBranch
|
||||
if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch {
|
||||
continue
|
||||
}
|
||||
|
||||
// only close issues in another repo if user has push access
|
||||
if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeCode) {
|
||||
if err := changeIssueStatus(refRepo, refIssue, doer, ref.Action == references.XRefActionCloses); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) {
|
||||
if err = notifyWatchers(e, &Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: ActionTransferRepo,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
Content: path.Join(oldOwner.Name, repo.Name),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("notifyWatchers: %v", err)
|
||||
}
|
||||
|
||||
// Remove watch for organization.
|
||||
if oldOwner.IsOrganization() {
|
||||
if err = watchRepo(e, oldOwner.ID, repo.ID, false); err != nil {
|
||||
return fmt.Errorf("watchRepo [false]: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransferRepoAction adds new action for transferring repository,
|
||||
// the Owner field of repository is assumed to be new owner.
|
||||
func TransferRepoAction(doer, oldOwner *User, repo *Repository) error {
|
||||
return transferRepoAction(x, doer, oldOwner, repo)
|
||||
}
|
||||
|
||||
func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error {
|
||||
return notifyWatchers(e, &Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: ActionMergePullRequest,
|
||||
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
})
|
||||
}
|
||||
|
||||
// MergePullRequestAction adds new action for merging pull request.
|
||||
func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error {
|
||||
return mergePullRequestAction(x, actUser, repo, pull)
|
||||
}
|
||||
|
||||
// GetFeedsOptions options for retrieving feeds
|
||||
type GetFeedsOptions struct {
|
||||
RequestedUser *User
|
||||
RequestingUserID int64
|
||||
IncludePrivate bool // include private actions
|
||||
OnlyPerformedBy bool // only actions performed by requested user
|
||||
IncludeDeleted bool // include deleted actions
|
||||
RequestedUser *User // the user we want activity for
|
||||
Actor *User // the user viewing the activity
|
||||
IncludePrivate bool // include private actions
|
||||
OnlyPerformedBy bool // only actions performed by requested user
|
||||
IncludeDeleted bool // include deleted actions
|
||||
}
|
||||
|
||||
// GetFeeds returns actions according to the provided options
|
||||
@@ -623,8 +299,14 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
|
||||
cond := builder.NewCond()
|
||||
|
||||
var repoIDs []int64
|
||||
var actorID int64
|
||||
|
||||
if opts.Actor != nil {
|
||||
actorID = opts.Actor.ID
|
||||
}
|
||||
|
||||
if opts.RequestedUser.IsOrganization() {
|
||||
env, err := opts.RequestedUser.AccessibleReposEnv(opts.RequestingUserID)
|
||||
env, err := opts.RequestedUser.AccessibleReposEnv(actorID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
|
||||
}
|
||||
@@ -633,6 +315,14 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
|
||||
}
|
||||
|
||||
cond = cond.And(builder.In("repo_id", repoIDs))
|
||||
} else {
|
||||
cond = cond.And(builder.In("repo_id", AccessibleRepoIDsQuery(opts.Actor)))
|
||||
}
|
||||
|
||||
if opts.Actor == nil || !opts.Actor.IsAdmin {
|
||||
if opts.RequestedUser.KeepActivityPrivate && actorID != opts.RequestedUser.ID {
|
||||
return make([]*Action, 0), nil
|
||||
}
|
||||
}
|
||||
|
||||
cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
|
||||
|
||||
+24
-426
@@ -1,8 +1,11 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -28,422 +31,17 @@ func TestAction_GetRepoLink(t *testing.T) {
|
||||
assert.Equal(t, expected, action.GetRepoLink())
|
||||
}
|
||||
|
||||
func TestNewRepoAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{OwnerID: user.ID}).(*Repository)
|
||||
repo.Owner = user
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionCreateRepo,
|
||||
ActUserID: user.ID,
|
||||
RepoID: repo.ID,
|
||||
ActUser: user,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}
|
||||
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, NewRepoAction(user, repo))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestRenameRepoAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{OwnerID: user.ID}).(*Repository)
|
||||
repo.Owner = user
|
||||
|
||||
oldRepoName := repo.Name
|
||||
const newRepoName = "newRepoName"
|
||||
repo.Name = newRepoName
|
||||
repo.LowerName = strings.ToLower(newRepoName)
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionRenameRepo,
|
||||
ActUserID: user.ID,
|
||||
ActUser: user,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
Content: oldRepoName,
|
||||
}
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, RenameRepoAction(user, oldRepoName, repo))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
|
||||
_, err := x.ID(repo.ID).Cols("name", "lower_name").Update(repo)
|
||||
assert.NoError(t, err)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestPushCommits_ToAPIPayloadCommits(t *testing.T) {
|
||||
pushCommits := NewPushCommits()
|
||||
pushCommits.Commits = []*PushCommit{
|
||||
{
|
||||
Sha1: "69554a6",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User2",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User2",
|
||||
Message: "not signed commit",
|
||||
},
|
||||
{
|
||||
Sha1: "27566bd",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User2",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User2",
|
||||
Message: "good signed commit (with not yet validated email)",
|
||||
},
|
||||
{
|
||||
Sha1: "5099b81",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User2",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User2",
|
||||
Message: "good signed commit",
|
||||
},
|
||||
}
|
||||
pushCommits.Len = len(pushCommits.Commits)
|
||||
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 16}).(*Repository)
|
||||
payloadCommits, err := pushCommits.ToAPIPayloadCommits(repo.RepoPath(), "/user2/repo16")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, len(payloadCommits))
|
||||
|
||||
assert.Equal(t, "69554a6", payloadCommits[0].ID)
|
||||
assert.Equal(t, "not signed commit", payloadCommits[0].Message)
|
||||
assert.Equal(t, "/user2/repo16/commit/69554a6", payloadCommits[0].URL)
|
||||
assert.Equal(t, "User2", payloadCommits[0].Committer.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[0].Committer.UserName)
|
||||
assert.Equal(t, "User2", payloadCommits[0].Author.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[0].Author.UserName)
|
||||
assert.EqualValues(t, []string{}, payloadCommits[0].Added)
|
||||
assert.EqualValues(t, []string{}, payloadCommits[0].Removed)
|
||||
assert.EqualValues(t, []string{"readme.md"}, payloadCommits[0].Modified)
|
||||
|
||||
assert.Equal(t, "27566bd", payloadCommits[1].ID)
|
||||
assert.Equal(t, "good signed commit (with not yet validated email)", payloadCommits[1].Message)
|
||||
assert.Equal(t, "/user2/repo16/commit/27566bd", payloadCommits[1].URL)
|
||||
assert.Equal(t, "User2", payloadCommits[1].Committer.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[1].Committer.UserName)
|
||||
assert.Equal(t, "User2", payloadCommits[1].Author.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[1].Author.UserName)
|
||||
assert.EqualValues(t, []string{}, payloadCommits[1].Added)
|
||||
assert.EqualValues(t, []string{}, payloadCommits[1].Removed)
|
||||
assert.EqualValues(t, []string{"readme.md"}, payloadCommits[1].Modified)
|
||||
|
||||
assert.Equal(t, "5099b81", payloadCommits[2].ID)
|
||||
assert.Equal(t, "good signed commit", payloadCommits[2].Message)
|
||||
assert.Equal(t, "/user2/repo16/commit/5099b81", payloadCommits[2].URL)
|
||||
assert.Equal(t, "User2", payloadCommits[2].Committer.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[2].Committer.UserName)
|
||||
assert.Equal(t, "User2", payloadCommits[2].Author.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[2].Author.UserName)
|
||||
assert.EqualValues(t, []string{"readme.md"}, payloadCommits[2].Added)
|
||||
assert.EqualValues(t, []string{}, payloadCommits[2].Removed)
|
||||
assert.EqualValues(t, []string{}, payloadCommits[2].Modified)
|
||||
}
|
||||
|
||||
func TestPushCommits_AvatarLink(t *testing.T) {
|
||||
pushCommits := NewPushCommits()
|
||||
pushCommits.Commits = []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user4@example.com",
|
||||
AuthorName: "User Four",
|
||||
Message: "message1",
|
||||
},
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "message2",
|
||||
},
|
||||
}
|
||||
pushCommits.Len = len(pushCommits.Commits)
|
||||
|
||||
assert.Equal(t,
|
||||
"/suburl/user/avatar/user2/-1",
|
||||
pushCommits.AvatarLink("user2@example.com"))
|
||||
|
||||
assert.Equal(t,
|
||||
"https://secure.gravatar.com/avatar/19ade630b94e1e0535b3df7387434154?d=identicon",
|
||||
pushCommits.AvatarLink("nonexistent@example.com"))
|
||||
}
|
||||
|
||||
func TestUpdateIssuesCommit(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
pushCommits := []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user4@example.com",
|
||||
AuthorName: "User Four",
|
||||
Message: "start working on #FST-1, #1",
|
||||
},
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "a plain message",
|
||||
},
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "close #2",
|
||||
},
|
||||
}
|
||||
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||
repo.Owner = user
|
||||
|
||||
commentBean := &Comment{
|
||||
Type: CommentTypeCommitRef,
|
||||
CommitSHA: "abcdef1",
|
||||
PosterID: user.ID,
|
||||
IssueID: 1,
|
||||
}
|
||||
issueBean := &Issue{RepoID: repo.ID, Index: 2}
|
||||
|
||||
AssertNotExistsBean(t, commentBean)
|
||||
AssertNotExistsBean(t, &Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
|
||||
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
|
||||
AssertExistsAndLoadBean(t, commentBean)
|
||||
AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
|
||||
// Test that push to a non-default branch closes no issue.
|
||||
pushCommits = []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user4@example.com",
|
||||
AuthorName: "User Four",
|
||||
Message: "close #1",
|
||||
},
|
||||
}
|
||||
repo = AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
|
||||
commentBean = &Comment{
|
||||
Type: CommentTypeCommitRef,
|
||||
CommitSHA: "abcdef1",
|
||||
PosterID: user.ID,
|
||||
IssueID: 6,
|
||||
}
|
||||
issueBean = &Issue{RepoID: repo.ID, Index: 1}
|
||||
|
||||
AssertNotExistsBean(t, commentBean)
|
||||
AssertNotExistsBean(t, &Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
|
||||
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, "non-existing-branch"))
|
||||
AssertExistsAndLoadBean(t, commentBean)
|
||||
AssertNotExistsBean(t, issueBean, "is_closed=1")
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestUpdateIssuesCommit_Colon(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
pushCommits := []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "close: #2",
|
||||
},
|
||||
}
|
||||
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||
repo.Owner = user
|
||||
|
||||
issueBean := &Issue{RepoID: repo.ID, Index: 2}
|
||||
|
||||
AssertNotExistsBean(t, &Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
|
||||
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
|
||||
AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestUpdateIssuesCommit_Issue5957(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
// Test that push to a non-default branch closes an issue.
|
||||
pushCommits := []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user4@example.com",
|
||||
AuthorName: "User Four",
|
||||
Message: "close #2",
|
||||
},
|
||||
}
|
||||
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
||||
commentBean := &Comment{
|
||||
Type: CommentTypeCommitRef,
|
||||
CommitSHA: "abcdef1",
|
||||
PosterID: user.ID,
|
||||
IssueID: 7,
|
||||
}
|
||||
|
||||
issueBean := &Issue{RepoID: repo.ID, Index: 2, ID: 7}
|
||||
|
||||
AssertNotExistsBean(t, commentBean)
|
||||
AssertNotExistsBean(t, issueBean, "is_closed=1")
|
||||
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, "non-existing-branch"))
|
||||
AssertExistsAndLoadBean(t, commentBean)
|
||||
AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestUpdateIssuesCommit_AnotherRepo(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
// Test that a push to default branch closes issue in another repo
|
||||
// If the user also has push permissions to that repo
|
||||
pushCommits := []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "close user2/repo1#1",
|
||||
},
|
||||
}
|
||||
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
||||
commentBean := &Comment{
|
||||
Type: CommentTypeCommitRef,
|
||||
CommitSHA: "abcdef1",
|
||||
PosterID: user.ID,
|
||||
IssueID: 1,
|
||||
}
|
||||
|
||||
issueBean := &Issue{RepoID: 1, Index: 1, ID: 1}
|
||||
|
||||
AssertNotExistsBean(t, commentBean)
|
||||
AssertNotExistsBean(t, issueBean, "is_closed=1")
|
||||
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
|
||||
AssertExistsAndLoadBean(t, commentBean)
|
||||
AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 10}).(*User)
|
||||
|
||||
// Test that a push with close reference *can not* close issue
|
||||
// If the commiter doesn't have push rights in that repo
|
||||
pushCommits := []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef3",
|
||||
CommitterEmail: "user10@example.com",
|
||||
CommitterName: "User Ten",
|
||||
AuthorEmail: "user10@example.com",
|
||||
AuthorName: "User Ten",
|
||||
Message: "close user3/repo3#1",
|
||||
},
|
||||
}
|
||||
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 6}).(*Repository)
|
||||
commentBean := &Comment{
|
||||
Type: CommentTypeCommitRef,
|
||||
CommitSHA: "abcdef3",
|
||||
PosterID: user.ID,
|
||||
IssueID: 6,
|
||||
}
|
||||
|
||||
issueBean := &Issue{RepoID: 3, Index: 1, ID: 6}
|
||||
|
||||
AssertNotExistsBean(t, commentBean)
|
||||
AssertNotExistsBean(t, issueBean, "is_closed=1")
|
||||
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
|
||||
AssertNotExistsBean(t, commentBean)
|
||||
AssertNotExistsBean(t, issueBean, "is_closed=1")
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestTransferRepoAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1, OwnerID: user2.ID}).(*Repository)
|
||||
|
||||
repo.OwnerID = user4.ID
|
||||
repo.Owner = user4
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionTransferRepo,
|
||||
ActUserID: user2.ID,
|
||||
ActUser: user2,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, TransferRepoAction(user2, user2, repo))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
|
||||
_, err := x.ID(repo.ID).Cols("owner_id").Update(repo)
|
||||
assert.NoError(t, err)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestMergePullRequestAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1, OwnerID: user.ID}).(*Repository)
|
||||
repo.Owner = user
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: 3, RepoID: repo.ID}).(*Issue)
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionMergePullRequest,
|
||||
ActUserID: user.ID,
|
||||
ActUser: user,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, MergePullRequestAction(user, repo, issue))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestGetFeeds(t *testing.T) {
|
||||
// test with an individual user
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
actions, err := GetFeeds(GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
RequestingUserID: user.ID,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
RequestedUser: user,
|
||||
Actor: user,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, actions, 1) {
|
||||
@@ -452,10 +50,10 @@ func TestGetFeeds(t *testing.T) {
|
||||
}
|
||||
|
||||
actions, err = GetFeeds(GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
RequestingUserID: user.ID,
|
||||
IncludePrivate: false,
|
||||
OnlyPerformedBy: false,
|
||||
RequestedUser: user,
|
||||
Actor: user,
|
||||
IncludePrivate: false,
|
||||
OnlyPerformedBy: false,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 0)
|
||||
@@ -465,14 +63,14 @@ func TestGetFeeds2(t *testing.T) {
|
||||
// test with an organization user
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
|
||||
const userID = 2 // user2 is an owner of the organization
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
actions, err := GetFeeds(GetFeedsOptions{
|
||||
RequestedUser: org,
|
||||
RequestingUserID: userID,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
RequestedUser: org,
|
||||
Actor: user,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
@@ -482,11 +80,11 @@ func TestGetFeeds2(t *testing.T) {
|
||||
}
|
||||
|
||||
actions, err = GetFeeds(GetFeedsOptions{
|
||||
RequestedUser: org,
|
||||
RequestingUserID: userID,
|
||||
IncludePrivate: false,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
RequestedUser: org,
|
||||
Actor: user,
|
||||
IncludePrivate: false,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 0)
|
||||
|
||||
+10
-5
@@ -20,6 +20,8 @@ type NoticeType int
|
||||
const (
|
||||
//NoticeRepository type
|
||||
NoticeRepository NoticeType = iota + 1
|
||||
// NoticeTask type
|
||||
NoticeTask
|
||||
)
|
||||
|
||||
// Notice represents a system notice for admin.
|
||||
@@ -36,11 +38,14 @@ func (n *Notice) TrStr() string {
|
||||
}
|
||||
|
||||
// CreateNotice creates new system notice.
|
||||
func CreateNotice(tp NoticeType, desc string) error {
|
||||
return createNotice(x, tp, desc)
|
||||
func CreateNotice(tp NoticeType, desc string, args ...interface{}) error {
|
||||
return createNotice(x, tp, desc, args...)
|
||||
}
|
||||
|
||||
func createNotice(e Engine, tp NoticeType, desc string) error {
|
||||
func createNotice(e Engine, tp NoticeType, desc string, args ...interface{}) error {
|
||||
if len(args) > 0 {
|
||||
desc = fmt.Sprintf(desc, args...)
|
||||
}
|
||||
n := &Notice{
|
||||
Type: tp,
|
||||
Description: desc,
|
||||
@@ -50,8 +55,8 @@ func createNotice(e Engine, tp NoticeType, desc string) error {
|
||||
}
|
||||
|
||||
// CreateRepositoryNotice creates new system notice with type NoticeRepository.
|
||||
func CreateRepositoryNotice(desc string) error {
|
||||
return createNotice(x, NoticeRepository, desc)
|
||||
func CreateRepositoryNotice(desc string, args ...interface{}) error {
|
||||
return createNotice(x, NoticeRepository, desc, args...)
|
||||
}
|
||||
|
||||
// RemoveAllWithNotice removes all directories in given path and
|
||||
|
||||
+36
-8
@@ -14,7 +14,7 @@ import (
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
gouuid "github.com/satori/go.uuid"
|
||||
gouuid "github.com/google/uuid"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@@ -71,9 +71,33 @@ func (a *Attachment) DownloadURL() string {
|
||||
return fmt.Sprintf("%sattachments/%s", setting.AppURL, a.UUID)
|
||||
}
|
||||
|
||||
// LinkedRepository returns the linked repo if any
|
||||
func (a *Attachment) LinkedRepository() (*Repository, UnitType, error) {
|
||||
if a.IssueID != 0 {
|
||||
iss, err := GetIssueByID(a.IssueID)
|
||||
if err != nil {
|
||||
return nil, UnitTypeIssues, err
|
||||
}
|
||||
repo, err := GetRepositoryByID(iss.RepoID)
|
||||
unitType := UnitTypeIssues
|
||||
if iss.IsPull {
|
||||
unitType = UnitTypePullRequests
|
||||
}
|
||||
return repo, unitType, err
|
||||
} else if a.ReleaseID != 0 {
|
||||
rel, err := GetReleaseByID(a.ReleaseID)
|
||||
if err != nil {
|
||||
return nil, UnitTypeReleases, err
|
||||
}
|
||||
repo, err := GetRepositoryByID(rel.RepoID)
|
||||
return repo, UnitTypeReleases, err
|
||||
}
|
||||
return nil, -1, nil
|
||||
}
|
||||
|
||||
// NewAttachment creates a new attachment object.
|
||||
func NewAttachment(attach *Attachment, buf []byte, file io.Reader) (_ *Attachment, err error) {
|
||||
attach.UUID = gouuid.NewV4().String()
|
||||
attach.UUID = gouuid.New().String()
|
||||
|
||||
localPath := attach.LocalPath()
|
||||
if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil {
|
||||
@@ -112,9 +136,8 @@ func GetAttachmentByID(id int64) (*Attachment, error) {
|
||||
}
|
||||
|
||||
func getAttachmentByID(e Engine, id int64) (*Attachment, error) {
|
||||
attach := &Attachment{ID: id}
|
||||
|
||||
if has, err := e.Get(attach); err != nil {
|
||||
attach := &Attachment{}
|
||||
if has, err := e.ID(id).Get(attach); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrAttachmentNotExist{ID: id, UUID: ""}
|
||||
@@ -123,8 +146,8 @@ func getAttachmentByID(e Engine, id int64) (*Attachment, error) {
|
||||
}
|
||||
|
||||
func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) {
|
||||
attach := &Attachment{UUID: uuid}
|
||||
has, err := e.Get(attach)
|
||||
attach := &Attachment{}
|
||||
has, err := e.Where("uuid=?", uuid).Get(attach)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -133,6 +156,11 @@ func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) {
|
||||
return attach, nil
|
||||
}
|
||||
|
||||
// GetAttachmentsByUUIDs returns attachment by given UUID list.
|
||||
func GetAttachmentsByUUIDs(uuids []string) ([]*Attachment, error) {
|
||||
return getAttachmentsByUUIDs(x, uuids)
|
||||
}
|
||||
|
||||
func getAttachmentsByUUIDs(e Engine, uuids []string) ([]*Attachment, error) {
|
||||
if len(uuids) == 0 {
|
||||
return []*Attachment{}, nil
|
||||
@@ -170,7 +198,7 @@ func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) {
|
||||
|
||||
func getAttachmentsByCommentID(e Engine, commentID int64) ([]*Attachment, error) {
|
||||
attachments := make([]*Attachment, 0, 10)
|
||||
return attachments, x.Where("comment_id=?", commentID).Find(&attachments)
|
||||
return attachments, e.Where("comment_id=?", commentID).Find(&attachments)
|
||||
}
|
||||
|
||||
// getAttachmentByReleaseIDFileName return a file based on the the following infos:
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestGetByCommentOrIssueID(t *testing.T) {
|
||||
// count of attachments from issue ID
|
||||
attachments, err := GetAttachmentsByIssueID(1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(attachments))
|
||||
assert.Equal(t, 1, len(attachments))
|
||||
|
||||
attachments, err = GetAttachmentsByCommentID(1)
|
||||
assert.NoError(t, err)
|
||||
@@ -73,7 +73,7 @@ func TestDeleteAttachments(t *testing.T) {
|
||||
|
||||
count, err := DeleteAttachmentsByIssue(4, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
assert.Equal(t, 2, count)
|
||||
|
||||
count, err = DeleteAttachmentsByComment(2, false)
|
||||
assert.NoError(t, err)
|
||||
@@ -116,3 +116,43 @@ func TestUpdateAttachment(t *testing.T) {
|
||||
|
||||
AssertExistsAndLoadBean(t, &Attachment{Name: "new_name"})
|
||||
}
|
||||
|
||||
func TestGetAttachmentsByUUIDs(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
attachList, err := GetAttachmentsByUUIDs([]string{"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", "not-existing-uuid"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(attachList))
|
||||
assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attachList[0].UUID)
|
||||
assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", attachList[1].UUID)
|
||||
assert.Equal(t, int64(1), attachList[0].IssueID)
|
||||
assert.Equal(t, int64(5), attachList[1].IssueID)
|
||||
}
|
||||
|
||||
func TestLinkedRepository(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
testCases := []struct {
|
||||
name string
|
||||
attachID int64
|
||||
expectedRepo *Repository
|
||||
expectedUnitType UnitType
|
||||
}{
|
||||
{"LinkedIssue", 1, &Repository{ID: 1}, UnitTypeIssues},
|
||||
{"LinkedComment", 3, &Repository{ID: 1}, UnitTypePullRequests},
|
||||
{"LinkedRelease", 9, &Repository{ID: 1}, UnitTypeReleases},
|
||||
{"Notlinked", 10, nil, -1},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
attach, err := GetAttachmentByID(tc.attachID)
|
||||
assert.NoError(t, err)
|
||||
repo, unitType, err := attach.LinkedRepository()
|
||||
assert.NoError(t, err)
|
||||
if tc.expectedRepo != nil {
|
||||
assert.Equal(t, tc.expectedRepo.ID, repo.ID)
|
||||
}
|
||||
assert.Equal(t, tc.expectedUnitType, unitType)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// EmailHash represents a pre-generated hash map
|
||||
type EmailHash struct {
|
||||
Hash string `xorm:"pk varchar(32)"`
|
||||
Email string `xorm:"UNIQUE NOT NULL"`
|
||||
}
|
||||
|
||||
// GetEmailForHash converts a provided md5sum to the email
|
||||
func GetEmailForHash(md5Sum string) (string, error) {
|
||||
return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
|
||||
emailHash := EmailHash{
|
||||
Hash: strings.ToLower(strings.TrimSpace(md5Sum)),
|
||||
}
|
||||
|
||||
_, err := x.Get(&emailHash)
|
||||
return emailHash.Email, err
|
||||
})
|
||||
}
|
||||
|
||||
// AvatarLink returns an avatar link for a provided email
|
||||
func AvatarLink(email string) string {
|
||||
lowerEmail := strings.ToLower(strings.TrimSpace(email))
|
||||
sum := fmt.Sprintf("%x", md5.Sum([]byte(lowerEmail)))
|
||||
_, _ = cache.GetString("Avatar:"+sum, func() (string, error) {
|
||||
emailHash := &EmailHash{
|
||||
Email: lowerEmail,
|
||||
Hash: sum,
|
||||
}
|
||||
_, _ = x.Insert(emailHash)
|
||||
return lowerEmail, nil
|
||||
})
|
||||
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum)
|
||||
}
|
||||
+142
-63
@@ -5,15 +5,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/unknwon/com"
|
||||
)
|
||||
|
||||
@@ -31,19 +33,26 @@ type ProtectedBranch struct {
|
||||
BranchName string `xorm:"UNIQUE(s)"`
|
||||
CanPush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableWhitelist bool
|
||||
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
||||
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
||||
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
|
||||
BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"`
|
||||
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
|
||||
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ProtectedFilePatterns string `xorm:"TEXT"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
// IsProtected returns if the branch is protected
|
||||
@@ -53,10 +62,25 @@ func (protectBranch *ProtectedBranch) IsProtected() bool {
|
||||
|
||||
// CanUserPush returns if some user could push to this protected branch
|
||||
func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
|
||||
if !protectBranch.EnableWhitelist {
|
||||
if !protectBranch.CanPush {
|
||||
return false
|
||||
}
|
||||
|
||||
if !protectBranch.EnableWhitelist {
|
||||
if user, err := GetUserByID(userID); err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return false
|
||||
} else if repo, err := GetRepositoryByID(protectBranch.RepoID); err != nil {
|
||||
log.Error("GetRepositoryByID: %v", err)
|
||||
return false
|
||||
} else if writeAccess, err := HasAccessUnit(user, repo, UnitTypeCode, AccessModeWrite); err != nil {
|
||||
log.Error("HasAccessUnit: %v", err)
|
||||
return false
|
||||
} else {
|
||||
return writeAccess
|
||||
}
|
||||
}
|
||||
|
||||
if base.Int64sContains(protectBranch.WhitelistUserIDs, userID) {
|
||||
return true
|
||||
}
|
||||
@@ -73,8 +97,8 @@ func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
|
||||
return in
|
||||
}
|
||||
|
||||
// CanUserMerge returns if some user could merge a pull request to this protected branch
|
||||
func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
|
||||
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
|
||||
func (protectBranch *ProtectedBranch) IsUserMergeWhitelisted(userID int64) bool {
|
||||
if !protectBranch.EnableMergeWhitelist {
|
||||
return true
|
||||
}
|
||||
@@ -95,6 +119,38 @@ func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
|
||||
return in
|
||||
}
|
||||
|
||||
// IsUserOfficialReviewer check if user is official reviewer for the branch (counts towards required approvals)
|
||||
func (protectBranch *ProtectedBranch) IsUserOfficialReviewer(user *User) (bool, error) {
|
||||
return protectBranch.isUserOfficialReviewer(x, user)
|
||||
}
|
||||
|
||||
func (protectBranch *ProtectedBranch) isUserOfficialReviewer(e Engine, user *User) (bool, error) {
|
||||
repo, err := getRepositoryByID(e, protectBranch.RepoID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !protectBranch.EnableApprovalsWhitelist {
|
||||
// Anyone with write access is considered official reviewer
|
||||
writeAccess, err := hasAccessUnit(e, user, repo, UnitTypeCode, AccessModeWrite)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return writeAccess, nil
|
||||
}
|
||||
|
||||
if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, user.ID) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
inTeam, err := isUserInTeams(e, user.ID, protectBranch.ApprovalsWhitelistTeamIDs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return inTeam, nil
|
||||
}
|
||||
|
||||
// HasEnoughApprovals returns true if pr has enough granted approvals.
|
||||
func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
|
||||
if protectBranch.RequiredApprovals == 0 {
|
||||
@@ -105,30 +161,58 @@ 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.
|
||||
func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
|
||||
reviews, err := GetReviewersByPullID(pr.IssueID)
|
||||
sess := x.Where("issue_id = ?", pr.IssueID).
|
||||
And("type = ?", ReviewTypeApprove).
|
||||
And("official = ?", true)
|
||||
if protectBranch.DismissStaleApprovals {
|
||||
sess = sess.And("stale = ?", false)
|
||||
}
|
||||
approvals, err := sess.Count(new(Review))
|
||||
if err != nil {
|
||||
log.Error("GetReviewersByPullID: %v", err)
|
||||
log.Error("GetGrantedApprovalsCount: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
approvals := int64(0)
|
||||
userIDs := make([]int64, 0)
|
||||
for _, review := range reviews {
|
||||
if review.Type != ReviewTypeApprove {
|
||||
continue
|
||||
}
|
||||
if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, review.ID) {
|
||||
approvals++
|
||||
continue
|
||||
}
|
||||
userIDs = append(userIDs, review.ID)
|
||||
return approvals
|
||||
}
|
||||
|
||||
// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
|
||||
// An official ReviewRequest should also block Merge like Reject
|
||||
func (protectBranch *ProtectedBranch) MergeBlockedByRejectedReview(pr *PullRequest) bool {
|
||||
if !protectBranch.BlockOnRejectedReviews {
|
||||
return false
|
||||
}
|
||||
approvalTeamCount, err := UsersInTeamsCount(userIDs, protectBranch.ApprovalsWhitelistTeamIDs)
|
||||
rejectExist, err := x.Where("issue_id = ?", pr.IssueID).
|
||||
And("type in ( ?, ?)", ReviewTypeReject, ReviewTypeRequest).
|
||||
And("official = ?", true).
|
||||
Exist(new(Review))
|
||||
if err != nil {
|
||||
log.Error("UsersInTeamsCount: %v", err)
|
||||
return 0
|
||||
log.Error("MergeBlockedByRejectedReview: %v", err)
|
||||
return true
|
||||
}
|
||||
return approvalTeamCount + approvals
|
||||
|
||||
return rejectExist
|
||||
}
|
||||
|
||||
// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch
|
||||
func (protectBranch *ProtectedBranch) MergeBlockedByOutdatedBranch(pr *PullRequest) bool {
|
||||
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
|
||||
}
|
||||
|
||||
// GetProtectedFilePatterns parses a semicolon separated list of protected file patterns and returns a glob.Glob slice
|
||||
func (protectBranch *ProtectedBranch) GetProtectedFilePatterns() []glob.Glob {
|
||||
extarr := make([]glob.Glob, 0, 10)
|
||||
for _, expr := range strings.Split(strings.ToLower(protectBranch.ProtectedFilePatterns), ";") {
|
||||
expr = strings.TrimSpace(expr)
|
||||
if expr != "" {
|
||||
if g, err := glob.Compile(expr, '.', '/'); err != nil {
|
||||
log.Info("Invalid glob expresion '%s' (skipped): %v", expr, err)
|
||||
} else {
|
||||
extarr = append(extarr, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
return extarr
|
||||
}
|
||||
|
||||
// GetProtectedBranchByRepoID getting protected branch by repo ID
|
||||
@@ -139,8 +223,12 @@ func GetProtectedBranchByRepoID(repoID int64) ([]*ProtectedBranch, error) {
|
||||
|
||||
// GetProtectedBranchBy getting protected branch by ID/Name
|
||||
func GetProtectedBranchBy(repoID int64, branchName string) (*ProtectedBranch, error) {
|
||||
return getProtectedBranchBy(x, repoID, branchName)
|
||||
}
|
||||
|
||||
func getProtectedBranchBy(e Engine, repoID int64, branchName string) (*ProtectedBranch, error) {
|
||||
rel := &ProtectedBranch{RepoID: repoID, BranchName: branchName}
|
||||
has, err := x.Get(rel)
|
||||
has, err := e.Get(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -152,8 +240,8 @@ func GetProtectedBranchBy(repoID int64, branchName string) (*ProtectedBranch, er
|
||||
|
||||
// GetProtectedBranchByID getting protected branch by ID
|
||||
func GetProtectedBranchByID(id int64) (*ProtectedBranch, error) {
|
||||
rel := &ProtectedBranch{ID: id}
|
||||
has, err := x.Get(rel)
|
||||
rel := &ProtectedBranch{}
|
||||
has, err := x.ID(id).Get(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -242,6 +330,11 @@ func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
|
||||
return protectedBranches, x.Find(&protectedBranches, &ProtectedBranch{RepoID: repo.ID})
|
||||
}
|
||||
|
||||
// GetBranchProtection get the branch protection of a branch
|
||||
func (repo *Repository) GetBranchProtection(branchName string) (*ProtectedBranch, error) {
|
||||
return GetProtectedBranchBy(repo.ID, branchName)
|
||||
}
|
||||
|
||||
// IsProtectedBranch checks if branch is protected
|
||||
func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, error) {
|
||||
if doer == nil {
|
||||
@@ -281,27 +374,6 @@ func (repo *Repository) IsProtectedBranchForPush(branchName string, doer *User)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// IsProtectedBranchForMerging checks if branch is protected for merging
|
||||
func (repo *Repository) IsProtectedBranchForMerging(pr *PullRequest, branchName string, doer *User) (bool, error) {
|
||||
if doer == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
protectedBranch := &ProtectedBranch{
|
||||
RepoID: repo.ID,
|
||||
BranchName: branchName,
|
||||
}
|
||||
|
||||
has, err := x.Get(protectedBranch)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if has {
|
||||
return !protectedBranch.CanUserMerge(doer.ID) || !protectedBranch.HasEnoughApprovals(pr), nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
|
||||
// the users from newWhitelist which have explicit read or write access to the repo.
|
||||
func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
|
||||
@@ -437,9 +509,9 @@ func (repo *Repository) GetDeletedBranches() ([]*DeletedBranch, error) {
|
||||
}
|
||||
|
||||
// GetDeletedBranchByID get a deleted branch by its ID
|
||||
func (repo *Repository) GetDeletedBranchByID(ID int64) (*DeletedBranch, error) {
|
||||
deletedBranch := &DeletedBranch{ID: ID}
|
||||
has, err := x.Get(deletedBranch)
|
||||
func (repo *Repository) GetDeletedBranchByID(id int64) (*DeletedBranch, error) {
|
||||
deletedBranch := &DeletedBranch{}
|
||||
has, err := x.ID(id).Get(deletedBranch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -481,11 +553,18 @@ func (deletedBranch *DeletedBranch) LoadUser() {
|
||||
deletedBranch.DeletedBy = user
|
||||
}
|
||||
|
||||
// RemoveDeletedBranch removes all deleted branches
|
||||
func RemoveDeletedBranch(repoID int64, branch string) error {
|
||||
_, err := x.Where("repo_id=? AND name=?", repoID, branch).Delete(new(DeletedBranch))
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveOldDeletedBranches removes old deleted branches
|
||||
func RemoveOldDeletedBranches() {
|
||||
func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) {
|
||||
// Nothing to do for shutdown or terminate
|
||||
log.Trace("Doing: DeletedBranchesCleanup")
|
||||
|
||||
deleteBefore := time.Now().Add(-setting.Cron.DeletedBranchesCleanup.OlderThan)
|
||||
deleteBefore := time.Now().Add(-olderThan)
|
||||
_, err := x.Where("deleted_unix < ?", deleteBefore.Unix()).Delete(new(DeletedBranch))
|
||||
if err != nil {
|
||||
log.Error("DeletedBranchesCleanup: %v", err)
|
||||
|
||||
+20
-49
@@ -19,52 +19,19 @@ import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CommitStatusState holds the state of a Status
|
||||
// It can be "pending", "success", "error", "failure", and "warning"
|
||||
type CommitStatusState string
|
||||
|
||||
// IsWorseThan returns true if this State is worse than the given State
|
||||
func (css CommitStatusState) IsWorseThan(css2 CommitStatusState) bool {
|
||||
switch css {
|
||||
case CommitStatusError:
|
||||
return true
|
||||
case CommitStatusFailure:
|
||||
return css2 != CommitStatusError
|
||||
case CommitStatusWarning:
|
||||
return css2 != CommitStatusError && css2 != CommitStatusFailure
|
||||
case CommitStatusSuccess:
|
||||
return css2 != CommitStatusError && css2 != CommitStatusFailure && css2 != CommitStatusWarning
|
||||
default:
|
||||
return css2 != CommitStatusError && css2 != CommitStatusFailure && css2 != CommitStatusWarning && css2 != CommitStatusSuccess
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// CommitStatusPending is for when the Status is Pending
|
||||
CommitStatusPending CommitStatusState = "pending"
|
||||
// CommitStatusSuccess is for when the Status is Success
|
||||
CommitStatusSuccess CommitStatusState = "success"
|
||||
// CommitStatusError is for when the Status is Error
|
||||
CommitStatusError CommitStatusState = "error"
|
||||
// CommitStatusFailure is for when the Status is Failure
|
||||
CommitStatusFailure CommitStatusState = "failure"
|
||||
// CommitStatusWarning is for when the Status is Warning
|
||||
CommitStatusWarning CommitStatusState = "warning"
|
||||
)
|
||||
|
||||
// CommitStatus holds a single Status of a single Commit
|
||||
type CommitStatus struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
|
||||
Repo *Repository `xorm:"-"`
|
||||
State CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
|
||||
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
|
||||
TargetURL string `xorm:"TEXT"`
|
||||
Description string `xorm:"TEXT"`
|
||||
ContextHash string `xorm:"char(40) index"`
|
||||
Context string `xorm:"TEXT"`
|
||||
Creator *User `xorm:"-"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
|
||||
Repo *Repository `xorm:"-"`
|
||||
State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
|
||||
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
|
||||
TargetURL string `xorm:"TEXT"`
|
||||
Description string `xorm:"TEXT"`
|
||||
ContextHash string `xorm:"char(40) index"`
|
||||
Context string `xorm:"TEXT"`
|
||||
Creator *User `xorm:"-"`
|
||||
CreatorID int64
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
@@ -118,9 +85,9 @@ func (status *CommitStatus) APIFormat() *api.Status {
|
||||
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
|
||||
func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
|
||||
var lastStatus *CommitStatus
|
||||
var state CommitStatusState
|
||||
var state api.CommitStatusState
|
||||
for _, status := range statuses {
|
||||
if status.State.IsWorseThan(state) {
|
||||
if status.State.NoBetterThan(state) {
|
||||
state = status.State
|
||||
lastStatus = status
|
||||
}
|
||||
@@ -137,7 +104,7 @@ func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
|
||||
|
||||
// CommitStatusOptions holds the options for query commit statuses
|
||||
type CommitStatusOptions struct {
|
||||
Page int
|
||||
ListOptions
|
||||
State string
|
||||
SortType string
|
||||
}
|
||||
@@ -147,18 +114,22 @@ func GetCommitStatuses(repo *Repository, sha string, opts *CommitStatusOptions)
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize <= 0 {
|
||||
opts.Page = ItemsPerPage
|
||||
}
|
||||
|
||||
countSession := listCommitStatusesStatement(repo, sha, opts)
|
||||
countSession = opts.setSessionPagination(countSession)
|
||||
maxResults, err := countSession.Count(new(CommitStatus))
|
||||
if err != nil {
|
||||
log.Error("Count PRs: %v", err)
|
||||
return nil, maxResults, err
|
||||
}
|
||||
|
||||
statuses := make([]*CommitStatus, 0, ItemsPerPage)
|
||||
statuses := make([]*CommitStatus, 0, opts.PageSize)
|
||||
findSession := listCommitStatusesStatement(repo, sha, opts)
|
||||
findSession = opts.setSessionPagination(findSession)
|
||||
sortCommitStatusesSession(findSession, opts.SortType)
|
||||
findSession.Limit(ItemsPerPage, (opts.Page-1)*ItemsPerPage)
|
||||
return statuses, maxResults, findSession.Find(&statuses)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package models
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -23,22 +24,22 @@ func TestGetCommitStatuses(t *testing.T) {
|
||||
assert.Len(t, statuses, 5)
|
||||
|
||||
assert.Equal(t, "ci/awesomeness", statuses[0].Context)
|
||||
assert.Equal(t, CommitStatusPending, statuses[0].State)
|
||||
assert.Equal(t, structs.CommitStatusPending, statuses[0].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL())
|
||||
|
||||
assert.Equal(t, "cov/awesomeness", statuses[1].Context)
|
||||
assert.Equal(t, CommitStatusWarning, statuses[1].State)
|
||||
assert.Equal(t, structs.CommitStatusWarning, statuses[1].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL())
|
||||
|
||||
assert.Equal(t, "cov/awesomeness", statuses[2].Context)
|
||||
assert.Equal(t, CommitStatusSuccess, statuses[2].State)
|
||||
assert.Equal(t, structs.CommitStatusSuccess, statuses[2].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL())
|
||||
|
||||
assert.Equal(t, "ci/awesomeness", statuses[3].Context)
|
||||
assert.Equal(t, CommitStatusFailure, statuses[3].State)
|
||||
assert.Equal(t, structs.CommitStatusFailure, statuses[3].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL())
|
||||
|
||||
assert.Equal(t, "deploy/awesomeness", statuses[4].Context)
|
||||
assert.Equal(t, CommitStatusError, statuses[4].State)
|
||||
assert.Equal(t, structs.CommitStatusError, statuses[4].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL())
|
||||
}
|
||||
|
||||
+133
-2
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// consistencyCheckable a type that can be tested for database consistency
|
||||
@@ -84,14 +85,17 @@ func (user *User) checkForConsistency(t *testing.T) {
|
||||
func (repo *Repository) checkForConsistency(t *testing.T) {
|
||||
assert.Equal(t, repo.LowerName, strings.ToLower(repo.Name), "repo: %+v", repo)
|
||||
assertCount(t, &Star{RepoID: repo.ID}, repo.NumStars)
|
||||
assertCount(t, &Watch{RepoID: repo.ID}, repo.NumWatches)
|
||||
assertCount(t, &Milestone{RepoID: repo.ID}, repo.NumMilestones)
|
||||
assertCount(t, &Repository{ForkID: repo.ID}, repo.NumForks)
|
||||
if repo.IsFork {
|
||||
AssertExistsAndLoadBean(t, &Repository{ID: repo.ForkID})
|
||||
}
|
||||
|
||||
actual := getCount(t, x.Where("is_pull=?", false), &Issue{RepoID: repo.ID})
|
||||
actual := getCount(t, x.Where("Mode<>?", RepoWatchModeDont), &Watch{RepoID: repo.ID})
|
||||
assert.EqualValues(t, repo.NumWatches, actual,
|
||||
"Unexpected number of watches for repo %+v", repo)
|
||||
|
||||
actual = getCount(t, x.Where("is_pull=?", false), &Issue{RepoID: repo.ID})
|
||||
assert.EqualValues(t, repo.NumIssues, actual,
|
||||
"Unexpected number of issues for repo %+v", repo)
|
||||
|
||||
@@ -164,3 +168,130 @@ func (action *Action) checkForConsistency(t *testing.T) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: action.RepoID}).(*Repository)
|
||||
assert.Equal(t, repo.IsPrivate, action.IsPrivate, "action: %+v", action)
|
||||
}
|
||||
|
||||
// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
|
||||
func CountOrphanedLabels() (int64, error) {
|
||||
noref, err := x.Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count("label.id")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
norepo, err := x.Table("label").
|
||||
Join("LEFT", "repository", "label.repo_id=repository.id").
|
||||
Where(builder.IsNull{"repository.id"}).And(builder.Gt{"label.repo_id": 0}).
|
||||
Count("id")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
noorg, err := x.Table("label").
|
||||
Join("LEFT", "`user`", "label.org_id=`user`.id").
|
||||
Where(builder.IsNull{"`user`.id"}).And(builder.Gt{"label.org_id": 0}).
|
||||
Count("id")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return noref + norepo + noorg, nil
|
||||
}
|
||||
|
||||
// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
|
||||
func DeleteOrphanedLabels() error {
|
||||
// delete labels with no reference
|
||||
if _, err := x.Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete labels with none existing repos
|
||||
if _, err := x.In("id", builder.Select("label.id").From("label").
|
||||
Join("LEFT", "repository", "label.repo_id=repository.id").
|
||||
Where(builder.IsNull{"repository.id"}).And(builder.Gt{"label.repo_id": 0})).
|
||||
Delete(Label{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete labels with none existing orgs
|
||||
if _, err := x.In("id", builder.Select("label.id").From("label").
|
||||
Join("LEFT", "`user`", "label.org_id=`user`.id").
|
||||
Where(builder.IsNull{"`user`.id"}).And(builder.Gt{"label.org_id": 0})).
|
||||
Delete(Label{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountOrphanedIssues count issues without a repo
|
||||
func CountOrphanedIssues() (int64, error) {
|
||||
return x.Table("issue").
|
||||
Join("LEFT", "repository", "issue.repo_id=repository.id").
|
||||
Where(builder.IsNull{"repository.id"}).
|
||||
Count("id")
|
||||
}
|
||||
|
||||
// DeleteOrphanedIssues delete issues without a repo
|
||||
func DeleteOrphanedIssues() error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ids []int64
|
||||
|
||||
if err := sess.Table("issue").Distinct("issue.repo_id").
|
||||
Join("LEFT", "repository", "issue.repo_id=repository.id").
|
||||
Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
|
||||
Find(&ids); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var attachmentPaths []string
|
||||
for i := range ids {
|
||||
paths, err := deleteIssuesByRepoID(sess, ids[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attachmentPaths = append(attachmentPaths, paths...)
|
||||
}
|
||||
|
||||
if err := sess.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove issue attachment files.
|
||||
for i := range attachmentPaths {
|
||||
removeAllWithNotice(x, "Delete issue attachment", attachmentPaths[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountOrphanedObjects count subjects with have no existing refobject anymore
|
||||
func CountOrphanedObjects(subject, refobject, joinCond string) (int64, error) {
|
||||
return x.Table("`"+subject+"`").
|
||||
Join("LEFT", refobject, joinCond).
|
||||
Where(builder.IsNull{"`" + refobject + "`.id"}).
|
||||
Count("id")
|
||||
}
|
||||
|
||||
// DeleteOrphanedObjects delete subjects with have no existing refobject anymore
|
||||
func DeleteOrphanedObjects(subject, refobject, joinCond string) error {
|
||||
_, err := x.In("id", builder.Select("`"+subject+"`.id").
|
||||
From("`"+subject+"`").
|
||||
Join("LEFT", "`"+refobject+"`", joinCond).
|
||||
Where(builder.IsNull{"`" + refobject + "`.id"})).
|
||||
Delete("`" + subject + "`")
|
||||
return err
|
||||
}
|
||||
|
||||
// CountNullArchivedRepository counts the number of repositories with is_archived is null
|
||||
func CountNullArchivedRepository() (int64, error) {
|
||||
return x.Where(builder.IsNull{"is_archived"}).Count(new(Repository))
|
||||
}
|
||||
|
||||
// FixNullArchivedRepository sets is_archived to false where it is null
|
||||
func FixNullArchivedRepository() (int64, error) {
|
||||
return x.Where(builder.IsNull{"is_archived"}).Cols("is_archived").Update(&Repository{
|
||||
IsArchived: false,
|
||||
})
|
||||
}
|
||||
|
||||
+14
-1
@@ -4,6 +4,12 @@
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// DBContext represents a db context
|
||||
type DBContext struct {
|
||||
e Engine
|
||||
@@ -17,7 +23,7 @@ func DefaultDBContext() DBContext {
|
||||
// Committer represents an interface to Commit or Close the dbcontext
|
||||
type Committer interface {
|
||||
Commit() error
|
||||
Close()
|
||||
Close() error
|
||||
}
|
||||
|
||||
// TxDBContext represents a transaction DBContext
|
||||
@@ -53,3 +59,10 @@ func WithTx(f func(ctx DBContext) error) error {
|
||||
sess.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate iterates the databases and doing something
|
||||
func Iterate(ctx DBContext, tableBean interface{}, cond builder.Cond, fun func(idx int, bean interface{}) error) error {
|
||||
return ctx.e.Where(cond).
|
||||
BufferSize(setting.Database.IterateBufferSize).
|
||||
Iterate(tableBean, fun)
|
||||
}
|
||||
|
||||
+299
-5
@@ -56,6 +56,21 @@ func (err ErrNamePatternNotAllowed) Error() string {
|
||||
return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
|
||||
}
|
||||
|
||||
// ErrNameCharsNotAllowed represents a "character not allowed in name" error.
|
||||
type ErrNameCharsNotAllowed struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// IsErrNameCharsNotAllowed checks if an error is an ErrNameCharsNotAllowed.
|
||||
func IsErrNameCharsNotAllowed(err error) bool {
|
||||
_, ok := err.(ErrNameCharsNotAllowed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNameCharsNotAllowed) Error() string {
|
||||
return fmt.Sprintf("User name is invalid [%s]: must be valid alpha or numeric or dash(-_) or dot characters", err.Name)
|
||||
}
|
||||
|
||||
// ErrSSHDisabled represents an "SSH disabled" error.
|
||||
type ErrSSHDisabled struct {
|
||||
}
|
||||
@@ -70,6 +85,28 @@ func (err ErrSSHDisabled) Error() string {
|
||||
return "SSH is disabled"
|
||||
}
|
||||
|
||||
// ErrCancelled represents an error due to context cancellation
|
||||
type ErrCancelled struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// IsErrCancelled checks if an error is a ErrCancelled.
|
||||
func IsErrCancelled(err error) bool {
|
||||
_, ok := err.(ErrCancelled)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCancelled) Error() string {
|
||||
return "Cancelled: " + err.Message
|
||||
}
|
||||
|
||||
// ErrCancelledf returns an ErrCancelled for the provided format and args
|
||||
func ErrCancelledf(format string, args ...interface{}) error {
|
||||
return ErrCancelled{
|
||||
fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
|
||||
// ____ ___
|
||||
// | | \______ ___________
|
||||
// | | / ___// __ \_ __ \
|
||||
@@ -212,7 +249,7 @@ func IsErrUserNotAllowedCreateOrg(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrUserNotAllowedCreateOrg) Error() string {
|
||||
return fmt.Sprintf("user is not allowed to create organizations")
|
||||
return "user is not allowed to create organizations"
|
||||
}
|
||||
|
||||
// ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error.
|
||||
@@ -546,7 +583,7 @@ func IsErrAccessTokenEmpty(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrAccessTokenEmpty) Error() string {
|
||||
return fmt.Sprintf("access token is empty")
|
||||
return "access token is empty"
|
||||
}
|
||||
|
||||
// ________ .__ __ .__
|
||||
@@ -900,6 +937,25 @@ func (err ErrFilePathInvalid) Error() string {
|
||||
return fmt.Sprintf("path is invalid [path: %s]", err.Path)
|
||||
}
|
||||
|
||||
// ErrFilePathProtected represents a "FilePathProtected" kind of error.
|
||||
type ErrFilePathProtected struct {
|
||||
Message string
|
||||
Path string
|
||||
}
|
||||
|
||||
// IsErrFilePathProtected checks if an error is an ErrFilePathProtected.
|
||||
func IsErrFilePathProtected(err error) bool {
|
||||
_, ok := err.(ErrFilePathProtected)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrFilePathProtected) Error() string {
|
||||
if err.Message != "" {
|
||||
return err.Message
|
||||
}
|
||||
return fmt.Sprintf("path is protected and can not be changed [path: %s]", err.Path)
|
||||
}
|
||||
|
||||
// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo.
|
||||
type ErrUserDoesNotHaveAccessToRepo struct {
|
||||
UserID int64
|
||||
@@ -916,6 +972,22 @@ func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
|
||||
return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
|
||||
}
|
||||
|
||||
// ErrWontSign explains the first reason why a commit would not be signed
|
||||
// There may be other reasons - this is just the first reason found
|
||||
type ErrWontSign struct {
|
||||
Reason signingMode
|
||||
}
|
||||
|
||||
func (e *ErrWontSign) Error() string {
|
||||
return fmt.Sprintf("wont sign: %s", e.Reason)
|
||||
}
|
||||
|
||||
// IsErrWontSign checks if an error is a ErrWontSign
|
||||
func IsErrWontSign(err error) bool {
|
||||
_, ok := err.(*ErrWontSign)
|
||||
return ok
|
||||
}
|
||||
|
||||
// __________ .__
|
||||
// \______ \____________ ____ ____ | |__
|
||||
// | | _/\_ __ \__ \ / \_/ ___\| | \
|
||||
@@ -923,6 +995,21 @@ func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
|
||||
// |______ / |__| (____ /___| /\___ >___| /
|
||||
// \/ \/ \/ \/ \/
|
||||
|
||||
// ErrBranchDoesNotExist represents an error that branch with such name does not exist.
|
||||
type ErrBranchDoesNotExist struct {
|
||||
BranchName string
|
||||
}
|
||||
|
||||
// IsErrBranchDoesNotExist checks if an error is an ErrBranchDoesNotExist.
|
||||
func IsErrBranchDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrBranchDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrBranchDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("branch does not exist [name: %s]", err.BranchName)
|
||||
}
|
||||
|
||||
// ErrBranchAlreadyExists represents an error that branch with such name already exists.
|
||||
type ErrBranchAlreadyExists struct {
|
||||
BranchName string
|
||||
@@ -953,6 +1040,22 @@ func (err ErrBranchNameConflict) Error() string {
|
||||
return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName)
|
||||
}
|
||||
|
||||
// ErrBranchesEqual represents an error that branch name conflicts with other branch.
|
||||
type ErrBranchesEqual struct {
|
||||
BaseBranchName string
|
||||
HeadBranchName string
|
||||
}
|
||||
|
||||
// IsErrBranchesEqual checks if an error is an ErrBranchesEqual.
|
||||
func IsErrBranchesEqual(err error) bool {
|
||||
_, ok := err.(ErrBranchesEqual)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrBranchesEqual) Error() string {
|
||||
return fmt.Sprintf("branches are equal [head: %sm base: %s]", err.HeadBranchName, err.BaseBranchName)
|
||||
}
|
||||
|
||||
// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
|
||||
type ErrNotAllowedToMerge struct {
|
||||
Reason string
|
||||
@@ -1041,7 +1144,7 @@ func IsErrSHAOrCommitIDNotProvided(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrSHAOrCommitIDNotProvided) Error() string {
|
||||
return fmt.Sprintf("a SHA or commmit ID must be proved when updating a file")
|
||||
return "a SHA or commmit ID must be proved when updating a file"
|
||||
}
|
||||
|
||||
// __ __ ___. .__ __
|
||||
@@ -1090,6 +1193,23 @@ func (err ErrIssueNotExist) Error() string {
|
||||
return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
|
||||
}
|
||||
|
||||
// ErrIssueIsClosed represents a "IssueIsClosed" kind of error.
|
||||
type ErrIssueIsClosed struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
Index int64
|
||||
}
|
||||
|
||||
// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist.
|
||||
func IsErrIssueIsClosed(err error) bool {
|
||||
_, ok := err.(ErrIssueIsClosed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIssueIsClosed) Error() string {
|
||||
return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
|
||||
}
|
||||
|
||||
// ErrIssueLabelTemplateLoad represents a "ErrIssueLabelTemplateLoad" kind of error.
|
||||
type ErrIssueLabelTemplateLoad struct {
|
||||
TemplateFile string
|
||||
@@ -1121,6 +1241,68 @@ func (err ErrNewIssueInsert) Error() string {
|
||||
return err.OriginalError.Error()
|
||||
}
|
||||
|
||||
// ErrIssueWasClosed is used when close a closed issue
|
||||
type ErrIssueWasClosed struct {
|
||||
ID int64
|
||||
Index int64
|
||||
}
|
||||
|
||||
// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed.
|
||||
func IsErrIssueWasClosed(err error) bool {
|
||||
_, ok := err.(ErrIssueWasClosed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIssueWasClosed) Error() string {
|
||||
return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ErrForbiddenIssueReaction is used when a forbidden reaction was try to created
|
||||
type ErrForbiddenIssueReaction struct {
|
||||
Reaction string
|
||||
}
|
||||
|
||||
// IsErrForbiddenIssueReaction checks if an error is a ErrForbiddenIssueReaction.
|
||||
func IsErrForbiddenIssueReaction(err error) bool {
|
||||
_, ok := err.(ErrForbiddenIssueReaction)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrForbiddenIssueReaction) Error() string {
|
||||
return fmt.Sprintf("'%s' is not an allowed reaction", err.Reaction)
|
||||
}
|
||||
|
||||
// ErrReactionAlreadyExist is used when a existing reaction was try to created
|
||||
type ErrReactionAlreadyExist struct {
|
||||
Reaction string
|
||||
}
|
||||
|
||||
// IsErrReactionAlreadyExist checks if an error is a ErrReactionAlreadyExist.
|
||||
func IsErrReactionAlreadyExist(err error) bool {
|
||||
_, ok := err.(ErrReactionAlreadyExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrReactionAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("reaction '%s' already exists", err.Reaction)
|
||||
}
|
||||
|
||||
// __________ .__ .__ __________ __
|
||||
// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_
|
||||
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
|
||||
@@ -1206,6 +1388,83 @@ func (err ErrInvalidMergeStyle) Error() string {
|
||||
err.ID, err.Style)
|
||||
}
|
||||
|
||||
// ErrMergeConflicts represents an error if merging fails with a conflict
|
||||
type ErrMergeConflicts struct {
|
||||
Style MergeStyle
|
||||
StdOut string
|
||||
StdErr string
|
||||
Err error
|
||||
}
|
||||
|
||||
// IsErrMergeConflicts checks if an error is a ErrMergeConflicts.
|
||||
func IsErrMergeConflicts(err error) bool {
|
||||
_, ok := err.(ErrMergeConflicts)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrMergeConflicts) Error() string {
|
||||
return fmt.Sprintf("Merge Conflict Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
|
||||
}
|
||||
|
||||
// ErrMergeUnrelatedHistories represents an error if merging fails due to unrelated histories
|
||||
type ErrMergeUnrelatedHistories struct {
|
||||
Style MergeStyle
|
||||
StdOut string
|
||||
StdErr string
|
||||
Err error
|
||||
}
|
||||
|
||||
// IsErrMergeUnrelatedHistories checks if an error is a ErrMergeUnrelatedHistories.
|
||||
func IsErrMergeUnrelatedHistories(err error) bool {
|
||||
_, ok := err.(ErrMergeUnrelatedHistories)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrMergeUnrelatedHistories) Error() string {
|
||||
return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
|
||||
}
|
||||
|
||||
// ErrRebaseConflicts represents an error if rebase fails with a conflict
|
||||
type ErrRebaseConflicts struct {
|
||||
Style MergeStyle
|
||||
CommitSHA string
|
||||
StdOut string
|
||||
StdErr string
|
||||
Err error
|
||||
}
|
||||
|
||||
// IsErrRebaseConflicts checks if an error is a ErrRebaseConflicts.
|
||||
func IsErrRebaseConflicts(err error) bool {
|
||||
_, ok := err.(ErrRebaseConflicts)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrRebaseConflicts) Error() string {
|
||||
return fmt.Sprintf("Rebase Error: %v: Whilst Rebasing: %s\n%s\n%s", err.Err, err.CommitSHA, err.StdErr, err.StdOut)
|
||||
}
|
||||
|
||||
// ErrPullRequestHasMerged represents a "PullRequestHasMerged"-error
|
||||
type ErrPullRequestHasMerged struct {
|
||||
ID int64
|
||||
IssueID int64
|
||||
HeadRepoID int64
|
||||
BaseRepoID int64
|
||||
HeadBranch string
|
||||
BaseBranch string
|
||||
}
|
||||
|
||||
// IsErrPullRequestHasMerged checks if an error is a ErrPullRequestHasMerged.
|
||||
func IsErrPullRequestHasMerged(err error) bool {
|
||||
_, ok := err.(ErrPullRequestHasMerged)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Error does pretty-printing :D
|
||||
func (err ErrPullRequestHasMerged) Error() string {
|
||||
return fmt.Sprintf("pull request has merged [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)
|
||||
}
|
||||
|
||||
// _________ __
|
||||
// \_ ___ \ ____ _____ _____ ____ _____/ |_
|
||||
// / \ \/ / _ \ / \ / \_/ __ \ / \ __\
|
||||
@@ -1280,10 +1539,41 @@ func (err ErrTrackedTimeNotExist) Error() string {
|
||||
// |_______ (____ /___ /\___ >____/
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
|
||||
type ErrRepoLabelNotExist struct {
|
||||
LabelID int64
|
||||
RepoID int64
|
||||
}
|
||||
|
||||
// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
|
||||
func IsErrRepoLabelNotExist(err error) bool {
|
||||
_, ok := err.(ErrRepoLabelNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrRepoLabelNotExist) Error() string {
|
||||
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
|
||||
}
|
||||
|
||||
// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
|
||||
type ErrOrgLabelNotExist struct {
|
||||
LabelID int64
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
|
||||
func IsErrOrgLabelNotExist(err error) bool {
|
||||
_, ok := err.(ErrOrgLabelNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrOrgLabelNotExist) Error() string {
|
||||
return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
|
||||
}
|
||||
|
||||
// ErrLabelNotExist represents a "LabelNotExist" kind of error.
|
||||
type ErrLabelNotExist struct {
|
||||
LabelID int64
|
||||
RepoID int64
|
||||
}
|
||||
|
||||
// IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
|
||||
@@ -1293,7 +1583,7 @@ func IsErrLabelNotExist(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrLabelNotExist) Error() string {
|
||||
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
|
||||
return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
|
||||
}
|
||||
|
||||
// _____ .__.__ __
|
||||
@@ -1307,6 +1597,7 @@ func (err ErrLabelNotExist) Error() string {
|
||||
type ErrMilestoneNotExist struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
Name string
|
||||
}
|
||||
|
||||
// IsErrMilestoneNotExist checks if an error is a ErrMilestoneNotExist.
|
||||
@@ -1316,6 +1607,9 @@ func IsErrMilestoneNotExist(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrMilestoneNotExist) Error() string {
|
||||
if len(err.Name) > 0 {
|
||||
return fmt.Sprintf("milestone does not exist [name: %s, repo_id: %d]", err.Name, err.RepoID)
|
||||
}
|
||||
return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID)
|
||||
}
|
||||
|
||||
|
||||
@@ -177,5 +177,12 @@ func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, us
|
||||
return err
|
||||
}
|
||||
|
||||
return UpdateReleasesMigrationsByType(tp, externalUserID, userID)
|
||||
if err := UpdateReleasesMigrationsByType(tp, externalUserID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
return UpdateReviewsMigrationsByType(tp, externalUserID, userID)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetYamlFixturesAccess returns a string containing the contents
|
||||
// for the access table, as recalculated using repo.RecalculateAccesses()
|
||||
func GetYamlFixturesAccess() (string, error) {
|
||||
|
||||
repos := make([]*Repository, 0, 50)
|
||||
if err := x.Find(&repos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
repo.MustOwner()
|
||||
if err := repo.RecalculateAccesses(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
accesses := make([]*Access, 0, 200)
|
||||
if err := x.OrderBy("user_id, repo_id").Find(&accesses); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i, a := range accesses {
|
||||
fmt.Fprintf(&b, "-\n")
|
||||
fmt.Fprintf(&b, " id: %d\n", i+1)
|
||||
fmt.Fprintf(&b, " user_id: %d\n", a.UserID)
|
||||
fmt.Fprintf(&b, " repo_id: %d\n", a.RepoID)
|
||||
fmt.Fprintf(&b, " mode: %d\n", a.Mode)
|
||||
fmt.Fprintf(&b, "\n")
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFixtureGeneration(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
test := func(gen func() (string, error), name string) {
|
||||
expected, err := gen()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
bytes, err := ioutil.ReadFile(filepath.Join(fixturesDir, name+".yml"))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
data := string(util.NormalizeEOL(bytes))
|
||||
assert.True(t, data == expected, "Differences detected for %s.yml", name)
|
||||
}
|
||||
|
||||
test(GetYamlFixturesAccess, "access")
|
||||
}
|
||||
+85
-36
@@ -2,76 +2,125 @@
|
||||
id: 1
|
||||
user_id: 2
|
||||
repo_id: 3
|
||||
mode: 2 # write
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 2
|
||||
user_id: 4
|
||||
repo_id: 4
|
||||
mode: 2 # write
|
||||
user_id: 2
|
||||
repo_id: 5
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 3
|
||||
user_id: 4
|
||||
repo_id: 3
|
||||
mode: 2 # write
|
||||
user_id: 2
|
||||
repo_id: 24
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 4
|
||||
user_id: 15
|
||||
repo_id: 22
|
||||
mode: 2 # write
|
||||
user_id: 2
|
||||
repo_id: 32
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 5
|
||||
user_id: 15
|
||||
repo_id: 21
|
||||
mode: 2 # write
|
||||
user_id: 4
|
||||
repo_id: 3
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 6
|
||||
user_id: 15
|
||||
repo_id: 23
|
||||
mode: 4 # owner
|
||||
user_id: 4
|
||||
repo_id: 4
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 7
|
||||
user_id: 15
|
||||
repo_id: 24
|
||||
mode: 4 # owner
|
||||
user_id: 4
|
||||
repo_id: 40
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 8
|
||||
user_id: 18
|
||||
repo_id: 23
|
||||
mode: 4 # owner
|
||||
user_id: 15
|
||||
repo_id: 21
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 9
|
||||
user_id: 18
|
||||
repo_id: 24
|
||||
mode: 4 # owner
|
||||
user_id: 15
|
||||
repo_id: 22
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 10
|
||||
user_id: 18
|
||||
repo_id: 22
|
||||
mode: 2 # write
|
||||
user_id: 15
|
||||
repo_id: 23
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 11
|
||||
user_id: 18
|
||||
repo_id: 21
|
||||
mode: 2 # write
|
||||
user_id: 15
|
||||
repo_id: 24
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 12
|
||||
user_id: 20
|
||||
repo_id: 27
|
||||
mode: 4 # owner
|
||||
|
||||
user_id: 15
|
||||
repo_id: 32
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 13
|
||||
user_id: 18
|
||||
repo_id: 21
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 14
|
||||
user_id: 18
|
||||
repo_id: 22
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 15
|
||||
user_id: 18
|
||||
repo_id: 23
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 16
|
||||
user_id: 18
|
||||
repo_id: 24
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 17
|
||||
user_id: 20
|
||||
repo_id: 24
|
||||
mode: 1
|
||||
|
||||
-
|
||||
id: 18
|
||||
user_id: 20
|
||||
repo_id: 27
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 19
|
||||
user_id: 20
|
||||
repo_id: 28
|
||||
mode: 4 # owner
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 20
|
||||
user_id: 29
|
||||
repo_id: 4
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 21
|
||||
user_id: 29
|
||||
repo_id: 24
|
||||
mode: 1
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
-
|
||||
id: 2
|
||||
uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12
|
||||
issue_id: 1
|
||||
issue_id: 4
|
||||
comment_id: 0
|
||||
name: attach2
|
||||
download_count: 1
|
||||
@@ -69,3 +69,27 @@
|
||||
name: attach1
|
||||
download_count: 0
|
||||
created_unix: 946684800
|
||||
|
||||
-
|
||||
id: 9
|
||||
uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19
|
||||
release_id: 1
|
||||
name: attach1
|
||||
download_count: 0
|
||||
created_unix: 946684800
|
||||
|
||||
-
|
||||
id: 10
|
||||
uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20
|
||||
uploader_id: 8
|
||||
name: attach1
|
||||
download_count: 0
|
||||
created_unix: 946684800
|
||||
|
||||
-
|
||||
id: 11
|
||||
uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a21
|
||||
release_id: 2
|
||||
name: attach1
|
||||
download_count: 0
|
||||
created_unix: 946684800
|
||||
|
||||
@@ -14,4 +14,34 @@
|
||||
id: 3
|
||||
repo_id: 40
|
||||
user_id: 4
|
||||
mode: 2 # write
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 4
|
||||
repo_id: 4
|
||||
user_id: 29
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 5
|
||||
repo_id: 21
|
||||
user_id: 15
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 6
|
||||
repo_id: 21
|
||||
user_id: 18
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 7
|
||||
repo_id: 22
|
||||
user_id: 15
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 8
|
||||
repo_id: 22
|
||||
user_id: 18
|
||||
mode: 2 # write
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
issue_id: 1 # in repo_id 1
|
||||
content: "good work!"
|
||||
created_unix: 946684811
|
||||
updated_unix: 946684811
|
||||
-
|
||||
id: 3
|
||||
type: 0 # comment
|
||||
@@ -20,6 +21,7 @@
|
||||
issue_id: 1 # in repo_id 1
|
||||
content: "meh..."
|
||||
created_unix: 946684812
|
||||
updated_unix: 946684812
|
||||
-
|
||||
id: 4
|
||||
type: 21 # code comment
|
||||
@@ -63,4 +65,4 @@
|
||||
review_id: 10
|
||||
tree_path: "README.md"
|
||||
created_unix: 946684812
|
||||
invalidated: true
|
||||
invalidated: true
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
poster_id: 1
|
||||
name: issue3
|
||||
content: content for the third issue
|
||||
milestone_id: 3
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
created_unix: 946684820
|
||||
@@ -96,4 +97,41 @@
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
created_unix: 946684820
|
||||
updated_unix: 978307180
|
||||
updated_unix: 978307180
|
||||
|
||||
-
|
||||
id: 9
|
||||
repo_id: 48
|
||||
index: 1
|
||||
poster_id: 11
|
||||
name: pr1
|
||||
content: a pull request
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
created_unix: 946684820
|
||||
updated_unix: 978307180
|
||||
|
||||
-
|
||||
id: 10
|
||||
repo_id: 42
|
||||
index: 1
|
||||
poster_id: 500
|
||||
name: issue from deleted account
|
||||
content: content from deleted account
|
||||
is_closed: false
|
||||
is_pull: false
|
||||
created_unix: 946684830
|
||||
updated_unix: 999307200
|
||||
deadline_unix: 1019307200
|
||||
|
||||
-
|
||||
id: 11
|
||||
repo_id: 1
|
||||
index: 5
|
||||
poster_id: 1
|
||||
name: pull5
|
||||
content: content for the a pull request
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
created_unix: 1579194806
|
||||
updated_unix: 1579194806
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
id: 2
|
||||
assignee_id: 1
|
||||
issue_id: 6
|
||||
-
|
||||
id: 3
|
||||
assignee_id: 2
|
||||
issue_id: 6
|
||||
|
||||
@@ -12,3 +12,8 @@
|
||||
id: 3
|
||||
issue_id: 2
|
||||
label_id: 1
|
||||
|
||||
-
|
||||
id: 4
|
||||
issue_id: 2
|
||||
label_id: 4
|
||||
|
||||
@@ -13,3 +13,19 @@
|
||||
is_watching: false
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
|
||||
-
|
||||
id: 3
|
||||
user_id: 2
|
||||
issue_id: 7
|
||||
is_watching: true
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
|
||||
-
|
||||
id: 4
|
||||
user_id: 1
|
||||
issue_id: 7
|
||||
is_watching: false
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
-
|
||||
id: 1
|
||||
repo_id: 1
|
||||
org_id: 0
|
||||
name: label1
|
||||
color: '#abcdef'
|
||||
num_issues: 2
|
||||
@@ -9,7 +10,26 @@
|
||||
-
|
||||
id: 2
|
||||
repo_id: 1
|
||||
org_id: 0
|
||||
name: label2
|
||||
color: '#000000'
|
||||
num_issues: 1
|
||||
num_closed_issues: 1
|
||||
-
|
||||
id: 3
|
||||
repo_id: 0
|
||||
org_id: 3
|
||||
name: orglabel3
|
||||
color: '#abcdef'
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
|
||||
-
|
||||
id: 4
|
||||
repo_id: 0
|
||||
org_id: 3
|
||||
name: orglabel4
|
||||
color: '#000000'
|
||||
num_issues: 1
|
||||
num_closed_issues: 0
|
||||
|
||||
|
||||
@@ -20,4 +20,12 @@
|
||||
name: milestone3
|
||||
content: content3
|
||||
is_closed: true
|
||||
num_issues: 1
|
||||
|
||||
-
|
||||
id: 4
|
||||
repo_id: 42
|
||||
name: milestone of repo42
|
||||
content: content random
|
||||
is_closed: false
|
||||
num_issues: 0
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
updated_by: 2
|
||||
issue_id: 1
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
updated_unix: 946684820
|
||||
|
||||
-
|
||||
id: 2
|
||||
@@ -17,8 +17,8 @@
|
||||
source: 1 # issue
|
||||
updated_by: 1
|
||||
issue_id: 2
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
created_unix: 946685800
|
||||
updated_unix: 946685820
|
||||
|
||||
-
|
||||
id: 3
|
||||
@@ -27,9 +27,9 @@
|
||||
status: 3 # pinned
|
||||
source: 1 # issue
|
||||
updated_by: 1
|
||||
issue_id: 2
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
issue_id: 3
|
||||
created_unix: 946686800
|
||||
updated_unix: 946686800
|
||||
|
||||
-
|
||||
id: 4
|
||||
@@ -38,6 +38,17 @@
|
||||
status: 1 # unread
|
||||
source: 1 # issue
|
||||
updated_by: 1
|
||||
issue_id: 2
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
issue_id: 5
|
||||
created_unix: 946687800
|
||||
updated_unix: 946687800
|
||||
|
||||
-
|
||||
id: 5
|
||||
user_id: 2
|
||||
repo_id: 2
|
||||
status: 1 # unread
|
||||
source: 1 # issue
|
||||
updated_by: 5
|
||||
issue_id: 4
|
||||
created_unix: 946688800
|
||||
updated_unix: 946688820
|
||||
|
||||
@@ -45,3 +45,21 @@
|
||||
uid: 24
|
||||
org_id: 25
|
||||
is_public: true
|
||||
|
||||
-
|
||||
id: 9
|
||||
uid: 28
|
||||
org_id: 3
|
||||
is_public: true
|
||||
|
||||
-
|
||||
id: 10
|
||||
uid: 28
|
||||
org_id: 6
|
||||
is_public: true
|
||||
|
||||
-
|
||||
id: 11
|
||||
uid: 29
|
||||
org_id: 17
|
||||
is_public: true
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
[] # empty
|
||||
-
|
||||
id: 1
|
||||
owner_id: 2
|
||||
name: user2@localhost
|
||||
fingerprint: "SHA256:M3iiFbqQKgLxi+WAoRa38ZVQ9ktdfau2sOu9xuPb9ew"
|
||||
content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user2@localhost"
|
||||
mode: 2
|
||||
type: 1
|
||||
created_unix: 1559593109
|
||||
updated_unix: 1565224552
|
||||
login_source_id: 0
|
||||
@@ -8,7 +8,7 @@
|
||||
base_repo_id: 1
|
||||
head_branch: branch1
|
||||
base_branch: master
|
||||
merge_base: 1234567890abcdef
|
||||
merge_base: 4a357436d925b5c974181ff12a994538ddc5a269
|
||||
has_merged: true
|
||||
merger_id: 2
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
base_repo_id: 1
|
||||
head_branch: branch2
|
||||
base_branch: master
|
||||
merge_base: fedcba9876543210
|
||||
merge_base: 4a357436d925b5c974181ff12a994538ddc5a269
|
||||
has_merged: false
|
||||
|
||||
-
|
||||
@@ -36,4 +36,30 @@
|
||||
head_branch: branch2
|
||||
base_branch: master
|
||||
merge_base: 0abcb056019adb83
|
||||
has_merged: false
|
||||
has_merged: false
|
||||
|
||||
-
|
||||
id: 4
|
||||
type: 0 # gitea pull request
|
||||
status: 2 # mergable
|
||||
issue_id: 9
|
||||
index: 1
|
||||
head_repo_id: 48
|
||||
base_repo_id: 48
|
||||
head_branch: branch1
|
||||
base_branch: master
|
||||
merge_base: abcdef1234567890
|
||||
has_merged: false
|
||||
|
||||
-
|
||||
id: 5 # this PR is outdated (one commit behind branch1 )
|
||||
type: 0 # gitea pull request
|
||||
status: 2 # mergable
|
||||
issue_id: 11
|
||||
index: 5
|
||||
head_repo_id: 1
|
||||
base_repo_id: 1
|
||||
head_branch: pr-to-update
|
||||
base_branch: branch1
|
||||
merge_base: 1234567890abcdef
|
||||
has_merged: false
|
||||
|
||||
@@ -1 +1,39 @@
|
||||
[] # empty
|
||||
-
|
||||
id: 1 #issue reaction
|
||||
type: zzz # not allowed reaction (added before allowed reaction list has changed)
|
||||
issue_id: 1
|
||||
comment_id: 0
|
||||
user_id: 2
|
||||
created_unix: 1573248001
|
||||
|
||||
-
|
||||
id: 2 #issue reaction
|
||||
type: zzz # not allowed reaction (added before allowed reaction list has changed)
|
||||
issue_id: 1
|
||||
comment_id: 0
|
||||
user_id: 1
|
||||
created_unix: 1573248002
|
||||
|
||||
-
|
||||
id: 3 #issue reaction
|
||||
type: eyes # allowed reaction
|
||||
issue_id: 1
|
||||
comment_id: 0
|
||||
user_id: 2
|
||||
created_unix: 1573248003
|
||||
|
||||
-
|
||||
id: 4 #comment reaction
|
||||
type: laugh # allowed reaction
|
||||
issue_id: 1
|
||||
comment_id: 2
|
||||
user_id: 2
|
||||
created_unix: 1573248004
|
||||
|
||||
-
|
||||
id: 5 #comment reaction
|
||||
type: laugh # allowed reaction
|
||||
issue_id: 1
|
||||
comment_id: 2
|
||||
user_id: 1
|
||||
created_unix: 1573248005
|
||||
|
||||
@@ -1 +1,29 @@
|
||||
[] # empty
|
||||
-
|
||||
id: 1
|
||||
repo_id: 1
|
||||
publisher_id: 2
|
||||
tag_name: "v1.1"
|
||||
lower_tag_name: "v1.1"
|
||||
target: "master"
|
||||
title: "testing-release"
|
||||
sha1: "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
||||
num_commits: 10
|
||||
is_draft: false
|
||||
is_prerelease: false
|
||||
is_tag: false
|
||||
created_unix: 946684800
|
||||
|
||||
-
|
||||
id: 2
|
||||
repo_id: 40
|
||||
publisher_id: 2
|
||||
tag_name: "v1.1"
|
||||
lower_tag_name: "v1.1"
|
||||
target: "master"
|
||||
title: "testing-release"
|
||||
sha1: "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
||||
num_commits: 10
|
||||
is_draft: false
|
||||
is_prerelease: false
|
||||
is_tag: false
|
||||
created_unix: 946684800
|
||||
|
||||
@@ -438,3 +438,79 @@
|
||||
type: 3
|
||||
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 64
|
||||
repo_id: 44
|
||||
type: 1
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 65
|
||||
repo_id: 45
|
||||
type: 1
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 66
|
||||
repo_id: 46
|
||||
type: 7
|
||||
config: "{\"ExternalTrackerURL\":\"https://tracker.com\",\"ExternalTrackerFormat\":\"https://tracker.com/{user}/{repo}/issues/{index}\",\"ExternalTrackerStyle\":\"\"}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 67
|
||||
repo_id: 47
|
||||
type: 7
|
||||
config: "{\"ExternalTrackerURL\":\"https://tracker.com\",\"ExternalTrackerFormat\":\"https://tracker.com/{user}/{repo}/issues/{index}\",\"ExternalTrackerStyle\":\"numeric\"}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 68
|
||||
repo_id: 48
|
||||
type: 7
|
||||
config: "{\"ExternalTrackerURL\":\"https://tracker.com\",\"ExternalTrackerFormat\":\"https://tracker.com/{user}/{repo}/issues/{index}\",\"ExternalTrackerStyle\":\"alphanumeric\"}"
|
||||
created_unix: 946684810
|
||||
-
|
||||
id: 69
|
||||
repo_id: 2
|
||||
type: 2
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 70
|
||||
repo_id: 5
|
||||
type: 4
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 71
|
||||
repo_id: 5
|
||||
type: 5
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 72
|
||||
repo_id: 5
|
||||
type: 1
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 73
|
||||
repo_id: 5
|
||||
type: 2
|
||||
config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 74
|
||||
repo_id: 5
|
||||
type: 3
|
||||
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
|
||||
created_unix: 946684810
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
-
|
||||
id: 1
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: repo1
|
||||
name: repo1
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_issues: 2
|
||||
num_closed_issues: 1
|
||||
num_pulls: 2
|
||||
num_pulls: 3
|
||||
num_closed_pulls: 0
|
||||
num_milestones: 3
|
||||
num_closed_milestones: 1
|
||||
num_watches: 3
|
||||
num_watches: 4
|
||||
status: 0
|
||||
|
||||
-
|
||||
id: 2
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: repo2
|
||||
name: repo2
|
||||
is_private: true
|
||||
@@ -30,6 +33,7 @@
|
||||
-
|
||||
id: 3
|
||||
owner_id: 3
|
||||
owner_name: user3
|
||||
lower_name: repo3
|
||||
name: repo3
|
||||
is_private: true
|
||||
@@ -43,6 +47,7 @@
|
||||
-
|
||||
id: 4
|
||||
owner_id: 5
|
||||
owner_name: user5
|
||||
lower_name: repo4
|
||||
name: repo4
|
||||
is_private: false
|
||||
@@ -56,6 +61,7 @@
|
||||
-
|
||||
id: 5
|
||||
owner_id: 3
|
||||
owner_name: user3
|
||||
lower_name: repo5
|
||||
name: repo5
|
||||
is_private: true
|
||||
@@ -70,6 +76,7 @@
|
||||
-
|
||||
id: 6
|
||||
owner_id: 10
|
||||
owner_name: user10
|
||||
lower_name: repo6
|
||||
name: repo6
|
||||
is_private: true
|
||||
@@ -83,6 +90,7 @@
|
||||
-
|
||||
id: 7
|
||||
owner_id: 10
|
||||
owner_name: user10
|
||||
lower_name: repo7
|
||||
name: repo7
|
||||
is_private: true
|
||||
@@ -96,6 +104,7 @@
|
||||
-
|
||||
id: 8
|
||||
owner_id: 10
|
||||
owner_name: user10
|
||||
lower_name: repo8
|
||||
name: repo8
|
||||
is_private: false
|
||||
@@ -109,6 +118,7 @@
|
||||
-
|
||||
id: 9
|
||||
owner_id: 11
|
||||
owner_name: user11
|
||||
lower_name: repo9
|
||||
name: repo9
|
||||
is_private: false
|
||||
@@ -122,6 +132,7 @@
|
||||
-
|
||||
id: 10
|
||||
owner_id: 12
|
||||
owner_name: user12
|
||||
lower_name: repo10
|
||||
name: repo10
|
||||
is_private: false
|
||||
@@ -137,6 +148,7 @@
|
||||
id: 11
|
||||
fork_id: 10
|
||||
owner_id: 13
|
||||
owner_name: user13
|
||||
lower_name: repo11
|
||||
name: repo11
|
||||
is_private: false
|
||||
@@ -150,6 +162,7 @@
|
||||
-
|
||||
id: 12
|
||||
owner_id: 14
|
||||
owner_name: user14
|
||||
lower_name: test_repo_12
|
||||
name: test_repo_12
|
||||
is_private: false
|
||||
@@ -163,6 +176,7 @@
|
||||
-
|
||||
id: 13
|
||||
owner_id: 14
|
||||
owner_name: user14
|
||||
lower_name: test_repo_13
|
||||
name: test_repo_13
|
||||
is_private: true
|
||||
@@ -176,6 +190,7 @@
|
||||
-
|
||||
id: 14
|
||||
owner_id: 14
|
||||
owner_name: user14
|
||||
lower_name: test_repo_14
|
||||
name: test_repo_14
|
||||
description: test_description_14
|
||||
@@ -190,6 +205,7 @@
|
||||
-
|
||||
id: 15
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: repo15
|
||||
name: repo15
|
||||
is_empty: true
|
||||
@@ -198,6 +214,7 @@
|
||||
-
|
||||
id: 16
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: repo16
|
||||
name: repo16
|
||||
is_private: true
|
||||
@@ -211,6 +228,7 @@
|
||||
-
|
||||
id: 17
|
||||
owner_id: 15
|
||||
owner_name: user15
|
||||
lower_name: big_test_public_1
|
||||
name: big_test_public_1
|
||||
is_private: false
|
||||
@@ -226,6 +244,7 @@
|
||||
-
|
||||
id: 18
|
||||
owner_id: 15
|
||||
owner_name: user15
|
||||
lower_name: big_test_public_2
|
||||
name: big_test_public_2
|
||||
is_private: false
|
||||
@@ -240,6 +259,7 @@
|
||||
-
|
||||
id: 19
|
||||
owner_id: 15
|
||||
owner_name: user15
|
||||
lower_name: big_test_private_1
|
||||
name: big_test_private_1
|
||||
is_private: true
|
||||
@@ -254,6 +274,7 @@
|
||||
-
|
||||
id: 20
|
||||
owner_id: 15
|
||||
owner_name: user15
|
||||
lower_name: big_test_private_2
|
||||
name: big_test_private_2
|
||||
is_private: true
|
||||
@@ -268,6 +289,7 @@
|
||||
-
|
||||
id: 21
|
||||
owner_id: 16
|
||||
owner_name: user16
|
||||
lower_name: big_test_public_3
|
||||
name: big_test_public_3
|
||||
is_private: false
|
||||
@@ -282,6 +304,7 @@
|
||||
-
|
||||
id: 22
|
||||
owner_id: 16
|
||||
owner_name: user16
|
||||
lower_name: big_test_private_3
|
||||
name: big_test_private_3
|
||||
is_private: true
|
||||
@@ -296,6 +319,7 @@
|
||||
-
|
||||
id: 23
|
||||
owner_id: 17
|
||||
owner_name: user17
|
||||
lower_name: big_test_public_4
|
||||
name: big_test_public_4
|
||||
is_private: false
|
||||
@@ -310,6 +334,7 @@
|
||||
-
|
||||
id: 24
|
||||
owner_id: 17
|
||||
owner_name: user17
|
||||
lower_name: big_test_private_4
|
||||
name: big_test_private_4
|
||||
is_private: true
|
||||
@@ -324,6 +349,7 @@
|
||||
-
|
||||
id: 25
|
||||
owner_id: 20
|
||||
owner_name: user20
|
||||
lower_name: big_test_public_mirror_5
|
||||
name: big_test_public_mirror_5
|
||||
is_private: false
|
||||
@@ -339,6 +365,7 @@
|
||||
-
|
||||
id: 26
|
||||
owner_id: 20
|
||||
owner_name: user20
|
||||
lower_name: big_test_private_mirror_5
|
||||
name: big_test_private_mirror_5
|
||||
is_private: true
|
||||
@@ -354,6 +381,7 @@
|
||||
-
|
||||
id: 27
|
||||
owner_id: 19
|
||||
owner_name: user19
|
||||
lower_name: big_test_public_mirror_6
|
||||
name: big_test_public_mirror_6
|
||||
is_private: false
|
||||
@@ -370,6 +398,7 @@
|
||||
-
|
||||
id: 28
|
||||
owner_id: 19
|
||||
owner_name: user19
|
||||
lower_name: big_test_private_mirror_6
|
||||
name: big_test_private_mirror_6
|
||||
is_private: true
|
||||
@@ -387,6 +416,7 @@
|
||||
id: 29
|
||||
fork_id: 27
|
||||
owner_id: 20
|
||||
owner_name: user20
|
||||
lower_name: big_test_public_fork_7
|
||||
name: big_test_public_fork_7
|
||||
is_private: false
|
||||
@@ -402,6 +432,7 @@
|
||||
id: 30
|
||||
fork_id: 28
|
||||
owner_id: 20
|
||||
owner_name: user20
|
||||
lower_name: big_test_private_fork_7
|
||||
name: big_test_private_fork_7
|
||||
is_private: true
|
||||
@@ -416,6 +447,7 @@
|
||||
-
|
||||
id: 31
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: repo20
|
||||
name: repo20
|
||||
num_stars: 0
|
||||
@@ -427,6 +459,7 @@
|
||||
-
|
||||
id: 32 # org public repo
|
||||
owner_id: 3
|
||||
owner_name: user3
|
||||
lower_name: repo21
|
||||
name: repo21
|
||||
is_private: false
|
||||
@@ -439,6 +472,7 @@
|
||||
-
|
||||
id: 33
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: utf8
|
||||
name: utf8
|
||||
is_private: false
|
||||
@@ -447,6 +481,7 @@
|
||||
-
|
||||
id: 34
|
||||
owner_id: 21
|
||||
owner_name: user21
|
||||
lower_name: golang
|
||||
name: golang
|
||||
is_private: false
|
||||
@@ -459,6 +494,7 @@
|
||||
-
|
||||
id: 35
|
||||
owner_id: 21
|
||||
owner_name: user21
|
||||
lower_name: graphql
|
||||
name: graphql
|
||||
is_private: false
|
||||
@@ -471,6 +507,7 @@
|
||||
-
|
||||
id: 36
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: commits_search_test
|
||||
name: commits_search_test
|
||||
is_private: false
|
||||
@@ -483,6 +520,7 @@
|
||||
-
|
||||
id: 37
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: git_hooks_test
|
||||
name: git_hooks_test
|
||||
is_private: false
|
||||
@@ -495,6 +533,7 @@
|
||||
-
|
||||
id: 38
|
||||
owner_id: 22
|
||||
owner_name: limited_org
|
||||
lower_name: public_repo_on_limited_org
|
||||
name: public_repo_on_limited_org
|
||||
is_private: false
|
||||
@@ -507,6 +546,7 @@
|
||||
-
|
||||
id: 39
|
||||
owner_id: 22
|
||||
owner_name: limited_org
|
||||
lower_name: private_repo_on_limited_org
|
||||
name: private_repo_on_limited_org
|
||||
is_private: true
|
||||
@@ -519,6 +559,7 @@
|
||||
-
|
||||
id: 40
|
||||
owner_id: 23
|
||||
owner_name: limited_org
|
||||
lower_name: public_repo_on_private_org
|
||||
name: public_repo_on_private_org
|
||||
is_private: false
|
||||
@@ -531,6 +572,7 @@
|
||||
-
|
||||
id: 41
|
||||
owner_id: 23
|
||||
owner_name: limited_org
|
||||
lower_name: private_repo_on_private_org
|
||||
name: private_repo_on_private_org
|
||||
is_private: true
|
||||
@@ -542,17 +584,20 @@
|
||||
-
|
||||
id: 42
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: glob
|
||||
name: glob
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
num_issues: 1
|
||||
num_milestones: 1
|
||||
is_mirror: false
|
||||
|
||||
-
|
||||
id: 43
|
||||
owner_id: 26
|
||||
owner_name: org26
|
||||
lower_name: repo26
|
||||
name: repo26
|
||||
is_private: true
|
||||
@@ -561,3 +606,71 @@
|
||||
num_issues: 0
|
||||
is_mirror: false
|
||||
status: 0
|
||||
|
||||
-
|
||||
id: 44
|
||||
owner_id: 27
|
||||
owner_name: user27
|
||||
lower_name: template1
|
||||
name: template1
|
||||
is_private: false
|
||||
is_template: true
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
is_mirror: false
|
||||
status: 0
|
||||
|
||||
-
|
||||
id: 45
|
||||
owner_id: 27
|
||||
owner_name: user27
|
||||
lower_name: template2
|
||||
name: template2
|
||||
is_private: false
|
||||
is_template: true
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
is_mirror: false
|
||||
status: 0
|
||||
|
||||
-
|
||||
id: 46
|
||||
owner_id: 26
|
||||
owner_name: org26
|
||||
lower_name: repo_external_tracker
|
||||
name: repo_external_tracker
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
is_mirror: false
|
||||
status: 0
|
||||
|
||||
-
|
||||
id: 47
|
||||
owner_id: 26
|
||||
owner_name: org26
|
||||
lower_name: repo_external_tracker_numeric
|
||||
name: repo_external_tracker_numeric
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
is_mirror: false
|
||||
status: 0
|
||||
|
||||
-
|
||||
id: 48
|
||||
owner_id: 26
|
||||
owner_name: org26
|
||||
lower_name: repo_external_tracker_alpha
|
||||
name: repo_external_tracker_alpha
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
num_pulls: 1
|
||||
is_mirror: false
|
||||
status: 0
|
||||
|
||||
+13
-10
@@ -44,32 +44,34 @@
|
||||
reviewer_id: 2
|
||||
issue_id: 3
|
||||
content: "New review 3"
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
updated_unix: 946684811
|
||||
created_unix: 946684811
|
||||
-
|
||||
id: 7
|
||||
type: 3
|
||||
reviewer_id: 3
|
||||
issue_id: 3
|
||||
content: "New review 4"
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
updated_unix: 946684812
|
||||
created_unix: 946684812
|
||||
-
|
||||
id: 8
|
||||
type: 1
|
||||
reviewer_id: 4
|
||||
issue_id: 3
|
||||
content: "New review 5"
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
commit_id: 8091a55037cd59e47293aca02981b5a67076b364
|
||||
stale: true
|
||||
updated_unix: 946684813
|
||||
created_unix: 946684813
|
||||
-
|
||||
id: 9
|
||||
type: 3
|
||||
reviewer_id: 2
|
||||
issue_id: 3
|
||||
content: "New review 3 rejected"
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
updated_unix: 946684814
|
||||
created_unix: 946684814
|
||||
|
||||
-
|
||||
id: 10
|
||||
@@ -77,5 +79,6 @@
|
||||
reviewer_id: 100
|
||||
issue_id: 3
|
||||
content: "a deleted user's review"
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
official: true
|
||||
updated_unix: 946684815
|
||||
created_unix: 946684815
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
id: 1
|
||||
user_id: 1
|
||||
issue_id: 1
|
||||
created_unix: 1500988502
|
||||
created_unix: 1500988001
|
||||
|
||||
-
|
||||
id: 2
|
||||
user_id: 2
|
||||
issue_id: 2
|
||||
created_unix: 1500988502
|
||||
created_unix: 1500988002
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
name: review_team
|
||||
authorize: 1 # read
|
||||
num_repos: 1
|
||||
num_members: 1
|
||||
num_members: 2
|
||||
|
||||
-
|
||||
id: 10
|
||||
@@ -96,3 +96,23 @@
|
||||
authorize: 1 # read
|
||||
num_repos: 0
|
||||
num_members: 0
|
||||
|
||||
-
|
||||
id: 12
|
||||
org_id: 3
|
||||
lower_name: team12creators
|
||||
name: team12Creators
|
||||
authorize: 3 # admin
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
can_create_org_repo: true
|
||||
|
||||
-
|
||||
id: 13
|
||||
org_id: 6
|
||||
lower_name: team13notcreators
|
||||
name: team13NotCreators
|
||||
authorize: 3 # admin
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
can_create_org_repo: false
|
||||
|
||||
@@ -69,3 +69,21 @@
|
||||
org_id: 25
|
||||
team_id: 10
|
||||
uid: 24
|
||||
|
||||
-
|
||||
id: 13
|
||||
org_id: 3
|
||||
team_id: 12
|
||||
uid: 28
|
||||
|
||||
-
|
||||
id: 14
|
||||
org_id: 6
|
||||
team_id: 13
|
||||
uid: 28
|
||||
|
||||
-
|
||||
id: 15
|
||||
org_id: 17
|
||||
team_id: 9
|
||||
uid: 29
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
issue_id: 1
|
||||
time: 400
|
||||
created_unix: 946684800
|
||||
deleted: false
|
||||
|
||||
-
|
||||
id: 2
|
||||
@@ -11,6 +12,7 @@
|
||||
issue_id: 2
|
||||
time: 3661
|
||||
created_unix: 946684801
|
||||
deleted: false
|
||||
|
||||
-
|
||||
id: 3
|
||||
@@ -18,17 +20,52 @@
|
||||
issue_id: 2
|
||||
time: 1
|
||||
created_unix: 946684802
|
||||
deleted: false
|
||||
|
||||
-
|
||||
id: 4
|
||||
user_id: -1
|
||||
issue_id: 4
|
||||
time: 1
|
||||
created_unix: 946684802
|
||||
created_unix: 946684803
|
||||
deleted: false
|
||||
|
||||
-
|
||||
id: 5
|
||||
user_id: 2
|
||||
issue_id: 5
|
||||
time: 1
|
||||
created_unix: 946684802
|
||||
created_unix: 946684804
|
||||
deleted: false
|
||||
|
||||
-
|
||||
id: 6
|
||||
user_id: 1
|
||||
issue_id: 2
|
||||
time: 20
|
||||
created_unix: 946684812
|
||||
deleted: false
|
||||
|
||||
-
|
||||
id: 7
|
||||
user_id: 2
|
||||
issue_id: 4
|
||||
time: 3
|
||||
created_unix: 946684813
|
||||
deleted: false
|
||||
|
||||
-
|
||||
id: 8
|
||||
user_id: 1
|
||||
issue_id: 4
|
||||
time: 71
|
||||
created_unix: 947688814
|
||||
deleted: false
|
||||
|
||||
-
|
||||
id: 9
|
||||
user_id: 2
|
||||
issue_id: 2
|
||||
time: 100000
|
||||
created_unix: 947688815
|
||||
deleted: true
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
avatar: avatar3
|
||||
avatar_email: user3@example.com
|
||||
num_repos: 3
|
||||
num_members: 2
|
||||
num_teams: 3
|
||||
num_members: 3
|
||||
num_teams: 4
|
||||
|
||||
-
|
||||
id: 4
|
||||
@@ -102,8 +102,8 @@
|
||||
avatar: avatar6
|
||||
avatar_email: user6@example.com
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
num_teams: 1
|
||||
num_members: 2
|
||||
num_teams: 2
|
||||
|
||||
-
|
||||
id: 7
|
||||
@@ -275,7 +275,7 @@
|
||||
avatar_email: user17@example.com
|
||||
num_repos: 2
|
||||
is_active: true
|
||||
num_members: 2
|
||||
num_members: 3
|
||||
num_teams: 3
|
||||
|
||||
-
|
||||
@@ -424,7 +424,57 @@
|
||||
is_admin: false
|
||||
avatar: avatar26
|
||||
avatar_email: org26@example.com
|
||||
num_repos: 1
|
||||
num_repos: 4
|
||||
num_members: 0
|
||||
num_teams: 1
|
||||
repo_admin_change_team_access: true
|
||||
repo_admin_change_team_access: true
|
||||
|
||||
-
|
||||
id: 27
|
||||
lower_name: user27
|
||||
name: user27
|
||||
full_name: User Twenty-Seven
|
||||
email: user27@example.com
|
||||
email_notifications_preference: enabled
|
||||
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
|
||||
type: 0 # individual
|
||||
salt: ZogKvWdyEx
|
||||
is_admin: false
|
||||
avatar: avatar27
|
||||
avatar_email: user27@example.com
|
||||
num_repos: 2
|
||||
|
||||
-
|
||||
id: 28
|
||||
lower_name: user28
|
||||
name: user28
|
||||
full_name: "user27"
|
||||
email: user28@example.com
|
||||
keep_email_private: true
|
||||
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
|
||||
type: 0 # individual
|
||||
salt: ZogKvWdyEx
|
||||
is_admin: false
|
||||
avatar: avatar28
|
||||
avatar_email: user28@example.com
|
||||
num_repos: 0
|
||||
num_stars: 0
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 29
|
||||
lower_name: user29
|
||||
name: user29
|
||||
full_name: User 29
|
||||
email: user29@example.com
|
||||
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
|
||||
type: 0 # individual
|
||||
salt: ZogKvWdyEx
|
||||
is_admin: false
|
||||
is_restricted: true
|
||||
avatar: avatar29
|
||||
avatar_email: user29@example.com
|
||||
num_repos: 0
|
||||
is_active: true
|
||||
|
||||
@@ -2,13 +2,28 @@
|
||||
id: 1
|
||||
user_id: 1
|
||||
repo_id: 1
|
||||
mode: 1 # normal
|
||||
|
||||
-
|
||||
id: 2
|
||||
user_id: 4
|
||||
repo_id: 1
|
||||
mode: 1 # normal
|
||||
|
||||
-
|
||||
id: 3
|
||||
user_id: 9
|
||||
repo_id: 1
|
||||
mode: 1 # normal
|
||||
|
||||
-
|
||||
id: 4
|
||||
user_id: 8
|
||||
repo_id: 1
|
||||
mode: 2 # don't watch
|
||||
|
||||
-
|
||||
id: 5
|
||||
user_id: 11
|
||||
repo_id: 1
|
||||
mode: 3 # auto
|
||||
|
||||
+65
-8
@@ -64,9 +64,14 @@ func (key *GPGKey) AfterLoad(session *xorm.Session) {
|
||||
}
|
||||
|
||||
// ListGPGKeys returns a list of public keys belongs to given user.
|
||||
func ListGPGKeys(uid int64) ([]*GPGKey, error) {
|
||||
keys := make([]*GPGKey, 0, 5)
|
||||
return keys, x.Where("owner_id=? AND primary_key_id=''", uid).Find(&keys)
|
||||
func ListGPGKeys(uid int64, listOptions ListOptions) ([]*GPGKey, error) {
|
||||
sess := x.Where("owner_id=? AND primary_key_id=''", uid)
|
||||
if listOptions.Page != 0 {
|
||||
sess = listOptions.setSessionPagination(sess)
|
||||
}
|
||||
|
||||
keys := make([]*GPGKey, 0, 2)
|
||||
return keys, sess.Find(&keys)
|
||||
}
|
||||
|
||||
// GetGPGKeyByID returns public key by given ID.
|
||||
@@ -268,7 +273,7 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
|
||||
for i, k := range e.Subkeys {
|
||||
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, ErrGPGKeyParsing{ParseError: err}
|
||||
}
|
||||
subkeys[i] = subs
|
||||
}
|
||||
@@ -369,6 +374,7 @@ type CommitVerification struct {
|
||||
CommittingUser *User
|
||||
SigningEmail string
|
||||
SigningKey *GPGKey
|
||||
TrustStatus string
|
||||
}
|
||||
|
||||
// SignCommit represents a commit with validation of signature.
|
||||
@@ -628,7 +634,7 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
|
||||
|
||||
// Now try to associate the signature with the committer, if present
|
||||
if committer.ID != 0 {
|
||||
keys, err := ListGPGKeys(committer.ID)
|
||||
keys, err := ListGPGKeys(committer.ID, ListOptions{})
|
||||
if err != nil { //Skipping failed to get gpg keys of user
|
||||
log.Error("ListGPGKeys: %v", err)
|
||||
return &CommitVerification{
|
||||
@@ -735,6 +741,21 @@ func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature,
|
||||
CanSign: pubkey.CanSign(),
|
||||
KeyID: pubkey.KeyIdString(),
|
||||
}
|
||||
for _, subKey := range ekey.Subkeys {
|
||||
content, err := base64EncPubKey(subKey.PublicKey)
|
||||
if err != nil {
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
k.SubsKey = append(k.SubsKey, &GPGKey{
|
||||
Content: content,
|
||||
CanSign: subKey.PublicKey.CanSign(),
|
||||
KeyID: subKey.PublicKey.KeyIdString(),
|
||||
})
|
||||
}
|
||||
if commitVerification := hashAndVerifyWithSubKeys(sig, payload, k, committer, &User{
|
||||
Name: gpgSettings.Name,
|
||||
Email: gpgSettings.Email,
|
||||
@@ -754,18 +775,54 @@ func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature,
|
||||
}
|
||||
|
||||
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
|
||||
func ParseCommitsWithSignature(oldCommits *list.List) *list.List {
|
||||
func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *list.List {
|
||||
var (
|
||||
newCommits = list.New()
|
||||
e = oldCommits.Front()
|
||||
)
|
||||
memberMap := map[int64]bool{}
|
||||
|
||||
for e != nil {
|
||||
c := e.Value.(UserCommit)
|
||||
newCommits.PushBack(SignCommit{
|
||||
signCommit := SignCommit{
|
||||
UserCommit: &c,
|
||||
Verification: ParseCommitWithSignature(c.Commit),
|
||||
})
|
||||
}
|
||||
|
||||
_ = CalculateTrustStatus(signCommit.Verification, repository, &memberMap)
|
||||
|
||||
newCommits.PushBack(signCommit)
|
||||
e = e.Next()
|
||||
}
|
||||
return newCommits
|
||||
}
|
||||
|
||||
// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
|
||||
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, memberMap *map[int64]bool) (err error) {
|
||||
if verification.Verified {
|
||||
verification.TrustStatus = "trusted"
|
||||
if verification.SigningUser.ID != 0 {
|
||||
var isMember bool
|
||||
if memberMap != nil {
|
||||
var has bool
|
||||
isMember, has = (*memberMap)[verification.SigningUser.ID]
|
||||
if !has {
|
||||
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
|
||||
(*memberMap)[verification.SigningUser.ID] = isMember
|
||||
}
|
||||
} else {
|
||||
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
verification.TrustStatus = "untrusted"
|
||||
if verification.CommittingUser.ID != verification.SigningUser.ID {
|
||||
// The committing user and the signing user are not the same and are not the default key
|
||||
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
|
||||
verification.TrustStatus = "unmatched"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
// Copyright 2016 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// GraphItem represent one commit, or one relation in timeline
|
||||
type GraphItem struct {
|
||||
GraphAcii string
|
||||
Relation string
|
||||
Branch string
|
||||
Rev string
|
||||
Date string
|
||||
Author string
|
||||
AuthorEmail string
|
||||
ShortRev string
|
||||
Subject string
|
||||
OnlyRelation bool
|
||||
}
|
||||
|
||||
// GraphItems is a list of commits from all branches
|
||||
type GraphItems []GraphItem
|
||||
|
||||
// GetCommitGraph return a list of commit (GraphItems) from all branches
|
||||
func GetCommitGraph(r *git.Repository, page int) (GraphItems, error) {
|
||||
|
||||
var CommitGraph []GraphItem
|
||||
|
||||
format := "DATA:|%d|%H|%ad|%an|%ae|%h|%s"
|
||||
|
||||
graphCmd := git.NewCommand("log")
|
||||
graphCmd.AddArguments("--graph",
|
||||
"--date-order",
|
||||
"--all",
|
||||
"-C",
|
||||
"-M",
|
||||
fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum),
|
||||
fmt.Sprintf("--skip=%d", setting.UI.GraphMaxCommitNum*(page-1)),
|
||||
"--date=iso",
|
||||
fmt.Sprintf("--pretty=format:%s", format),
|
||||
)
|
||||
graph, err := graphCmd.RunInDir(r.Path)
|
||||
if err != nil {
|
||||
return CommitGraph, err
|
||||
}
|
||||
|
||||
CommitGraph = make([]GraphItem, 0, 100)
|
||||
for _, s := range strings.Split(graph, "\n") {
|
||||
GraphItem, err := graphItemFromString(s, r)
|
||||
if err != nil {
|
||||
return CommitGraph, err
|
||||
}
|
||||
CommitGraph = append(CommitGraph, GraphItem)
|
||||
}
|
||||
|
||||
return CommitGraph, nil
|
||||
}
|
||||
|
||||
func graphItemFromString(s string, r *git.Repository) (GraphItem, error) {
|
||||
|
||||
var ascii string
|
||||
var data = "|||||||"
|
||||
lines := strings.SplitN(s, "DATA:", 2)
|
||||
|
||||
switch len(lines) {
|
||||
case 1:
|
||||
ascii = lines[0]
|
||||
case 2:
|
||||
ascii = lines[0]
|
||||
data = lines[1]
|
||||
default:
|
||||
return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s. Expect 1 or two fields", s)
|
||||
}
|
||||
|
||||
rows := strings.SplitN(data, "|", 8)
|
||||
if len(rows) < 8 {
|
||||
return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s - Should containt 8 datafields", s)
|
||||
}
|
||||
|
||||
/* // see format in getCommitGraph()
|
||||
0 Relation string
|
||||
1 Branch string
|
||||
2 Rev string
|
||||
3 Date string
|
||||
4 Author string
|
||||
5 AuthorEmail string
|
||||
6 ShortRev string
|
||||
7 Subject string
|
||||
*/
|
||||
gi := GraphItem{ascii,
|
||||
rows[0],
|
||||
rows[1],
|
||||
rows[2],
|
||||
rows[3],
|
||||
rows[4],
|
||||
rows[5],
|
||||
rows[6],
|
||||
rows[7],
|
||||
len(rows[2]) == 0, // no commits referred to, only relation in current line.
|
||||
}
|
||||
return gi, nil
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright 2016 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
func BenchmarkGetCommitGraph(b *testing.B) {
|
||||
|
||||
currentRepo, err := git.OpenRepository(".")
|
||||
if err != nil {
|
||||
b.Error("Could not open repository")
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
graph, err := GetCommitGraph(currentRepo, 1)
|
||||
if err != nil {
|
||||
b.Error("Could get commit graph")
|
||||
}
|
||||
|
||||
if len(graph) < 100 {
|
||||
b.Error("Should get 100 log lines.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseCommitString(b *testing.B) {
|
||||
testString := "* DATA:||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Kjell Kvinge|kjell@kvinge.biz|4e61bac|Add route for graph"
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
graphItem, err := graphItemFromString(testString, nil)
|
||||
if err != nil {
|
||||
b.Error("could not parse teststring")
|
||||
}
|
||||
|
||||
if graphItem.Author != "Kjell Kvinge" {
|
||||
b.Error("Did not get expected data")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitStringParsing(t *testing.T) {
|
||||
dataFirstPart := "* DATA:||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Author|user@mail.something|4e61bac|"
|
||||
tests := []struct {
|
||||
shouldPass bool
|
||||
testName string
|
||||
commitMessage string
|
||||
}{
|
||||
{true, "normal", "not a fancy message"},
|
||||
{true, "extra pipe", "An extra pipe: |"},
|
||||
{true, "extra 'Data:'", "DATA: might be trouble"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
testString := fmt.Sprintf("%s%s", dataFirstPart, test.commitMessage)
|
||||
graphItem, err := graphItemFromString(testString, nil)
|
||||
if err != nil && test.shouldPass {
|
||||
t.Errorf("Could not parse %s", testString)
|
||||
return
|
||||
}
|
||||
|
||||
if test.commitMessage != graphItem.Subject {
|
||||
t.Errorf("%s does not match %s", test.commitMessage, graphItem.Subject)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,13 @@ package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
)
|
||||
|
||||
// LocalCopyPath returns the local repository temporary copy path.
|
||||
@@ -27,11 +25,15 @@ func LocalCopyPath() string {
|
||||
|
||||
// CreateTemporaryPath creates a temporary path
|
||||
func CreateTemporaryPath(prefix string) (string, error) {
|
||||
timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE
|
||||
basePath := path.Join(LocalCopyPath(), prefix+"-"+timeStr+".git")
|
||||
if err := os.MkdirAll(filepath.Dir(basePath), os.ModePerm); err != nil {
|
||||
log.Error("Unable to create temporary directory: %s (%v)", basePath, err)
|
||||
return "", fmt.Errorf("Failed to create dir %s: %v", basePath, err)
|
||||
if err := os.MkdirAll(LocalCopyPath(), os.ModePerm); err != nil {
|
||||
log.Error("Unable to create localcopypath directory: %s (%v)", LocalCopyPath(), err)
|
||||
return "", fmt.Errorf("Failed to create localcopypath directory %s: %v", LocalCopyPath(), err)
|
||||
}
|
||||
basePath, err := ioutil.TempDir(LocalCopyPath(), prefix+".git")
|
||||
if err != nil {
|
||||
log.Error("Unable to create temporary directory: %s-*.git (%v)", prefix, err)
|
||||
return "", fmt.Errorf("Failed to create dir %s-*.git: %v", prefix, err)
|
||||
|
||||
}
|
||||
return basePath, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,29 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// env keys for git hooks need
|
||||
const (
|
||||
EnvRepoName = "GITEA_REPO_NAME"
|
||||
EnvRepoUsername = "GITEA_REPO_USER_NAME"
|
||||
EnvRepoIsWiki = "GITEA_REPO_IS_WIKI"
|
||||
EnvPusherName = "GITEA_PUSHER_NAME"
|
||||
EnvPusherEmail = "GITEA_PUSHER_EMAIL"
|
||||
EnvPusherID = "GITEA_PUSHER_ID"
|
||||
EnvKeyID = "GITEA_KEY_ID"
|
||||
EnvIsDeployKey = "GITEA_IS_DEPLOY_KEY"
|
||||
EnvIsInternal = "GITEA_INTERNAL_PUSH"
|
||||
)
|
||||
|
||||
// InternalPushingEnvironment returns an os environment to switch off hooks on push
|
||||
// It is recommended to avoid using this unless you are pushing within a transaction
|
||||
// or if you absolutely are sure that post-receive and pre-receive will do nothing
|
||||
// We provide the full pushing-environment for other hook providers
|
||||
func InternalPushingEnvironment(doer *User, repo *Repository) []string {
|
||||
return append(PushingEnvironment(doer, repo),
|
||||
EnvIsInternal+"=true",
|
||||
)
|
||||
}
|
||||
|
||||
// PushingEnvironment returns an os environment to allow hooks to work on push
|
||||
func PushingEnvironment(doer *User, repo *Repository) []string {
|
||||
return FullPushingEnvironment(doer, doer, repo, repo.Name, 0)
|
||||
@@ -33,7 +56,7 @@ func FullPushingEnvironment(author, committer *User, repo *Repository, repoName
|
||||
"GIT_COMMITTER_NAME="+committerSig.Name,
|
||||
"GIT_COMMITTER_EMAIL="+committerSig.Email,
|
||||
EnvRepoName+"="+repoName,
|
||||
EnvRepoUsername+"="+repo.MustOwnerName(),
|
||||
EnvRepoUsername+"="+repo.OwnerName,
|
||||
EnvRepoIsWiki+"="+isWiki,
|
||||
EnvPusherName+"="+committer.Name,
|
||||
EnvPusherID+"="+fmt.Sprintf("%d", committer.ID),
|
||||
|
||||
+398
-194
@@ -1,4 +1,5 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// 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.
|
||||
|
||||
@@ -6,7 +7,6 @@ package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -73,8 +73,8 @@ var (
|
||||
issueTasksDonePat *regexp.Regexp
|
||||
)
|
||||
|
||||
const issueTasksRegexpStr = `(^\s*[-*]\s\[[\sx]\]\s.)|(\n\s*[-*]\s\[[\sx]\]\s.)`
|
||||
const issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[x]\]\s.)|(\n\s*[-*]\s\[[x]\]\s.)`
|
||||
const issueTasksRegexpStr = `(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`
|
||||
const issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`
|
||||
const issueMaxDupIndexAttempts = 3
|
||||
|
||||
func init() {
|
||||
@@ -138,6 +138,11 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// LoadLabels loads labels
|
||||
func (issue *Issue) LoadLabels() error {
|
||||
return issue.loadLabels(x)
|
||||
}
|
||||
|
||||
func (issue *Issue) loadLabels(e Engine) (err error) {
|
||||
if issue.Labels == nil {
|
||||
issue.Labels, err = getLabelsByIssueID(e, issue.ID)
|
||||
@@ -218,8 +223,11 @@ func (issue *Issue) loadReactions(e Engine) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = issue.loadRepo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
// Load reaction user data
|
||||
if _, err := ReactionList(reactions).loadUsers(e); err != nil {
|
||||
if _, err := ReactionList(reactions).loadUsers(e, issue.Repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -239,6 +247,16 @@ func (issue *Issue) loadReactions(e Engine) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) loadMilestone(e Engine) (err error) {
|
||||
if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
||||
issue.Milestone, err = getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
|
||||
if err != nil && !IsErrMilestoneNotExist(err) {
|
||||
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) loadAttributes(e Engine) (err error) {
|
||||
if err = issue.loadRepo(e); err != nil {
|
||||
return
|
||||
@@ -252,11 +270,8 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if issue.Milestone == nil && issue.MilestoneID > 0 {
|
||||
issue.Milestone, err = getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
|
||||
if err != nil && !IsErrMilestoneNotExist(err) {
|
||||
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err)
|
||||
}
|
||||
if err = issue.loadMilestone(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issue.loadAssignees(e); err != nil {
|
||||
@@ -296,6 +311,11 @@ func (issue *Issue) LoadAttributes() error {
|
||||
return issue.loadAttributes(x)
|
||||
}
|
||||
|
||||
// LoadMilestone load milestone of this issue.
|
||||
func (issue *Issue) LoadMilestone() error {
|
||||
return issue.loadMilestone(x)
|
||||
}
|
||||
|
||||
// GetIsRead load the `IsRead` field of the issue
|
||||
func (issue *Issue) GetIsRead(userID int64) error {
|
||||
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
||||
@@ -311,7 +331,14 @@ func (issue *Issue) GetIsRead(userID int64) error {
|
||||
|
||||
// APIURL returns the absolute APIURL to this issue.
|
||||
func (issue *Issue) APIURL() string {
|
||||
return issue.Repo.APIURL() + "/" + path.Join("issues", fmt.Sprint(issue.Index))
|
||||
if issue.Repo == nil {
|
||||
err := issue.LoadRepo()
|
||||
if err != nil {
|
||||
log.Error("Issue[%d].APIURL(): %v", issue.ID, err)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index)
|
||||
}
|
||||
|
||||
// HTMLURL returns the absolute URL to this issue.
|
||||
@@ -349,73 +376,6 @@ func (issue *Issue) State() api.StateType {
|
||||
return api.StateOpen
|
||||
}
|
||||
|
||||
// APIFormat assumes some fields assigned with values:
|
||||
// Required - Poster, Labels,
|
||||
// Optional - Milestone, Assignee, PullRequest
|
||||
func (issue *Issue) APIFormat() *api.Issue {
|
||||
return issue.apiFormat(x)
|
||||
}
|
||||
|
||||
func (issue *Issue) apiFormat(e Engine) *api.Issue {
|
||||
issue.loadLabels(e)
|
||||
apiLabels := make([]*api.Label, len(issue.Labels))
|
||||
for i := range issue.Labels {
|
||||
apiLabels[i] = issue.Labels[i].APIFormat()
|
||||
}
|
||||
|
||||
issue.loadPoster(e)
|
||||
issue.loadRepo(e)
|
||||
apiIssue := &api.Issue{
|
||||
ID: issue.ID,
|
||||
URL: issue.APIURL(),
|
||||
Index: issue.Index,
|
||||
Poster: issue.Poster.APIFormat(),
|
||||
Title: issue.Title,
|
||||
Body: issue.Content,
|
||||
Labels: apiLabels,
|
||||
State: issue.State(),
|
||||
Comments: issue.NumComments,
|
||||
Created: issue.CreatedUnix.AsTime(),
|
||||
Updated: issue.UpdatedUnix.AsTime(),
|
||||
}
|
||||
|
||||
apiIssue.Repo = &api.RepositoryMeta{
|
||||
ID: issue.Repo.ID,
|
||||
Name: issue.Repo.Name,
|
||||
FullName: issue.Repo.FullName(),
|
||||
}
|
||||
|
||||
if issue.ClosedUnix != 0 {
|
||||
apiIssue.Closed = issue.ClosedUnix.AsTimePtr()
|
||||
}
|
||||
|
||||
if issue.Milestone != nil {
|
||||
apiIssue.Milestone = issue.Milestone.APIFormat()
|
||||
}
|
||||
issue.loadAssignees(e)
|
||||
|
||||
if len(issue.Assignees) > 0 {
|
||||
for _, assignee := range issue.Assignees {
|
||||
apiIssue.Assignees = append(apiIssue.Assignees, assignee.APIFormat())
|
||||
}
|
||||
apiIssue.Assignee = issue.Assignees[0].APIFormat() // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
|
||||
}
|
||||
if issue.IsPull {
|
||||
issue.loadPullRequest(e)
|
||||
apiIssue.PullRequest = &api.PullRequestMeta{
|
||||
HasMerged: issue.PullRequest.HasMerged,
|
||||
}
|
||||
if issue.PullRequest.HasMerged {
|
||||
apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
|
||||
}
|
||||
}
|
||||
if issue.DeadlineUnix != 0 {
|
||||
apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
|
||||
}
|
||||
|
||||
return apiIssue
|
||||
}
|
||||
|
||||
// HashTag returns unique hash tag for issue.
|
||||
func (issue *Issue) HashTag() string {
|
||||
return "issue-" + com.ToStr(issue.ID)
|
||||
@@ -423,7 +383,7 @@ func (issue *Issue) HashTag() string {
|
||||
|
||||
// IsPoster returns true if given user by ID is the poster.
|
||||
func (issue *Issue) IsPoster(uid int64) bool {
|
||||
return issue.PosterID == uid
|
||||
return issue.OriginalAuthorID == 0 && issue.PosterID == uid
|
||||
}
|
||||
|
||||
func (issue *Issue) hasLabel(e Engine, labelID int64) bool {
|
||||
@@ -505,7 +465,7 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
|
||||
return err
|
||||
}
|
||||
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
return ErrLabelNotExist{}
|
||||
return ErrRepoLabelNotExist{}
|
||||
}
|
||||
|
||||
if err = issue.clearLabels(sess, doer); err != nil {
|
||||
@@ -600,89 +560,113 @@ func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (err error) {
|
||||
func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed, isMergePull bool) (*Comment, error) {
|
||||
// Reload the issue
|
||||
currentIssue, err := getIssueByID(e, issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Nothing should be performed if current status is same as target status
|
||||
if currentIssue.IsClosed == isClosed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for open dependencies
|
||||
if isClosed && issue.Repo.isDependenciesEnabled(e) {
|
||||
// only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies
|
||||
noDeps, err := issueNoDependenciesLeft(e, issue)
|
||||
if err != nil {
|
||||
return err
|
||||
if !issue.IsPull {
|
||||
return nil, ErrIssueWasClosed{
|
||||
ID: issue.ID,
|
||||
}
|
||||
}
|
||||
|
||||
if !noDeps {
|
||||
return ErrDependenciesLeft{issue.ID}
|
||||
return nil, ErrPullWasClosed{
|
||||
ID: issue.ID,
|
||||
}
|
||||
}
|
||||
|
||||
issue.IsClosed = isClosed
|
||||
if isClosed {
|
||||
return issue.doChangeStatus(e, doer, isMergePull)
|
||||
}
|
||||
|
||||
func (issue *Issue) doChangeStatus(e *xorm.Session, doer *User, isMergePull bool) (*Comment, error) {
|
||||
// Check for open dependencies
|
||||
if issue.IsClosed && issue.Repo.isDependenciesEnabled(e) {
|
||||
// only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies
|
||||
noDeps, err := issueNoDependenciesLeft(e, issue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !noDeps {
|
||||
return nil, ErrDependenciesLeft{issue.ID}
|
||||
}
|
||||
}
|
||||
|
||||
if issue.IsClosed {
|
||||
issue.ClosedUnix = timeutil.TimeStampNow()
|
||||
} else {
|
||||
issue.ClosedUnix = 0
|
||||
}
|
||||
|
||||
if err = updateIssueCols(e, issue, "is_closed", "closed_unix"); err != nil {
|
||||
return err
|
||||
if err := updateIssueCols(e, issue, "is_closed", "closed_unix"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update issue count of labels
|
||||
if err = issue.getLabels(e); err != nil {
|
||||
return err
|
||||
if err := issue.getLabels(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for idx := range issue.Labels {
|
||||
if err = updateLabel(e, issue.Labels[idx]); err != nil {
|
||||
return err
|
||||
if err := updateLabelCols(e, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Update issue count of milestone
|
||||
if err := updateMilestoneClosedNum(e, issue.MilestoneID); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := issue.updateClosedNum(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// New action comment
|
||||
if _, err = createStatusComment(e, doer, issue); err != nil {
|
||||
return err
|
||||
cmtType := CommentTypeClose
|
||||
if !issue.IsClosed {
|
||||
cmtType = CommentTypeReopen
|
||||
} else if isMergePull {
|
||||
cmtType = CommentTypeMergePull
|
||||
}
|
||||
|
||||
return nil
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: cmtType,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangeStatus changes issue status to open or closed.
|
||||
func (issue *Issue) ChangeStatus(doer *User, isClosed bool) (err error) {
|
||||
func (issue *Issue) ChangeStatus(doer *User, isClosed bool) (*Comment, error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
if err := sess.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = issue.loadRepo(sess); err != nil {
|
||||
return err
|
||||
if err := issue.loadRepo(sess); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = issue.loadPoster(sess); err != nil {
|
||||
return err
|
||||
if err := issue.loadPoster(sess); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = issue.changeStatus(sess, doer, isClosed); err != nil {
|
||||
return err
|
||||
comment, err := issue.changeStatus(sess, doer, isClosed, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = sess.Commit(); err != nil {
|
||||
return fmt.Errorf("Commit: %v", err)
|
||||
return nil, fmt.Errorf("Commit: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// ChangeTitle changes the title of this issue, as the given user.
|
||||
@@ -702,15 +686,18 @@ func (issue *Issue) ChangeTitle(doer *User, oldTitle string) (err error) {
|
||||
return fmt.Errorf("loadRepo: %v", err)
|
||||
}
|
||||
|
||||
if _, err = createChangeTitleComment(sess, doer, issue.Repo, issue, oldTitle, issue.Title); err != nil {
|
||||
return fmt.Errorf("createChangeTitleComment: %v", err)
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: CommentTypeChangeTitle,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
OldTitle: oldTitle,
|
||||
NewTitle: issue.Title,
|
||||
}
|
||||
|
||||
if err = issue.neuterCrossReferences(sess); err != nil {
|
||||
return err
|
||||
if _, err = createComment(sess, opts); err != nil {
|
||||
return fmt.Errorf("createComment: %v", err)
|
||||
}
|
||||
|
||||
if err = issue.addCrossReferences(sess, doer); err != nil {
|
||||
if err = issue.addCrossReferences(sess, doer, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -728,7 +715,14 @@ func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branc
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := createDeleteBranchComment(sess, doer, repo, issue, branchName); err != nil {
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: CommentTypeDeleteBranch,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
CommitSHA: branchName,
|
||||
}
|
||||
if _, err = createComment(sess, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -768,10 +762,8 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
|
||||
if err = updateIssueCols(sess, issue, "content"); err != nil {
|
||||
return fmt.Errorf("UpdateIssueCols: %v", err)
|
||||
}
|
||||
if err = issue.neuterCrossReferences(sess); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = issue.addCrossReferences(sess, doer); err != nil {
|
||||
|
||||
if err = issue.addCrossReferences(sess, doer, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -807,6 +799,20 @@ func (issue *Issue) GetLastEventLabel() string {
|
||||
return "repo.issues.opened_by"
|
||||
}
|
||||
|
||||
// GetLastComment return last comment for the current issue.
|
||||
func (issue *Issue) GetLastComment() (*Comment, error) {
|
||||
var c Comment
|
||||
exist, err := x.Where("type = ?", CommentTypeComment).
|
||||
And("issue_id = ?", issue.ID).Desc("id").Get(&c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, nil
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
|
||||
func (issue *Issue) GetLastEventLabelFake() string {
|
||||
if issue.IsClosed {
|
||||
@@ -864,7 +870,15 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = createMilestoneComment(e, doer, opts.Repo, opts.Issue, 0, opts.Issue.MilestoneID); err != nil {
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: CommentTypeMilestone,
|
||||
Doer: doer,
|
||||
Repo: opts.Repo,
|
||||
Issue: opts.Issue,
|
||||
OldMilestoneID: 0,
|
||||
MilestoneID: opts.Issue.MilestoneID,
|
||||
}
|
||||
if _, err = createComment(e, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -892,7 +906,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
||||
|
||||
for _, label := range labels {
|
||||
// Silently drop invalid labels.
|
||||
if label.RepoID != opts.Repo.ID {
|
||||
if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -922,7 +936,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
||||
if err = opts.Issue.loadAttributes(e); err != nil {
|
||||
return err
|
||||
}
|
||||
return opts.Issue.addCrossReferences(e, doer)
|
||||
return opts.Issue.addCrossReferences(e, doer, false)
|
||||
}
|
||||
|
||||
// NewIssue creates new issue with labels for repository.
|
||||
@@ -1042,18 +1056,19 @@ func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) {
|
||||
|
||||
// IssuesOptions represents options of an issue.
|
||||
type IssuesOptions struct {
|
||||
RepoIDs []int64 // include all repos if empty
|
||||
AssigneeID int64
|
||||
PosterID int64
|
||||
MentionedID int64
|
||||
MilestoneID int64
|
||||
Page int
|
||||
PageSize int
|
||||
IsClosed util.OptionalBool
|
||||
IsPull util.OptionalBool
|
||||
LabelIDs []int64
|
||||
SortType string
|
||||
IssueIDs []int64
|
||||
ListOptions
|
||||
RepoIDs []int64 // include all repos if empty
|
||||
AssigneeID int64
|
||||
PosterID int64
|
||||
MentionedID int64
|
||||
MilestoneIDs []int64
|
||||
IsClosed util.OptionalBool
|
||||
IsPull util.OptionalBool
|
||||
LabelIDs []int64
|
||||
IncludedLabelNames []string
|
||||
ExcludedLabelNames []string
|
||||
SortType string
|
||||
IssueIDs []int64
|
||||
// prioritize issues from this repo
|
||||
PriorityRepoID int64
|
||||
}
|
||||
@@ -1075,7 +1090,8 @@ func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64
|
||||
case "priority":
|
||||
sess.Desc("issue.priority")
|
||||
case "nearduedate":
|
||||
sess.Asc("issue.deadline_unix")
|
||||
// 253370764800 is 01/01/9999 @ 12:00am (UTC)
|
||||
sess.OrderBy("CASE WHEN issue.deadline_unix = 0 THEN 253370764800 ELSE issue.deadline_unix END ASC")
|
||||
case "farduedate":
|
||||
sess.Desc("issue.deadline_unix")
|
||||
case "priorityrepo":
|
||||
@@ -1127,8 +1143,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
|
||||
And("issue_user.uid = ?", opts.MentionedID)
|
||||
}
|
||||
|
||||
if opts.MilestoneID > 0 {
|
||||
sess.And("issue.milestone_id=?", opts.MilestoneID)
|
||||
if len(opts.MilestoneIDs) > 0 {
|
||||
sess.In("issue.milestone_id", opts.MilestoneIDs)
|
||||
}
|
||||
|
||||
switch opts.IsPull {
|
||||
@@ -1148,6 +1164,14 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(opts.IncludedLabelNames) > 0 {
|
||||
sess.In("issue.id", BuildLabelNamesIssueIDsCondition(opts.IncludedLabelNames))
|
||||
}
|
||||
|
||||
if len(opts.ExcludedLabelNames) > 0 {
|
||||
sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames)))
|
||||
}
|
||||
}
|
||||
|
||||
// CountIssuesByRepo map from repoID to number of issues matching the options
|
||||
@@ -1175,6 +1199,26 @@ func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) {
|
||||
return countMap, nil
|
||||
}
|
||||
|
||||
// GetRepoIDsForIssuesOptions find all repo ids for the given options
|
||||
func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *User) ([]int64, error) {
|
||||
repoIDs := make([]int64, 0, 5)
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
opts.setupSession(sess)
|
||||
|
||||
accessCond := accessibleRepositoryCondition(user)
|
||||
if err := sess.Where(accessCond).
|
||||
Join("INNER", "repository", "`issue`.repo_id = `repository`.id").
|
||||
Distinct("issue.repo_id").
|
||||
Table("issue").
|
||||
Find(&repoIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repoIDs, nil
|
||||
}
|
||||
|
||||
// Issues returns a list of issues by given conditions.
|
||||
func Issues(opts *IssuesOptions) ([]*Issue, error) {
|
||||
sess := x.NewSession()
|
||||
@@ -1196,29 +1240,27 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// GetParticipantsByIssueID returns all users who are participated in comments of an issue.
|
||||
func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
|
||||
return getParticipantsByIssueID(x, issueID)
|
||||
// GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
|
||||
// but skips joining with `user` for performance reasons.
|
||||
// User permissions must be verified elsewhere if required.
|
||||
func GetParticipantsIDsByIssueID(issueID int64) ([]int64, error) {
|
||||
userIDs := make([]int64, 0, 5)
|
||||
return userIDs, x.Table("comment").
|
||||
Cols("poster_id").
|
||||
Where("issue_id = ?", issueID).
|
||||
And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
|
||||
Distinct("poster_id").
|
||||
Find(&userIDs)
|
||||
}
|
||||
|
||||
func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
|
||||
userIDs := make([]int64, 0, 5)
|
||||
if err := e.Table("comment").Cols("poster_id").
|
||||
Where("`comment`.issue_id = ?", issueID).
|
||||
And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
|
||||
And("`user`.is_active = ?", true).
|
||||
And("`user`.prohibit_login = ?", false).
|
||||
Join("INNER", "`user`", "`user`.id = `comment`.poster_id").
|
||||
Distinct("poster_id").
|
||||
Find(&userIDs); err != nil {
|
||||
return nil, fmt.Errorf("get poster IDs: %v", err)
|
||||
// IsUserParticipantsOfIssue return true if user is participants of an issue
|
||||
func IsUserParticipantsOfIssue(user *User, issue *Issue) bool {
|
||||
userIDs, err := issue.getParticipantIDsByIssue(x)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return false
|
||||
}
|
||||
if len(userIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
users := make([]*User, 0, len(userIDs))
|
||||
return users, e.In("id", userIDs).Find(&users)
|
||||
return util.IsInt64InSlice(user.ID, userIDs)
|
||||
}
|
||||
|
||||
// UpdateIssueMentions updates issue-user relations for mentioned users.
|
||||
@@ -1277,6 +1319,36 @@ type IssueStatsOptions struct {
|
||||
|
||||
// GetIssueStats returns issue statistic information by given conditions.
|
||||
func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
|
||||
if len(opts.IssueIDs) <= maxQueryParameters {
|
||||
return getIssueStatsChunk(opts, opts.IssueIDs)
|
||||
}
|
||||
|
||||
// If too long a list of IDs is provided, we get the statistics in
|
||||
// smaller chunks and get accumulates. Note: this could potentially
|
||||
// get us invalid results. The alternative is to insert the list of
|
||||
// ids in a temporary table and join from them.
|
||||
accum := &IssueStats{}
|
||||
for i := 0; i < len(opts.IssueIDs); {
|
||||
chunk := i + maxQueryParameters
|
||||
if chunk > len(opts.IssueIDs) {
|
||||
chunk = len(opts.IssueIDs)
|
||||
}
|
||||
stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accum.OpenCount += stats.OpenCount
|
||||
accum.ClosedCount += stats.ClosedCount
|
||||
accum.YourRepositoriesCount += stats.YourRepositoriesCount
|
||||
accum.AssignCount += stats.AssignCount
|
||||
accum.CreateCount += stats.CreateCount
|
||||
accum.OpenCount += stats.MentionCount
|
||||
i = chunk
|
||||
}
|
||||
return accum, nil
|
||||
}
|
||||
|
||||
func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats, error) {
|
||||
stats := &IssueStats{}
|
||||
|
||||
countSession := func(opts *IssueStatsOptions) *xorm.Session {
|
||||
@@ -1348,11 +1420,12 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
|
||||
// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats.
|
||||
type UserIssueStatsOptions struct {
|
||||
UserID int64
|
||||
RepoID int64
|
||||
RepoIDs []int64
|
||||
UserRepoIDs []int64
|
||||
FilterMode int
|
||||
IsPull bool
|
||||
IsClosed bool
|
||||
IssueIDs []int64
|
||||
}
|
||||
|
||||
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
|
||||
@@ -1362,19 +1435,22 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
|
||||
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull})
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
|
||||
if len(opts.RepoIDs) > 0 {
|
||||
cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs))
|
||||
}
|
||||
if len(opts.IssueIDs) > 0 {
|
||||
cond = cond.And(builder.In("issue.id", opts.IssueIDs))
|
||||
}
|
||||
|
||||
switch opts.FilterMode {
|
||||
case FilterModeAll:
|
||||
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
|
||||
stats.OpenCount, err = x.Where(cond).And("issue.is_closed = ?", false).
|
||||
And(builder.In("issue.repo_id", opts.UserRepoIDs)).
|
||||
Count(new(Issue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
|
||||
stats.ClosedCount, err = x.Where(cond).And("issue.is_closed = ?", true).
|
||||
And(builder.In("issue.repo_id", opts.UserRepoIDs)).
|
||||
Count(new(Issue))
|
||||
if err != nil {
|
||||
@@ -1396,14 +1472,14 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
|
||||
return nil, err
|
||||
}
|
||||
case FilterModeCreate:
|
||||
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
|
||||
And("poster_id = ?", opts.UserID).
|
||||
stats.OpenCount, err = x.Where(cond).And("issue.is_closed = ?", false).
|
||||
And("issue.poster_id = ?", opts.UserID).
|
||||
Count(new(Issue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
|
||||
And("poster_id = ?", opts.UserID).
|
||||
stats.ClosedCount, err = x.Where(cond).And("issue.is_closed = ?", true).
|
||||
And("issue.poster_id = ?", opts.UserID).
|
||||
Count(new(Issue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1524,34 +1600,60 @@ func SearchIssueIDsByKeyword(kw string, repoIDs []int64, limit, start int) (int6
|
||||
return total, ids, nil
|
||||
}
|
||||
|
||||
func updateIssue(e Engine, issue *Issue) error {
|
||||
_, err := e.ID(issue.ID).AllCols().Update(issue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateIssue updates all fields of given issue.
|
||||
func UpdateIssue(issue *Issue) error {
|
||||
// UpdateIssueByAPI updates all allowed fields of given issue.
|
||||
// If the issue status is changed a statusChangeComment is returned
|
||||
// similarly if the title is changed the titleChanged bool is set to true
|
||||
func UpdateIssueByAPI(issue *Issue, doer *User) (statusChangeComment *Comment, titleChanged bool, err error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
return nil, false, err
|
||||
}
|
||||
if err := updateIssue(sess, issue); err != nil {
|
||||
return err
|
||||
|
||||
if err := issue.loadRepo(sess); err != nil {
|
||||
return nil, false, fmt.Errorf("loadRepo: %v", err)
|
||||
}
|
||||
if err := issue.neuterCrossReferences(sess); err != nil {
|
||||
return err
|
||||
|
||||
// Reload the issue
|
||||
currentIssue, err := getIssueByID(sess, issue.ID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := issue.loadPoster(sess); err != nil {
|
||||
return err
|
||||
|
||||
if _, err := sess.ID(issue.ID).Cols(
|
||||
"name", "content", "milestone_id", "priority",
|
||||
"deadline_unix", "updated_unix", "is_locked").
|
||||
Update(issue); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := issue.addCrossReferences(sess, issue.Poster); err != nil {
|
||||
return err
|
||||
|
||||
titleChanged = currentIssue.Title != issue.Title
|
||||
if titleChanged {
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: CommentTypeChangeTitle,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
OldTitle: currentIssue.Title,
|
||||
NewTitle: issue.Title,
|
||||
}
|
||||
_, err := createComment(sess, opts)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("createComment: %v", err)
|
||||
}
|
||||
}
|
||||
return sess.Commit()
|
||||
|
||||
if currentIssue.IsClosed != issue.IsClosed {
|
||||
statusChangeComment, err = issue.doChangeStatus(sess, doer, false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := issue.addCrossReferences(sess, doer, true); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return statusChangeComment, titleChanged, sess.Commit()
|
||||
}
|
||||
|
||||
// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
|
||||
@@ -1587,6 +1689,28 @@ type DependencyInfo struct {
|
||||
Repository `xorm:"extends"`
|
||||
}
|
||||
|
||||
// getParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
|
||||
func (issue *Issue) getParticipantIDsByIssue(e Engine) ([]int64, error) {
|
||||
if issue == nil {
|
||||
return nil, nil
|
||||
}
|
||||
userIDs := make([]int64, 0, 5)
|
||||
if err := e.Table("comment").Cols("poster_id").
|
||||
Where("`comment`.issue_id = ?", issue.ID).
|
||||
And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
|
||||
And("`user`.is_active = ?", true).
|
||||
And("`user`.prohibit_login = ?", false).
|
||||
Join("INNER", "`user`", "`user`.id = `comment`.poster_id").
|
||||
Distinct("poster_id").
|
||||
Find(&userIDs); err != nil {
|
||||
return nil, fmt.Errorf("get poster IDs: %v", err)
|
||||
}
|
||||
if !util.IsInt64InSlice(issue.PosterID, userIDs) {
|
||||
return append(userIDs, issue.PosterID), nil
|
||||
}
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
// Get Blocked By Dependencies, aka all issues this issue is blocked by.
|
||||
func (issue *Issue) getBlockedByDependencies(e Engine) (issueDeps []*DependencyInfo, err error) {
|
||||
return issueDeps, e.
|
||||
@@ -1769,3 +1893,83 @@ func UpdateIssuesMigrationsByType(gitServiceType structs.GitServiceType, origina
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
|
||||
func UpdateReactionsMigrationsByType(gitServiceType structs.GitServiceType, originalAuthorID string, userID int64) error {
|
||||
_, err := x.Table("reaction").
|
||||
Where("original_author_id = ?", originalAuthorID).
|
||||
And(migratedIssueCond(gitServiceType)).
|
||||
Update(map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"original_author": "",
|
||||
"original_author_id": 0,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func deleteIssuesByRepoID(sess Engine, repoID int64) (attachmentPaths []string, err error) {
|
||||
deleteCond := builder.Select("id").From("issue").Where(builder.Eq{"issue.repo_id": repoID})
|
||||
|
||||
// Delete comments and attachments
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&Comment{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Dependencies for issues in this repository
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&IssueDependency{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete dependencies for issues in other repositories
|
||||
if _, err = sess.In("dependency_id", deleteCond).
|
||||
Delete(&IssueDependency{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&IssueUser{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&Reaction{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&IssueWatch{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&Stopwatch{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&TrackedTime{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var attachments []*Attachment
|
||||
if err = sess.In("issue_id", deleteCond).
|
||||
Find(&attachments); err != nil {
|
||||
return
|
||||
}
|
||||
for j := range attachments {
|
||||
attachmentPaths = append(attachmentPaths, attachments[j].LocalPath())
|
||||
}
|
||||
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&Attachment{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = sess.Delete(&Issue{RepoID: repoID}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+38
-33
@@ -7,6 +7,8 @@ package models
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@@ -17,6 +19,11 @@ type IssueAssignees struct {
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// LoadAssignees load assignees of this issue.
|
||||
func (issue *Issue) LoadAssignees() error {
|
||||
return issue.loadAssignees(x)
|
||||
}
|
||||
|
||||
// This loads all assignees of an issue
|
||||
func (issue *Issue) loadAssignees(e Engine) (err error) {
|
||||
// Reset maybe preexisting assignees
|
||||
@@ -39,6 +46,18 @@ func (issue *Issue) loadAssignees(e Engine) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
||||
// but skips joining with `user` for performance reasons.
|
||||
// User permissions must be verified elsewhere if required.
|
||||
func GetAssigneeIDsByIssue(issueID int64) ([]int64, error) {
|
||||
userIDs := make([]int64, 0, 5)
|
||||
return userIDs, x.Table("issue_assignees").
|
||||
Cols("assignee_id").
|
||||
Where("issue_id = ?", issueID).
|
||||
Distinct("assignee_id").
|
||||
Find(&userIDs)
|
||||
}
|
||||
|
||||
// GetAssigneesByIssue returns everyone assigned to that issue
|
||||
func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) {
|
||||
return getAssigneesByIssue(x, issue)
|
||||
@@ -62,23 +81,6 @@ func isUserAssignedToIssue(e Engine, issue *Issue, user *User) (isAssigned bool,
|
||||
return e.Get(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
|
||||
}
|
||||
|
||||
// MakeAssigneeList concats a string with all names of the assignees. Useful for logs.
|
||||
func MakeAssigneeList(issue *Issue) (assigneeList string, err error) {
|
||||
err = issue.loadAssignees(x)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for in, assignee := range issue.Assignees {
|
||||
assigneeList += assignee.Name
|
||||
|
||||
if len(issue.Assignees) > (in + 1) {
|
||||
assigneeList += ", "
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ClearAssigneeByUserID deletes all assignments of an user
|
||||
func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) {
|
||||
_, err = sess.Delete(&IssueAssignees{AssigneeID: userID})
|
||||
@@ -117,10 +119,18 @@ func (issue *Issue) toggleAssignee(sess *xorm.Session, doer *User, assigneeID in
|
||||
return false, nil, fmt.Errorf("loadRepo: %v", err)
|
||||
}
|
||||
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: CommentTypeAssignees,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
RemovedAssignee: removed,
|
||||
AssigneeID: assigneeID,
|
||||
}
|
||||
// Comment
|
||||
comment, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed)
|
||||
comment, err = createComment(sess, opts)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("createAssigneeComment: %v", err)
|
||||
return false, nil, fmt.Errorf("createComment: %v", err)
|
||||
}
|
||||
|
||||
// if pull request is in the middle of creation - don't call webhook
|
||||
@@ -171,25 +181,20 @@ func toggleUserAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (remove
|
||||
// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
|
||||
func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) {
|
||||
|
||||
var requestAssignees []string
|
||||
|
||||
// Keeping the old assigning method for compatibility reasons
|
||||
if oneAssignee != "" {
|
||||
if oneAssignee != "" && !util.IsStringInSlice(oneAssignee, multipleAssignees) {
|
||||
requestAssignees = append(requestAssignees, oneAssignee)
|
||||
}
|
||||
|
||||
// Prevent double adding assignees
|
||||
var isDouble bool
|
||||
for _, assignee := range multipleAssignees {
|
||||
if assignee == oneAssignee {
|
||||
isDouble = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isDouble {
|
||||
multipleAssignees = append(multipleAssignees, oneAssignee)
|
||||
}
|
||||
//Prevent empty assignees
|
||||
if len(multipleAssignees) > 0 && multipleAssignees[0] != "" {
|
||||
requestAssignees = append(requestAssignees, multipleAssignees...)
|
||||
}
|
||||
|
||||
// Get the IDs of all assignees
|
||||
assigneeIDs, err = GetUserIDsByNames(multipleAssignees, false)
|
||||
assigneeIDs, err = GetUserIDsByNames(requestAssignees, false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -59,3 +59,24 @@ func TestUpdateAssignee(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isAssigned)
|
||||
}
|
||||
|
||||
func TestMakeIDsFromAPIAssigneesToAdd(t *testing.T) {
|
||||
IDs, err := MakeIDsFromAPIAssigneesToAdd("", []string{""})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{}, IDs)
|
||||
|
||||
IDs, err = MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"})
|
||||
assert.Error(t, err)
|
||||
|
||||
IDs, err = MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{1}, IDs)
|
||||
|
||||
IDs, err = MakeIDsFromAPIAssigneesToAdd("user2", []string{""})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{2}, IDs)
|
||||
|
||||
IDs, err = MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{1, 2}, IDs)
|
||||
}
|
||||
|
||||
+281
-152
@@ -7,8 +7,13 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -82,6 +87,16 @@ const (
|
||||
CommentTypeLock
|
||||
// Unlocks a previously locked issue
|
||||
CommentTypeUnlock
|
||||
// Change pull request's target branch
|
||||
CommentTypeChangeTargetBranch
|
||||
// Delete time manual for time tracking
|
||||
CommentTypeDeleteTimeManual
|
||||
// add or remove Request from one
|
||||
CommentTypeReviewRequest
|
||||
// merge pull request
|
||||
CommentTypeMergePull
|
||||
// push to PR head branch
|
||||
CommentTypePullPush
|
||||
)
|
||||
|
||||
// CommentTag defines comment tag type
|
||||
@@ -98,7 +113,7 @@ const (
|
||||
// Comment represents a comment in commit and issue page.
|
||||
type Comment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type CommentType `xorm:"index"`
|
||||
Type CommentType `xorm:"INDEX"`
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *User `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
@@ -114,8 +129,12 @@ type Comment struct {
|
||||
AssigneeID int64
|
||||
RemovedAssignee bool
|
||||
Assignee *User `xorm:"-"`
|
||||
ResolveDoerID int64
|
||||
ResolveDoer *User `xorm:"-"`
|
||||
OldTitle string
|
||||
NewTitle string
|
||||
OldRef string
|
||||
NewRef string
|
||||
DependentIssueID int64
|
||||
DependentIssue *Issue `xorm:"-"`
|
||||
|
||||
@@ -126,7 +145,8 @@ type Comment struct {
|
||||
RenderedContent string `xorm:"-"`
|
||||
|
||||
// Path represents the 4 lines of code cemented by this comment
|
||||
Patch string `xorm:"TEXT"`
|
||||
Patch string `xorm:"-"`
|
||||
PatchQuoted string `xorm:"TEXT patch"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
@@ -155,6 +175,18 @@ type Comment struct {
|
||||
RefRepo *Repository `xorm:"-"`
|
||||
RefIssue *Issue `xorm:"-"`
|
||||
RefComment *Comment `xorm:"-"`
|
||||
|
||||
Commits *list.List `xorm:"-"`
|
||||
OldCommit string `xorm:"-"`
|
||||
NewCommit string `xorm:"-"`
|
||||
CommitsNum int64 `xorm:"-"`
|
||||
IsForcePush bool `xorm:"-"`
|
||||
}
|
||||
|
||||
// PushActionContent is content of push pull comment
|
||||
type PushActionContent struct {
|
||||
IsForcePush bool `json:"is_force_push"`
|
||||
CommitIDs []string `json:"commit_ids"`
|
||||
}
|
||||
|
||||
// LoadIssue loads issue from database
|
||||
@@ -170,6 +202,33 @@ func (c *Comment) loadIssue(e Engine) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// BeforeInsert will be invoked by XORM before inserting a record
|
||||
func (c *Comment) BeforeInsert() {
|
||||
c.PatchQuoted = c.Patch
|
||||
if !utf8.ValidString(c.Patch) {
|
||||
c.PatchQuoted = strconv.Quote(c.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeUpdate will be invoked by XORM before updating a record
|
||||
func (c *Comment) BeforeUpdate() {
|
||||
c.PatchQuoted = c.Patch
|
||||
if !utf8.ValidString(c.Patch) {
|
||||
c.PatchQuoted = strconv.Quote(c.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||
func (c *Comment) AfterLoad(session *xorm.Session) {
|
||||
c.Patch = c.PatchQuoted
|
||||
if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' {
|
||||
unquoted, err := strconv.Unquote(c.PatchQuoted)
|
||||
if err == nil {
|
||||
c.Patch = unquoted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Comment) loadPoster(e Engine) (err error) {
|
||||
if c.PosterID <= 0 || c.Poster != nil {
|
||||
return nil
|
||||
@@ -229,6 +288,22 @@ func (c *Comment) HTMLURL() string {
|
||||
return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||
}
|
||||
|
||||
// APIURL formats a API-string to the issue-comment
|
||||
func (c *Comment) APIURL() string {
|
||||
err := c.LoadIssue()
|
||||
if err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadIssue(%d): %v", c.IssueID, err)
|
||||
return ""
|
||||
}
|
||||
err = c.Issue.loadRepo(x)
|
||||
if err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID)
|
||||
}
|
||||
|
||||
// IssueURL formats a URL-string to the issue
|
||||
func (c *Comment) IssueURL() string {
|
||||
err := c.LoadIssue()
|
||||
@@ -394,6 +469,26 @@ func (c *Comment) LoadAssigneeUser() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadResolveDoer if comment.Type is CommentTypeCode and ResolveDoerID not zero, then load resolveDoer
|
||||
func (c *Comment) LoadResolveDoer() (err error) {
|
||||
if c.ResolveDoerID == 0 || c.Type != CommentTypeCode {
|
||||
return nil
|
||||
}
|
||||
c.ResolveDoer, err = getUserByID(x, c.ResolveDoerID)
|
||||
if err != nil {
|
||||
if IsErrUserNotExist(err) {
|
||||
c.ResolveDoer = NewGhostUser()
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IsResolved check if an code comment is resolved
|
||||
func (c *Comment) IsResolved() bool {
|
||||
return c.ResolveDoerID != 0 && c.Type == CommentTypeCode
|
||||
}
|
||||
|
||||
// LoadDepIssueDetails loads Dependent Issue Details
|
||||
func (c *Comment) LoadDepIssueDetails() (err error) {
|
||||
if c.DependentIssueID <= 0 || c.DependentIssue != nil {
|
||||
@@ -403,7 +498,7 @@ func (c *Comment) LoadDepIssueDetails() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Comment) loadReactions(e Engine) (err error) {
|
||||
func (c *Comment) loadReactions(e Engine, repo *Repository) (err error) {
|
||||
if c.Reactions != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -415,15 +510,15 @@ func (c *Comment) loadReactions(e Engine) (err error) {
|
||||
return err
|
||||
}
|
||||
// Load reaction user data
|
||||
if _, err := c.Reactions.LoadUsers(); err != nil {
|
||||
if _, err := c.Reactions.loadUsers(e, repo); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadReactions loads comment reactions
|
||||
func (c *Comment) LoadReactions() error {
|
||||
return c.loadReactions(x)
|
||||
func (c *Comment) LoadReactions(repo *Repository) error {
|
||||
return c.loadReactions(x, repo)
|
||||
}
|
||||
|
||||
func (c *Comment) loadReview(e Engine) (err error) {
|
||||
@@ -441,10 +536,12 @@ func (c *Comment) LoadReview() error {
|
||||
return c.loadReview(x)
|
||||
}
|
||||
|
||||
var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
|
||||
|
||||
func (c *Comment) checkInvalidation(doer *User, repo *git.Repository, branch string) error {
|
||||
// FIXME differentiate between previous and proposed line
|
||||
commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
|
||||
if err != nil && strings.Contains(err.Error(), "fatal: no such path") {
|
||||
if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
|
||||
c.Invalidated = true
|
||||
return UpdateComment(c, doer)
|
||||
}
|
||||
@@ -495,6 +592,47 @@ func (c *Comment) CodeCommentURL() string {
|
||||
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||
}
|
||||
|
||||
// LoadPushCommits Load push commits
|
||||
func (c *Comment) LoadPushCommits() (err error) {
|
||||
if c.Content == "" || c.Commits != nil || c.Type != CommentTypePullPush {
|
||||
return nil
|
||||
}
|
||||
|
||||
var data PushActionContent
|
||||
|
||||
err = json.Unmarshal([]byte(c.Content), &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.IsForcePush = data.IsForcePush
|
||||
|
||||
if c.IsForcePush {
|
||||
if len(data.CommitIDs) != 2 {
|
||||
return nil
|
||||
}
|
||||
c.OldCommit = data.CommitIDs[0]
|
||||
c.NewCommit = data.CommitIDs[1]
|
||||
} else {
|
||||
repoPath := c.Issue.Repo.RepoPath()
|
||||
gitRepo, err := git.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
c.Commits = gitRepo.GetCommitsFromIDs(data.CommitIDs)
|
||||
c.CommitsNum = int64(c.Commits.Len())
|
||||
if c.CommitsNum > 0 {
|
||||
c.Commits = ValidateCommitsWithEmails(c.Commits)
|
||||
c.Commits = ParseCommitsWithSignature(c.Commits, c.Issue.Repo)
|
||||
c.Commits = ParseCommitsWithStatus(c.Commits, c.Issue.Repo)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
|
||||
var LabelID int64
|
||||
if opts.Label != nil {
|
||||
@@ -517,6 +655,8 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||
Content: opts.Content,
|
||||
OldTitle: opts.OldTitle,
|
||||
NewTitle: opts.NewTitle,
|
||||
OldRef: opts.OldRef,
|
||||
NewRef: opts.NewRef,
|
||||
DependentIssueID: opts.DependentIssueID,
|
||||
TreePath: opts.TreePath,
|
||||
ReviewID: opts.ReviewID,
|
||||
@@ -526,6 +666,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||
RefCommentID: opts.RefCommentID,
|
||||
RefAction: opts.RefAction,
|
||||
RefIsPull: opts.RefIsPull,
|
||||
IsForcePush: opts.IsForcePush,
|
||||
}
|
||||
if _, err = e.Insert(comment); err != nil {
|
||||
return nil, err
|
||||
@@ -535,30 +676,18 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = sendCreateCommentAction(e, opts, comment); err != nil {
|
||||
if err = updateCommentInfos(e, opts, comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = comment.addCrossReferences(e, opts.Doer); err != nil {
|
||||
if err = comment.addCrossReferences(e, opts.Doer, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) {
|
||||
// Compose comment action, could be plain comment, close or reopen issue/pull request.
|
||||
// This object will be used to notify watchers in the end of function.
|
||||
act := &Action{
|
||||
ActUserID: opts.Doer.ID,
|
||||
ActUser: opts.Doer,
|
||||
Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]),
|
||||
RepoID: opts.Repo.ID,
|
||||
Repo: opts.Repo,
|
||||
Comment: comment,
|
||||
CommentID: comment.ID,
|
||||
IsPrivate: opts.Repo.IsPrivate,
|
||||
}
|
||||
func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) {
|
||||
// Check comment type.
|
||||
switch opts.Type {
|
||||
case CommentTypeCode:
|
||||
@@ -574,23 +703,14 @@ func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, commen
|
||||
}
|
||||
fallthrough
|
||||
case CommentTypeComment:
|
||||
act.OpType = ActionCommentIssue
|
||||
|
||||
if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check attachments
|
||||
attachments := make([]*Attachment, 0, len(opts.Attachments))
|
||||
for _, uuid := range opts.Attachments {
|
||||
attach, err := getAttachmentByUUID(e, uuid)
|
||||
if err != nil {
|
||||
if IsErrAttachmentNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
|
||||
}
|
||||
attachments = append(attachments, attach)
|
||||
attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
|
||||
}
|
||||
|
||||
for i := range attachments {
|
||||
@@ -601,92 +721,16 @@ func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, commen
|
||||
return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
case CommentTypeReopen:
|
||||
act.OpType = ActionReopenIssue
|
||||
if opts.Issue.IsPull {
|
||||
act.OpType = ActionReopenPullRequest
|
||||
}
|
||||
|
||||
if err = opts.Issue.updateClosedNum(e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case CommentTypeClose:
|
||||
act.OpType = ActionCloseIssue
|
||||
if opts.Issue.IsPull {
|
||||
act.OpType = ActionClosePullRequest
|
||||
}
|
||||
|
||||
case CommentTypeReopen, CommentTypeClose:
|
||||
if err = opts.Issue.updateClosedNum(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// update the issue's updated_unix column
|
||||
if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Notify watchers for whatever action comes in, ignore if no action type.
|
||||
if act.OpType > 0 {
|
||||
if err = notifyWatchers(e, act); err != nil {
|
||||
log.Error("notifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createStatusComment(e *xorm.Session, doer *User, issue *Issue) (*Comment, error) {
|
||||
cmtType := CommentTypeClose
|
||||
if !issue.IsClosed {
|
||||
cmtType = CommentTypeReopen
|
||||
}
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: cmtType,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
})
|
||||
}
|
||||
|
||||
func createLabelComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, label *Label, add bool) (*Comment, error) {
|
||||
var content string
|
||||
if add {
|
||||
content = "1"
|
||||
}
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeLabel,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
Label: label,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldMilestoneID, milestoneID int64) (*Comment, error) {
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeMilestone,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
OldMilestoneID: oldMilestoneID,
|
||||
MilestoneID: milestoneID,
|
||||
})
|
||||
}
|
||||
|
||||
func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, assigneeID int64, removedAssignee bool) (*Comment, error) {
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeAssignees,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
RemovedAssignee: removedAssignee,
|
||||
AssigneeID: assigneeID,
|
||||
})
|
||||
return updateIssueCols(e, opts.Issue, "updated_unix")
|
||||
}
|
||||
|
||||
func createDeadlineComment(e *xorm.Session, doer *User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
|
||||
|
||||
var content string
|
||||
var commentType CommentType
|
||||
|
||||
@@ -708,34 +752,18 @@ func createDeadlineComment(e *xorm.Session, doer *User, issue *Issue, newDeadlin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: commentType,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
func createChangeTitleComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldTitle, newTitle string) (*Comment, error) {
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeChangeTitle,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
OldTitle: oldTitle,
|
||||
NewTitle: newTitle,
|
||||
})
|
||||
}
|
||||
|
||||
func createDeleteBranchComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, branchName string) (*Comment, error) {
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeDeleteBranch,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
CommitSHA: branchName,
|
||||
})
|
||||
}
|
||||
comment, err := createComment(e, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// Creates issue dependency comment
|
||||
@@ -749,28 +777,25 @@ func createIssueDependencyComment(e *xorm.Session, doer *User, issue *Issue, dep
|
||||
}
|
||||
|
||||
// Make two comments, one in each issue
|
||||
_, err = createComment(e, &CreateCommentOptions{
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: cType,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
DependentIssueID: dependentIssue.ID,
|
||||
})
|
||||
if err != nil {
|
||||
}
|
||||
if _, err = createComment(e, opts); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = createComment(e, &CreateCommentOptions{
|
||||
opts = &CreateCommentOptions{
|
||||
Type: cType,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: dependentIssue,
|
||||
DependentIssueID: issue.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = createComment(e, opts)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -789,6 +814,8 @@ type CreateCommentOptions struct {
|
||||
RemovedAssignee bool
|
||||
OldTitle string
|
||||
NewTitle string
|
||||
OldRef string
|
||||
NewRef string
|
||||
CommitID int64
|
||||
CommitSHA string
|
||||
Patch string
|
||||
@@ -802,6 +829,7 @@ type CreateCommentOptions struct {
|
||||
RefCommentID int64
|
||||
RefAction references.XRefAction
|
||||
RefIsPull bool
|
||||
IsForcePush bool
|
||||
}
|
||||
|
||||
// CreateComment creates comment of issue or commit.
|
||||
@@ -855,8 +883,12 @@ func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commi
|
||||
|
||||
// GetCommentByID returns the comment by given ID.
|
||||
func GetCommentByID(id int64) (*Comment, error) {
|
||||
return getCommentByID(x, id)
|
||||
}
|
||||
|
||||
func getCommentByID(e Engine, id int64) (*Comment, error) {
|
||||
c := new(Comment)
|
||||
has, err := x.ID(id).Get(c)
|
||||
has, err := e.ID(id).Get(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -867,10 +899,12 @@ func GetCommentByID(id int64) (*Comment, error) {
|
||||
|
||||
// FindCommentsOptions describes the conditions to Find comments
|
||||
type FindCommentsOptions struct {
|
||||
ListOptions
|
||||
RepoID int64
|
||||
IssueID int64
|
||||
ReviewID int64
|
||||
Since int64
|
||||
Before int64
|
||||
Type CommentType
|
||||
}
|
||||
|
||||
@@ -888,6 +922,9 @@ func (opts *FindCommentsOptions) toConds() builder.Cond {
|
||||
if opts.Since > 0 {
|
||||
cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
|
||||
}
|
||||
if opts.Before > 0 {
|
||||
cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before})
|
||||
}
|
||||
if opts.Type != CommentTypeUnknown {
|
||||
cond = cond.And(builder.Eq{"comment.type": opts.Type})
|
||||
}
|
||||
@@ -900,6 +937,11 @@ func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) {
|
||||
if opts.RepoID > 0 {
|
||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||
}
|
||||
|
||||
if opts.Page != 0 {
|
||||
sess = opts.setSessionPagination(sess)
|
||||
}
|
||||
|
||||
return comments, sess.
|
||||
Asc("comment.created_unix").
|
||||
Asc("comment.id").
|
||||
@@ -925,10 +967,7 @@ func UpdateComment(c *Comment, doer *User) error {
|
||||
if err := c.loadIssue(sess); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.neuterCrossReferences(sess); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.addCrossReferences(sess, doer); err != nil {
|
||||
if err := c.addCrossReferences(sess, doer, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sess.Commit(); err != nil {
|
||||
@@ -999,10 +1038,6 @@ func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := CommentList(comments).loadPosters(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := issue.loadRepo(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1022,7 +1057,12 @@ func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review
|
||||
if err := e.In("id", ids).Find(&reviews); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if err := comment.LoadResolveDoer(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
|
||||
// If the review is pending only the author can see the comments (except the review is set)
|
||||
if review.ID == 0 {
|
||||
@@ -1068,3 +1108,92 @@ func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// CreatePushPullComment create push code to pull base commend
|
||||
func CreatePushPullComment(pusher *User, pr *PullRequest, oldCommitID, newCommitID string) (comment *Comment, err error) {
|
||||
if pr.HasMerged || oldCommitID == "" || newCommitID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ops := &CreateCommentOptions{
|
||||
Type: CommentTypePullPush,
|
||||
Doer: pusher,
|
||||
Repo: pr.BaseRepo,
|
||||
}
|
||||
|
||||
var data PushActionContent
|
||||
|
||||
data.CommitIDs, data.IsForcePush, err = getCommitIDsFromRepo(pr.BaseRepo, oldCommitID, newCommitID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ops.Issue = pr.Issue
|
||||
dataJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ops.Content = string(dataJSON)
|
||||
|
||||
comment, err = CreateComment(ops)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getCommitsFromRepo get commit IDs from repo in betwern oldCommitID and newCommitID
|
||||
// isForcePush will be true if oldCommit isn't on the branch
|
||||
// Commit on baseBranch will skip
|
||||
func getCommitIDsFromRepo(repo *Repository, oldCommitID, newCommitID, baseBranch string) (commitIDs []string, isForcePush bool, err error) {
|
||||
repoPath := repo.RepoPath()
|
||||
gitRepo, err := git.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
oldCommit, err := gitRepo.GetCommit(oldCommitID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
oldCommitBranch, err := oldCommit.GetBranchName()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if oldCommitBranch == "" {
|
||||
commitIDs = make([]string, 2)
|
||||
commitIDs[0] = oldCommitID
|
||||
commitIDs[1] = newCommitID
|
||||
|
||||
return commitIDs, true, err
|
||||
}
|
||||
|
||||
newCommit, err := gitRepo.GetCommit(newCommitID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var commits *list.List
|
||||
commits, err = newCommit.CommitsBeforeUntil(oldCommitID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
commitIDs = make([]string, 0, commits.Len())
|
||||
|
||||
for e := commits.Back(); e != nil; e = e.Prev() {
|
||||
commit := e.Value.(*git.Commit)
|
||||
commitBranch, err := commit.GetBranchName()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if commitBranch != baseBranch {
|
||||
commitIDs = append(commitIDs, commit.ID.String())
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -376,6 +376,11 @@ func (comments CommentList) loadDependentIssues(e Engine) error {
|
||||
for _, comment := range comments {
|
||||
if comment.DependentIssue == nil {
|
||||
comment.DependentIssue = issues[comment.DependentIssueID]
|
||||
if comment.DependentIssue != nil {
|
||||
if err := comment.DependentIssue.loadRepo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestCreateIssueDependency(t *testing.T) {
|
||||
assert.False(t, left)
|
||||
|
||||
// Close #2 and check again
|
||||
err = issue2.ChangeStatus(user1, true)
|
||||
_, err = issue2.ChangeStatus(user1, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
left, err = IssueNoDependenciesLeft(issue1)
|
||||
|
||||
+365
-113
@@ -1,4 +1,5 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@@ -11,20 +12,37 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
var labelColorPattern = regexp.MustCompile("#([a-fA-F0-9]{6})")
|
||||
// LabelColorPattern is a regexp witch can validate LabelColor
|
||||
var LabelColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||
|
||||
// Label represents a label of repository for issues.
|
||||
type Label struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
Description string
|
||||
Color string `xorm:"VARCHAR(7)"`
|
||||
NumIssues int
|
||||
NumClosedIssues int
|
||||
NumOpenIssues int `xorm:"-"`
|
||||
NumOpenRepoIssues int64 `xorm:"-"`
|
||||
IsChecked bool `xorm:"-"`
|
||||
QueryString string `xorm:"-"`
|
||||
IsSelected bool `xorm:"-"`
|
||||
IsExcluded bool `xorm:"-"`
|
||||
}
|
||||
|
||||
// GetLabelTemplateFile loads the label template file by given name,
|
||||
// then parses and returns a list of name-color pairs and optionally description.
|
||||
func GetLabelTemplateFile(name string) ([][3]string, error) {
|
||||
data, err := getRepoInitFile("label", name)
|
||||
data, err := GetRepoInitFile("label", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getRepoInitFile: %v", err)
|
||||
return nil, fmt.Errorf("GetRepoInitFile: %v", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
@@ -42,7 +60,11 @@ func GetLabelTemplateFile(name string) ([][3]string, error) {
|
||||
return nil, fmt.Errorf("line is malformed: %s", line)
|
||||
}
|
||||
|
||||
if !labelColorPattern.MatchString(fields[0]) {
|
||||
color := strings.Trim(fields[0], " ")
|
||||
if len(color) == 6 {
|
||||
color = "#" + color
|
||||
}
|
||||
if !LabelColorPattern.MatchString(color) {
|
||||
return nil, fmt.Errorf("bad HTML color code in line: %s", line)
|
||||
}
|
||||
|
||||
@@ -53,43 +75,32 @@ func GetLabelTemplateFile(name string) ([][3]string, error) {
|
||||
}
|
||||
|
||||
fields[1] = strings.TrimSpace(fields[1])
|
||||
list = append(list, [3]string{fields[1], fields[0], description})
|
||||
list = append(list, [3]string{fields[1], color, description})
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// Label represents a label of repository for issues.
|
||||
type Label struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
Description string
|
||||
Color string `xorm:"VARCHAR(7)"`
|
||||
NumIssues int
|
||||
NumClosedIssues int
|
||||
NumOpenIssues int `xorm:"-"`
|
||||
IsChecked bool `xorm:"-"`
|
||||
QueryString string `xorm:"-"`
|
||||
IsSelected bool `xorm:"-"`
|
||||
IsExcluded bool `xorm:"-"`
|
||||
}
|
||||
|
||||
// APIFormat converts a Label to the api.Label format
|
||||
func (label *Label) APIFormat() *api.Label {
|
||||
return &api.Label{
|
||||
ID: label.ID,
|
||||
Name: label.Name,
|
||||
Color: strings.TrimLeft(label.Color, "#"),
|
||||
Description: label.Description,
|
||||
}
|
||||
}
|
||||
|
||||
// CalOpenIssues calculates the open issues of label.
|
||||
// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues.
|
||||
func (label *Label) CalOpenIssues() {
|
||||
label.NumOpenIssues = label.NumIssues - label.NumClosedIssues
|
||||
}
|
||||
|
||||
// CalOpenOrgIssues calculates the open issues of a label for a specific repo
|
||||
func (label *Label) CalOpenOrgIssues(repoID, labelID int64) {
|
||||
repoIDs := []int64{repoID}
|
||||
labelIDs := []int64{labelID}
|
||||
|
||||
counts, _ := CountIssuesByRepo(&IssuesOptions{
|
||||
RepoIDs: repoIDs,
|
||||
LabelIDs: labelIDs,
|
||||
})
|
||||
|
||||
for _, count := range counts {
|
||||
label.NumOpenRepoIssues += count
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
|
||||
func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) {
|
||||
var labelQuerySlice []string
|
||||
@@ -112,6 +123,16 @@ func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64)
|
||||
label.QueryString = strings.Join(labelQuerySlice, ",")
|
||||
}
|
||||
|
||||
// BelongsToOrg returns true if label is an organization label
|
||||
func (label *Label) BelongsToOrg() bool {
|
||||
return label.OrgID > 0
|
||||
}
|
||||
|
||||
// BelongsToRepo returns true if label is a repository label
|
||||
func (label *Label) BelongsToRepo() bool {
|
||||
return label.RepoID > 0
|
||||
}
|
||||
|
||||
// ForegroundColor calculates the text color for labels based
|
||||
// on their background color.
|
||||
func (label *Label) ForegroundColor() template.CSS {
|
||||
@@ -132,7 +153,32 @@ func (label *Label) ForegroundColor() template.CSS {
|
||||
return template.CSS("#000")
|
||||
}
|
||||
|
||||
func initalizeLabels(e Engine, repoID int64, labelTemplate string) error {
|
||||
// .____ ___. .__
|
||||
// | | _____ \_ |__ ____ | |
|
||||
// | | \__ \ | __ \_/ __ \| |
|
||||
// | |___ / __ \| \_\ \ ___/| |__
|
||||
// >_______ (____ /___ /\___ >____/
|
||||
|
||||
func loadLabels(labelTemplate string) ([]string, error) {
|
||||
list, err := GetLabelTemplateFile(labelTemplate)
|
||||
if err != nil {
|
||||
return nil, ErrIssueLabelTemplateLoad{labelTemplate, err}
|
||||
}
|
||||
|
||||
labels := make([]string, len(list))
|
||||
for i := 0; i < len(list); i++ {
|
||||
labels[i] = list[i][0]
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
// LoadLabelsFormatted loads the labels' list of a template file as a string separated by comma
|
||||
func LoadLabelsFormatted(labelTemplate string) (string, error) {
|
||||
labels, err := loadLabels(labelTemplate)
|
||||
return strings.Join(labels, ", "), err
|
||||
}
|
||||
|
||||
func initializeLabels(e Engine, id int64, labelTemplate string, isOrg bool) error {
|
||||
list, err := GetLabelTemplateFile(labelTemplate)
|
||||
if err != nil {
|
||||
return ErrIssueLabelTemplateLoad{labelTemplate, err}
|
||||
@@ -141,11 +187,15 @@ func initalizeLabels(e Engine, repoID int64, labelTemplate string) error {
|
||||
labels := make([]*Label, len(list))
|
||||
for i := 0; i < len(list); i++ {
|
||||
labels[i] = &Label{
|
||||
RepoID: repoID,
|
||||
Name: list[i][0],
|
||||
Description: list[i][2],
|
||||
Color: list[i][1],
|
||||
}
|
||||
if isOrg {
|
||||
labels[i].OrgID = id
|
||||
} else {
|
||||
labels[i].RepoID = id
|
||||
}
|
||||
}
|
||||
for _, label := range labels {
|
||||
if err = newLabel(e, label); err != nil {
|
||||
@@ -155,9 +205,9 @@ func initalizeLabels(e Engine, repoID int64, labelTemplate string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitalizeLabels adds a label set to a repository using a template
|
||||
func InitalizeLabels(repoID int64, labelTemplate string) error {
|
||||
return initalizeLabels(x, repoID, labelTemplate)
|
||||
// InitializeLabels adds a label set to a repository using a template
|
||||
func InitializeLabels(ctx DBContext, repoID int64, labelTemplate string, isOrg bool) error {
|
||||
return initializeLabels(ctx.e, repoID, labelTemplate, isOrg)
|
||||
}
|
||||
|
||||
func newLabel(e Engine, label *Label) error {
|
||||
@@ -165,12 +215,15 @@ func newLabel(e Engine, label *Label) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// NewLabel creates a new label for a repository
|
||||
// NewLabel creates a new label
|
||||
func NewLabel(label *Label) error {
|
||||
if !LabelColorPattern.MatchString(label.Color) {
|
||||
return fmt.Errorf("bad color code: %s", label.Color)
|
||||
}
|
||||
return newLabel(x, label)
|
||||
}
|
||||
|
||||
// NewLabels creates new labels for a repository.
|
||||
// NewLabels creates new labels
|
||||
func NewLabels(labels ...*Label) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
@@ -178,6 +231,9 @@ func NewLabels(labels ...*Label) error {
|
||||
return err
|
||||
}
|
||||
for _, label := range labels {
|
||||
if !LabelColorPattern.MatchString(label.Color) {
|
||||
return fmt.Errorf("bad color code: %s", label.Color)
|
||||
}
|
||||
if err := newLabel(sess, label); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -185,12 +241,96 @@ func NewLabels(labels ...*Label) error {
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// UpdateLabel updates label information.
|
||||
func UpdateLabel(l *Label) error {
|
||||
if !LabelColorPattern.MatchString(l.Color) {
|
||||
return fmt.Errorf("bad color code: %s", l.Color)
|
||||
}
|
||||
return updateLabelCols(x, l, "name", "description", "color")
|
||||
}
|
||||
|
||||
// DeleteLabel delete a label
|
||||
func DeleteLabel(id, labelID int64) error {
|
||||
|
||||
label, err := GetLabelByID(labelID)
|
||||
if err != nil {
|
||||
if IsErrLabelNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if label.BelongsToOrg() && label.OrgID != id {
|
||||
return nil
|
||||
}
|
||||
if label.BelongsToRepo() && label.RepoID != id {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
|
||||
return err
|
||||
} else if _, err = sess.
|
||||
Where("label_id = ?", labelID).
|
||||
Delete(new(IssueLabel)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete comments about now deleted label_id
|
||||
if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// getLabelByID returns a label by label id
|
||||
func getLabelByID(e Engine, labelID int64) (*Label, error) {
|
||||
if labelID <= 0 {
|
||||
return nil, ErrLabelNotExist{labelID}
|
||||
}
|
||||
|
||||
l := &Label{}
|
||||
has, err := e.ID(labelID).Get(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrLabelNotExist{l.ID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetLabelByID returns a label by given ID.
|
||||
func GetLabelByID(id int64) (*Label, error) {
|
||||
return getLabelByID(x, id)
|
||||
}
|
||||
|
||||
// GetLabelsByIDs returns a list of labels by IDs
|
||||
func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) {
|
||||
labels := make([]*Label, 0, len(labelIDs))
|
||||
return labels, x.Table("label").
|
||||
In("id", labelIDs).
|
||||
Asc("name").
|
||||
Cols("id").
|
||||
Find(&labels)
|
||||
}
|
||||
|
||||
// __________ .__ __
|
||||
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
||||
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
||||
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
|
||||
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
|
||||
// \/ \/|__| \/ \/
|
||||
|
||||
// getLabelInRepoByName returns a label by Name in given repository.
|
||||
// If pass repoID as 0, then ORM will ignore limitation of repository
|
||||
// and can return arbitrary label with any valid ID.
|
||||
func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, error) {
|
||||
if len(labelName) == 0 {
|
||||
return nil, ErrLabelNotExist{0, repoID}
|
||||
if len(labelName) == 0 || repoID <= 0 {
|
||||
return nil, ErrRepoLabelNotExist{0, repoID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
@@ -201,17 +341,15 @@ func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrLabelNotExist{0, l.RepoID}
|
||||
return nil, ErrRepoLabelNotExist{0, l.RepoID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// getLabelInRepoByID returns a label by ID in given repository.
|
||||
// If pass repoID as 0, then ORM will ignore limitation of repository
|
||||
// and can return arbitrary label with any valid ID.
|
||||
func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
|
||||
if labelID <= 0 {
|
||||
return nil, ErrLabelNotExist{labelID, repoID}
|
||||
if labelID <= 0 || repoID <= 0 {
|
||||
return nil, ErrRepoLabelNotExist{labelID, repoID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
@@ -222,16 +360,11 @@ func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrLabelNotExist{l.ID, l.RepoID}
|
||||
return nil, ErrRepoLabelNotExist{l.ID, l.RepoID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetLabelByID returns a label by given ID.
|
||||
func GetLabelByID(id int64) (*Label, error) {
|
||||
return getLabelInRepoByID(x, 0, id)
|
||||
}
|
||||
|
||||
// GetLabelInRepoByName returns a label by name in given repository.
|
||||
func GetLabelInRepoByName(repoID int64, labelName string) (*Label, error) {
|
||||
return getLabelInRepoByName(x, repoID, labelName)
|
||||
@@ -250,17 +383,15 @@ func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error
|
||||
Find(&labelIDs)
|
||||
}
|
||||
|
||||
// GetLabelIDsInReposByNames returns a list of labelIDs by names in one of the given
|
||||
// repositories.
|
||||
// it silently ignores label names that do not belong to the repository.
|
||||
func GetLabelIDsInReposByNames(repoIDs []int64, labelNames []string) ([]int64, error) {
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
return labelIDs, x.Table("label").
|
||||
In("repo_id", repoIDs).
|
||||
In("name", labelNames).
|
||||
Asc("name").
|
||||
Cols("id").
|
||||
Find(&labelIDs)
|
||||
// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
|
||||
func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
|
||||
return builder.Select("issue_label.issue_id").
|
||||
From("issue_label").
|
||||
InnerJoin("label", "label.id = issue_label.label_id").
|
||||
Where(
|
||||
builder.In("label.name", labelNames),
|
||||
).
|
||||
GroupBy("issue_label.issue_id")
|
||||
}
|
||||
|
||||
// GetLabelInRepoByID returns a label by ID in given repository.
|
||||
@@ -279,10 +410,12 @@ func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
|
||||
Find(&labels)
|
||||
}
|
||||
|
||||
// GetLabelsByRepoID returns all labels that belong to given repository by ID.
|
||||
func GetLabelsByRepoID(repoID int64, sortType string) ([]*Label, error) {
|
||||
func getLabelsByRepoID(e Engine, repoID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
|
||||
if repoID <= 0 {
|
||||
return nil, ErrRepoLabelNotExist{0, repoID}
|
||||
}
|
||||
labels := make([]*Label, 0, 10)
|
||||
sess := x.Where("repo_id = ?", repoID)
|
||||
sess := e.Where("repo_id = ?", repoID)
|
||||
|
||||
switch sortType {
|
||||
case "reversealphabetically":
|
||||
@@ -295,9 +428,150 @@ func GetLabelsByRepoID(repoID int64, sortType string) ([]*Label, error) {
|
||||
sess.Asc("name")
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = listOptions.setSessionPagination(sess)
|
||||
}
|
||||
|
||||
return labels, sess.Find(&labels)
|
||||
}
|
||||
|
||||
// GetLabelsByRepoID returns all labels that belong to given repository by ID.
|
||||
func GetLabelsByRepoID(repoID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
|
||||
return getLabelsByRepoID(x, repoID, sortType, listOptions)
|
||||
}
|
||||
|
||||
// ________
|
||||
// \_____ \_______ ____
|
||||
// / | \_ __ \/ ___\
|
||||
// / | \ | \/ /_/ >
|
||||
// \_______ /__| \___ /
|
||||
// \/ /_____/
|
||||
|
||||
// getLabelInOrgByName returns a label by Name in given organization
|
||||
func getLabelInOrgByName(e Engine, orgID int64, labelName string) (*Label, error) {
|
||||
if len(labelName) == 0 || orgID <= 0 {
|
||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
Name: labelName,
|
||||
OrgID: orgID,
|
||||
}
|
||||
has, err := e.Get(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrOrgLabelNotExist{0, l.OrgID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// getLabelInOrgByID returns a label by ID in given organization.
|
||||
func getLabelInOrgByID(e Engine, orgID, labelID int64) (*Label, error) {
|
||||
if labelID <= 0 || orgID <= 0 {
|
||||
return nil, ErrOrgLabelNotExist{labelID, orgID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
ID: labelID,
|
||||
OrgID: orgID,
|
||||
}
|
||||
has, err := e.Get(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrOrgLabelNotExist{l.ID, l.OrgID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetLabelInOrgByName returns a label by name in given organization.
|
||||
func GetLabelInOrgByName(orgID int64, labelName string) (*Label, error) {
|
||||
return getLabelInOrgByName(x, orgID, labelName)
|
||||
}
|
||||
|
||||
// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given
|
||||
// organization.
|
||||
func GetLabelIDsInOrgByNames(orgID int64, labelNames []string) ([]int64, error) {
|
||||
if orgID <= 0 {
|
||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||
}
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
|
||||
return labelIDs, x.Table("label").
|
||||
Where("org_id = ?", orgID).
|
||||
In("name", labelNames).
|
||||
Asc("name").
|
||||
Cols("id").
|
||||
Find(&labelIDs)
|
||||
}
|
||||
|
||||
// GetLabelIDsInOrgsByNames returns a list of labelIDs by names in one of the given
|
||||
// organization.
|
||||
// it silently ignores label names that do not belong to the organization.
|
||||
func GetLabelIDsInOrgsByNames(orgIDs []int64, labelNames []string) ([]int64, error) {
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
return labelIDs, x.Table("label").
|
||||
In("org_id", orgIDs).
|
||||
In("name", labelNames).
|
||||
Asc("name").
|
||||
Cols("id").
|
||||
Find(&labelIDs)
|
||||
}
|
||||
|
||||
// GetLabelInOrgByID returns a label by ID in given organization.
|
||||
func GetLabelInOrgByID(orgID, labelID int64) (*Label, error) {
|
||||
return getLabelInOrgByID(x, orgID, labelID)
|
||||
}
|
||||
|
||||
// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization,
|
||||
// it silently ignores label IDs that do not belong to the organization.
|
||||
func GetLabelsInOrgByIDs(orgID int64, labelIDs []int64) ([]*Label, error) {
|
||||
labels := make([]*Label, 0, len(labelIDs))
|
||||
return labels, x.
|
||||
Where("org_id = ?", orgID).
|
||||
In("id", labelIDs).
|
||||
Asc("name").
|
||||
Find(&labels)
|
||||
}
|
||||
|
||||
func getLabelsByOrgID(e Engine, orgID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
|
||||
if orgID <= 0 {
|
||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||
}
|
||||
labels := make([]*Label, 0, 10)
|
||||
sess := e.Where("org_id = ?", orgID)
|
||||
|
||||
switch sortType {
|
||||
case "reversealphabetically":
|
||||
sess.Desc("name")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("name")
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = listOptions.setSessionPagination(sess)
|
||||
}
|
||||
|
||||
return labels, sess.Find(&labels)
|
||||
}
|
||||
|
||||
// GetLabelsByOrgID returns all labels that belong to given organization by ID.
|
||||
func GetLabelsByOrgID(orgID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
|
||||
return getLabelsByOrgID(x, orgID, sortType, listOptions)
|
||||
}
|
||||
|
||||
// .___
|
||||
// | | ______ ________ __ ____
|
||||
// | |/ ___// ___/ | \_/ __ \
|
||||
// | |\___ \ \___ \| | /\ ___/
|
||||
// |___/____ >____ >____/ \___ |
|
||||
// \/ \/ \/
|
||||
|
||||
func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
|
||||
var labels []*Label
|
||||
return labels, e.Where("issue_label.issue_id = ?", issueID).
|
||||
@@ -311,7 +585,7 @@ func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
|
||||
return getLabelsByIssueID(x, issueID)
|
||||
}
|
||||
|
||||
func updateLabel(e Engine, l *Label) error {
|
||||
func updateLabelCols(e Engine, l *Label, cols ...string) error {
|
||||
_, err := e.ID(l.ID).
|
||||
SetExpr("num_issues",
|
||||
builder.Select("count(*)").From("issue_label").
|
||||
@@ -325,47 +599,10 @@ func updateLabel(e Engine, l *Label) error {
|
||||
"issue.is_closed": true,
|
||||
}),
|
||||
).
|
||||
AllCols().Update(l)
|
||||
Cols(cols...).Update(l)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateLabel updates label information.
|
||||
func UpdateLabel(l *Label) error {
|
||||
return updateLabel(x, l)
|
||||
}
|
||||
|
||||
// DeleteLabel delete a label of given repository.
|
||||
func DeleteLabel(repoID, labelID int64) error {
|
||||
_, err := GetLabelInRepoByID(repoID, labelID)
|
||||
if err != nil {
|
||||
if IsErrLabelNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
|
||||
return err
|
||||
} else if _, err = sess.
|
||||
Where("label_id = ?", labelID).
|
||||
Delete(new(IssueLabel)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear label id in comment table
|
||||
if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Update(&Comment{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// .___ .____ ___. .__
|
||||
// | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
|
||||
// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
|
||||
@@ -402,11 +639,19 @@ func newIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = createLabelComment(e, doer, issue.Repo, issue, label, true); err != nil {
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: CommentTypeLabel,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
Label: label,
|
||||
Content: "1",
|
||||
}
|
||||
if _, err = createComment(e, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateLabel(e, label)
|
||||
return updateLabelCols(e, label, "num_issues", "num_closed_issue")
|
||||
}
|
||||
|
||||
// NewIssueLabel creates a new issue-label relation.
|
||||
@@ -471,11 +716,18 @@ func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = createLabelComment(e, doer, issue.Repo, issue, label, false); err != nil {
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: CommentTypeLabel,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
Label: label,
|
||||
}
|
||||
if _, err = createComment(e, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateLabel(e, label)
|
||||
return updateLabelCols(e, label, "num_issues", "num_closed_issue")
|
||||
}
|
||||
|
||||
// DeleteIssueLabel deletes issue-label relation.
|
||||
|
||||
+121
-22
@@ -8,23 +8,11 @@ import (
|
||||
"html/template"
|
||||
"testing"
|
||||
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TODO TestGetLabelTemplateFile
|
||||
|
||||
func TestLabel_APIFormat(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
assert.Equal(t, api.Label{
|
||||
ID: label.ID,
|
||||
Name: label.Name,
|
||||
Color: "abcdef",
|
||||
}, *label.APIFormat())
|
||||
}
|
||||
|
||||
func TestLabel_CalOpenIssues(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
@@ -45,8 +33,11 @@ func TestNewLabels(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
labels := []*Label{
|
||||
{RepoID: 2, Name: "labelName2", Color: "#123456"},
|
||||
{RepoID: 3, Name: "labelName3", Color: "#234567"},
|
||||
{RepoID: 3, Name: "labelName3", Color: "#23456F"},
|
||||
}
|
||||
assert.Error(t, NewLabel(&Label{RepoID: 3, Name: "invalid Color", Color: ""}))
|
||||
assert.Error(t, NewLabel(&Label{RepoID: 3, Name: "invalid Color", Color: "123456"}))
|
||||
assert.Error(t, NewLabel(&Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"}))
|
||||
for _, label := range labels {
|
||||
AssertNotExistsBean(t, label)
|
||||
}
|
||||
@@ -75,10 +66,10 @@ func TestGetLabelInRepoByName(t *testing.T) {
|
||||
assert.Equal(t, "label1", label.Name)
|
||||
|
||||
_, err = GetLabelInRepoByName(1, "")
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
assert.True(t, IsErrRepoLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInRepoByName(NonexistentID, "nonexistent")
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
assert.True(t, IsErrRepoLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelInRepoByNames(t *testing.T) {
|
||||
@@ -112,10 +103,10 @@ func TestGetLabelInRepoByID(t *testing.T) {
|
||||
assert.EqualValues(t, 1, label.ID)
|
||||
|
||||
_, err = GetLabelInRepoByID(1, -1)
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
assert.True(t, IsErrRepoLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInRepoByID(NonexistentID, NonexistentID)
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
assert.True(t, IsErrRepoLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelsInRepoByIDs(t *testing.T) {
|
||||
@@ -131,7 +122,7 @@ func TestGetLabelsInRepoByIDs(t *testing.T) {
|
||||
func TestGetLabelsByRepoID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := GetLabelsByRepoID(repoID, sortType)
|
||||
labels, err := GetLabelsByRepoID(repoID, sortType, ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, len(expectedIssueIDs))
|
||||
for i, label := range labels {
|
||||
@@ -144,6 +135,107 @@ func TestGetLabelsByRepoID(t *testing.T) {
|
||||
testSuccess(1, "default", []int64{1, 2})
|
||||
}
|
||||
|
||||
// Org vrsions
|
||||
|
||||
func TestGetLabelInOrgByName(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label, err := GetLabelInOrgByName(3, "orglabel3")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, label.ID)
|
||||
assert.Equal(t, "orglabel3", label.Name)
|
||||
|
||||
_, err = GetLabelInOrgByName(3, "")
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInOrgByName(0, "orglabel3")
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInOrgByName(-1, "orglabel3")
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInOrgByName(NonexistentID, "nonexistent")
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelInOrgByNames(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, labelIDs, 2)
|
||||
|
||||
assert.Equal(t, int64(3), labelIDs[0])
|
||||
assert.Equal(t, int64(4), labelIDs[1])
|
||||
}
|
||||
|
||||
func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
// orglabel99 doesn't exists.. See labels.yml
|
||||
labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4", "orglabel99"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, labelIDs, 2)
|
||||
|
||||
assert.Equal(t, int64(3), labelIDs[0])
|
||||
assert.Equal(t, int64(4), labelIDs[1])
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetLabelInOrgByID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label, err := GetLabelInOrgByID(3, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, label.ID)
|
||||
|
||||
_, err = GetLabelInOrgByID(3, -1)
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInOrgByID(0, 3)
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInOrgByID(-1, 3)
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInOrgByID(NonexistentID, NonexistentID)
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelsInOrgByIDs(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
labels, err := GetLabelsInOrgByIDs(3, []int64{3, 4, NonexistentID})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, labels, 2) {
|
||||
assert.EqualValues(t, 3, labels[0].ID)
|
||||
assert.EqualValues(t, 4, labels[1].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLabelsByOrgID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := GetLabelsByOrgID(orgID, sortType, ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, len(expectedIssueIDs))
|
||||
for i, label := range labels {
|
||||
assert.EqualValues(t, expectedIssueIDs[i], label.ID)
|
||||
}
|
||||
}
|
||||
testSuccess(3, "leastissues", []int64{3, 4})
|
||||
testSuccess(3, "mostissues", []int64{4, 3})
|
||||
testSuccess(3, "reversealphabetically", []int64{4, 3})
|
||||
testSuccess(3, "default", []int64{3, 4})
|
||||
|
||||
var err error
|
||||
_, err = GetLabelsByOrgID(0, "leastissues", ListOptions{})
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelsByOrgID(-1, "leastissues", ListOptions{})
|
||||
assert.True(t, IsErrOrgLabelNotExist(err))
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func TestGetLabelsByIssueID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
labels, err := GetLabelsByIssueID(1)
|
||||
@@ -160,9 +252,16 @@ func TestGetLabelsByIssueID(t *testing.T) {
|
||||
func TestUpdateLabel(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
label.Color = "#ffff00"
|
||||
label.Name = "newLabelName"
|
||||
assert.NoError(t, UpdateLabel(label))
|
||||
// make sure update wont overwrite it
|
||||
update := &Label{
|
||||
ID: label.ID,
|
||||
Color: "#ffff00",
|
||||
Name: "newLabelName",
|
||||
Description: label.Description,
|
||||
}
|
||||
label.Color = update.Color
|
||||
label.Name = update.Name
|
||||
assert.NoError(t, UpdateLabel(update))
|
||||
newLabel := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
assert.Equal(t, *label, *newLabel)
|
||||
CheckConsistencyFor(t, &Label{}, &Repository{})
|
||||
@@ -175,7 +274,7 @@ func TestDeleteLabel(t *testing.T) {
|
||||
AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID})
|
||||
|
||||
assert.NoError(t, DeleteLabel(label.RepoID, label.ID))
|
||||
AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID})
|
||||
AssertNotExistsBean(t, &Label{ID: label.ID})
|
||||
|
||||
assert.NoError(t, DeleteLabel(NonexistentID, NonexistentID))
|
||||
CheckConsistencyFor(t, &Label{}, &Repository{})
|
||||
|
||||
@@ -429,6 +429,7 @@ func (issues IssueList) loadTotalTrackedTimes(e Engine) (err error) {
|
||||
|
||||
// select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
|
||||
rows, err := e.Table("tracked_time").
|
||||
Where("deleted = ?", false).
|
||||
Select("issue_id, sum(time) as time").
|
||||
In("issue_id", ids[:limit]).
|
||||
GroupBy("issue_id").
|
||||
@@ -514,3 +515,35 @@ func (issues IssueList) LoadComments() error {
|
||||
func (issues IssueList) LoadDiscussComments() error {
|
||||
return issues.loadComments(x, builder.Eq{"comment.type": CommentTypeComment})
|
||||
}
|
||||
|
||||
// GetApprovalCounts returns a map of issue ID to slice of approval counts
|
||||
// FIXME: only returns official counts due to double counting of non-official approvals
|
||||
func (issues IssueList) GetApprovalCounts() (map[int64][]*ReviewCount, error) {
|
||||
return issues.getApprovalCounts(x)
|
||||
}
|
||||
|
||||
func (issues IssueList) getApprovalCounts(e Engine) (map[int64][]*ReviewCount, error) {
|
||||
rCounts := make([]*ReviewCount, 0, 2*len(issues))
|
||||
ids := make([]int64, len(issues))
|
||||
for i, issue := range issues {
|
||||
ids[i] = issue.ID
|
||||
}
|
||||
sess := e.In("issue_id", ids)
|
||||
err := sess.Select("issue_id, type, count(id) as `count`").
|
||||
Where("official = ?", true).
|
||||
GroupBy("issue_id, type").
|
||||
OrderBy("issue_id").
|
||||
Table("review").
|
||||
Find(&rCounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
approvalCountMap := make(map[int64][]*ReviewCount, len(issues))
|
||||
|
||||
for _, c := range rCounts {
|
||||
approvalCountMap[c.IssueID] = append(approvalCountMap[c.IssueID], c)
|
||||
}
|
||||
|
||||
return approvalCountMap, nil
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ func TestIssueList_LoadAttributes(t *testing.T) {
|
||||
setting.Service.EnableTimetracking = true
|
||||
issueList := IssueList{
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue),
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ func TestIssueList_LoadAttributes(t *testing.T) {
|
||||
if issue.ID == int64(1) {
|
||||
assert.Equal(t, int64(400), issue.TotalTrackedTime)
|
||||
} else if issue.ID == int64(2) {
|
||||
assert.Equal(t, int64(3662), issue.TotalTrackedTime)
|
||||
assert.Equal(t, int64(3682), issue.TotalTrackedTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,14 +45,14 @@ func updateIssueLock(opts *IssueLockOptions, lock bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := createComment(sess, &CreateCommentOptions{
|
||||
var opt = &CreateCommentOptions{
|
||||
Doer: opts.Doer,
|
||||
Issue: opts.Issue,
|
||||
Repo: opts.Issue.Repo,
|
||||
Type: commentType,
|
||||
Content: opts.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
}
|
||||
if _, err := createComment(sess, opt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+352
-193
@@ -6,19 +6,21 @@ package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"xorm.io/builder"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// Milestone represents a milestone of repository.
|
||||
type Milestone struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
Repo *Repository `xorm:"-"`
|
||||
Name string
|
||||
Content string `xorm:"TEXT"`
|
||||
RenderedContent string `xorm:"-"`
|
||||
@@ -67,25 +69,6 @@ func (m *Milestone) State() api.StateType {
|
||||
return api.StateOpen
|
||||
}
|
||||
|
||||
// APIFormat returns this Milestone in API format.
|
||||
func (m *Milestone) APIFormat() *api.Milestone {
|
||||
apiMilestone := &api.Milestone{
|
||||
ID: m.ID,
|
||||
State: m.State(),
|
||||
Title: m.Name,
|
||||
Description: m.Content,
|
||||
OpenIssues: m.NumOpenIssues,
|
||||
ClosedIssues: m.NumClosedIssues,
|
||||
}
|
||||
if m.IsClosed {
|
||||
apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr()
|
||||
}
|
||||
if m.DeadlineUnix.Year() < 9999 {
|
||||
apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr()
|
||||
}
|
||||
return apiMilestone
|
||||
}
|
||||
|
||||
// NewMilestone creates new milestone of repository.
|
||||
func NewMilestone(m *Milestone) (err error) {
|
||||
sess := x.NewSession()
|
||||
@@ -94,6 +77,8 @@ func NewMilestone(m *Milestone) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Name = strings.TrimSpace(m.Name)
|
||||
|
||||
if _, err = sess.Insert(m); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -105,15 +90,12 @@ func NewMilestone(m *Milestone) (err error) {
|
||||
}
|
||||
|
||||
func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
|
||||
m := &Milestone{
|
||||
ID: id,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := e.Get(m)
|
||||
m := new(Milestone)
|
||||
has, err := e.ID(id).Where("repo_id=?", repoID).Get(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrMilestoneNotExist{id, repoID}
|
||||
return nil, ErrMilestoneNotExist{ID: id, RepoID: repoID}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -123,6 +105,19 @@ func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
|
||||
return getMilestoneByRepoID(x, repoID, id)
|
||||
}
|
||||
|
||||
// GetMilestoneByRepoIDANDName return a milestone if one exist by name and repo
|
||||
func GetMilestoneByRepoIDANDName(repoID int64, name string) (*Milestone, error) {
|
||||
var mile Milestone
|
||||
has, err := x.Where("repo_id=? AND name=?", repoID, name).Get(&mile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, ErrMilestoneNotExist{Name: name, RepoID: repoID}
|
||||
}
|
||||
return &mile, nil
|
||||
}
|
||||
|
||||
// GetMilestoneByID returns the milestone via id .
|
||||
func GetMilestoneByID(id int64) (*Milestone, error) {
|
||||
var m Milestone
|
||||
@@ -130,114 +125,43 @@ func GetMilestoneByID(id int64) (*Milestone, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrMilestoneNotExist{id, 0}
|
||||
return nil, ErrMilestoneNotExist{ID: id, RepoID: 0}
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// MilestoneList is a list of milestones offering additional functionality
|
||||
type MilestoneList []*Milestone
|
||||
|
||||
func (milestones MilestoneList) loadTotalTrackedTimes(e Engine) error {
|
||||
type totalTimesByMilestone struct {
|
||||
MilestoneID int64
|
||||
Time int64
|
||||
}
|
||||
if len(milestones) == 0 {
|
||||
return nil
|
||||
}
|
||||
var trackedTimes = make(map[int64]int64, len(milestones))
|
||||
|
||||
// Get total tracked time by milestone_id
|
||||
rows, err := e.Table("issue").
|
||||
Join("INNER", "milestone", "issue.milestone_id = milestone.id").
|
||||
Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
|
||||
Select("milestone_id, sum(time) as time").
|
||||
In("milestone_id", milestones.getMilestoneIDs()).
|
||||
GroupBy("milestone_id").
|
||||
Rows(new(totalTimesByMilestone))
|
||||
if err != nil {
|
||||
// UpdateMilestone updates information of given milestone.
|
||||
func UpdateMilestone(m *Milestone, oldIsClosed bool) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
if m.IsClosed && !oldIsClosed {
|
||||
m.ClosedDateUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var totalTime totalTimesByMilestone
|
||||
err = rows.Scan(&totalTime)
|
||||
if err != nil {
|
||||
if err := updateMilestone(sess, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateMilestoneCompleteness(sess, m.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if IsClosed changed, update milestone numbers of repository
|
||||
if oldIsClosed != m.IsClosed {
|
||||
if err := updateRepoMilestoneNum(sess, m.RepoID); err != nil {
|
||||
return err
|
||||
}
|
||||
trackedTimes[totalTime.MilestoneID] = totalTime.Time
|
||||
}
|
||||
|
||||
for _, milestone := range milestones {
|
||||
milestone.TotalTrackedTime = trackedTimes[milestone.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
|
||||
func (milestones MilestoneList) LoadTotalTrackedTimes() error {
|
||||
return milestones.loadTotalTrackedTimes(x)
|
||||
}
|
||||
|
||||
func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
||||
var ids = make([]int64, 0, len(milestones))
|
||||
for _, ms := range milestones {
|
||||
ids = append(ids, ms.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// GetMilestonesByRepoID returns all opened milestones of a repository.
|
||||
func GetMilestonesByRepoID(repoID int64, state api.StateType) (MilestoneList, error) {
|
||||
sess := x.Where("repo_id = ?", repoID)
|
||||
|
||||
switch state {
|
||||
case api.StateClosed:
|
||||
sess = sess.And("is_closed = ?", true)
|
||||
|
||||
case api.StateAll:
|
||||
break
|
||||
|
||||
case api.StateOpen:
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
sess = sess.And("is_closed = ?", false)
|
||||
}
|
||||
|
||||
miles := make([]*Milestone, 0, 10)
|
||||
return miles, sess.Asc("deadline_unix").Asc("id").Find(&miles)
|
||||
}
|
||||
|
||||
// GetMilestones returns a list of milestones of given repository and status.
|
||||
func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
|
||||
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
|
||||
sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
|
||||
if page > 0 {
|
||||
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
|
||||
}
|
||||
|
||||
switch sortType {
|
||||
case "furthestduedate":
|
||||
sess.Desc("deadline_unix")
|
||||
case "leastcomplete":
|
||||
sess.Asc("completeness")
|
||||
case "mostcomplete":
|
||||
sess.Desc("completeness")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("deadline_unix")
|
||||
}
|
||||
return miles, sess.Find(&miles)
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func updateMilestone(e Engine, m *Milestone) error {
|
||||
m.Name = strings.TrimSpace(m.Name)
|
||||
_, err := e.ID(m.ID).AllCols().
|
||||
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
|
||||
builder.Eq{"milestone_id": m.ID},
|
||||
@@ -252,15 +176,6 @@ func updateMilestone(e Engine, m *Milestone) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateMilestone updates information of given milestone.
|
||||
func UpdateMilestone(m *Milestone) error {
|
||||
if err := updateMilestone(x, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateMilestoneCompleteness(x, m.ID)
|
||||
}
|
||||
|
||||
func updateMilestoneCompleteness(e Engine, milestoneID int64) error {
|
||||
_, err := e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
|
||||
milestoneID,
|
||||
@@ -268,35 +183,6 @@ func updateMilestoneCompleteness(e Engine, milestoneID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func countRepoMilestones(e Engine, repoID int64) (int64, error) {
|
||||
return e.
|
||||
Where("repo_id=?", repoID).
|
||||
Count(new(Milestone))
|
||||
}
|
||||
|
||||
func countRepoClosedMilestones(e Engine, repoID int64) (int64, error) {
|
||||
return e.
|
||||
Where("repo_id=? AND is_closed=?", repoID, true).
|
||||
Count(new(Milestone))
|
||||
}
|
||||
|
||||
// CountRepoClosedMilestones returns number of closed milestones in given repository.
|
||||
func CountRepoClosedMilestones(repoID int64) (int64, error) {
|
||||
return countRepoClosedMilestones(x, repoID)
|
||||
}
|
||||
|
||||
// MilestoneStats returns number of open and closed milestones of given repository.
|
||||
func MilestoneStats(repoID int64) (open int64, closed int64, err error) {
|
||||
open, err = x.
|
||||
Where("repo_id=? AND is_closed=?", repoID, false).
|
||||
Count(new(Milestone))
|
||||
if err != nil {
|
||||
return 0, 0, nil
|
||||
}
|
||||
closed, err = CountRepoClosedMilestones(repoID)
|
||||
return open, closed, err
|
||||
}
|
||||
|
||||
// ChangeMilestoneStatus changes the milestone open/closed status.
|
||||
func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
|
||||
sess := x.NewSession()
|
||||
@@ -321,39 +207,6 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func updateRepoMilestoneNum(e Engine, repoID int64) error {
|
||||
_, err := e.Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?",
|
||||
repoID,
|
||||
repoID,
|
||||
true,
|
||||
repoID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateMilestoneTotalNum(e Engine, milestoneID int64) (err error) {
|
||||
if _, err = e.Exec("UPDATE `milestone` SET num_issues=(SELECT count(*) FROM issue WHERE milestone_id=?) WHERE id=?",
|
||||
milestoneID,
|
||||
milestoneID,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return updateMilestoneCompleteness(e, milestoneID)
|
||||
}
|
||||
|
||||
func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) {
|
||||
if _, err = e.Exec("UPDATE `milestone` SET num_closed_issues=(SELECT count(*) FROM issue WHERE milestone_id=? AND is_closed=?) WHERE id=?",
|
||||
milestoneID,
|
||||
true,
|
||||
milestoneID,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return updateMilestoneCompleteness(e, milestoneID)
|
||||
}
|
||||
|
||||
func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error {
|
||||
if err := updateIssueCols(e, issue, "milestone_id"); err != nil {
|
||||
return err
|
||||
@@ -386,7 +239,15 @@ func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilesto
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := createMilestoneComment(e, doer, issue.Repo, issue, oldMilestoneID, issue.MilestoneID); err != nil {
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: CommentTypeMilestone,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
OldMilestoneID: oldMilestoneID,
|
||||
MilestoneID: issue.MilestoneID,
|
||||
}
|
||||
if _, err := createComment(e, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -457,3 +318,301 @@ func DeleteMilestoneByRepoID(repoID, id int64) error {
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// MilestoneList is a list of milestones offering additional functionality
|
||||
type MilestoneList []*Milestone
|
||||
|
||||
func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
||||
var ids = make([]int64, 0, len(milestones))
|
||||
for _, ms := range milestones {
|
||||
ids = append(ids, ms.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// GetMilestonesByRepoID returns all opened milestones of a repository.
|
||||
func GetMilestonesByRepoID(repoID int64, state api.StateType, listOptions ListOptions) (MilestoneList, error) {
|
||||
sess := x.Where("repo_id = ?", repoID)
|
||||
|
||||
switch state {
|
||||
case api.StateClosed:
|
||||
sess = sess.And("is_closed = ?", true)
|
||||
|
||||
case api.StateAll:
|
||||
break
|
||||
|
||||
case api.StateOpen:
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
sess = sess.And("is_closed = ?", false)
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = listOptions.setSessionPagination(sess)
|
||||
}
|
||||
|
||||
miles := make([]*Milestone, 0, listOptions.PageSize)
|
||||
return miles, sess.Asc("deadline_unix").Asc("id").Find(&miles)
|
||||
}
|
||||
|
||||
// GetMilestones returns a list of milestones of given repository and status.
|
||||
func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
|
||||
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
|
||||
sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
|
||||
if page > 0 {
|
||||
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
|
||||
}
|
||||
|
||||
switch sortType {
|
||||
case "furthestduedate":
|
||||
sess.Desc("deadline_unix")
|
||||
case "leastcomplete":
|
||||
sess.Asc("completeness")
|
||||
case "mostcomplete":
|
||||
sess.Desc("completeness")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("deadline_unix")
|
||||
}
|
||||
return miles, sess.Find(&miles)
|
||||
}
|
||||
|
||||
// SearchMilestones search milestones
|
||||
func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType string) (MilestoneList, error) {
|
||||
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
|
||||
sess := x.Where("is_closed = ?", isClosed)
|
||||
if repoCond.IsValid() {
|
||||
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
|
||||
}
|
||||
if page > 0 {
|
||||
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
|
||||
}
|
||||
|
||||
switch sortType {
|
||||
case "furthestduedate":
|
||||
sess.Desc("deadline_unix")
|
||||
case "leastcomplete":
|
||||
sess.Asc("completeness")
|
||||
case "mostcomplete":
|
||||
sess.Desc("completeness")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("deadline_unix")
|
||||
}
|
||||
return miles, sess.Find(&miles)
|
||||
}
|
||||
|
||||
// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
|
||||
func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
|
||||
return SearchMilestones(
|
||||
builder.In("repo_id", repoIDs),
|
||||
page,
|
||||
isClosed,
|
||||
sortType,
|
||||
)
|
||||
}
|
||||
|
||||
// ____ _ _
|
||||
// / ___|| |_ __ _| |_ ___
|
||||
// \___ \| __/ _` | __/ __|
|
||||
// ___) | || (_| | |_\__ \
|
||||
// |____/ \__\__,_|\__|___/
|
||||
//
|
||||
|
||||
// MilestonesStats represents milestone statistic information.
|
||||
type MilestonesStats struct {
|
||||
OpenCount, ClosedCount int64
|
||||
}
|
||||
|
||||
// Total returns the total counts of milestones
|
||||
func (m MilestonesStats) Total() int64 {
|
||||
return m.OpenCount + m.ClosedCount
|
||||
}
|
||||
|
||||
// GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions.
|
||||
func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) {
|
||||
var err error
|
||||
stats := &MilestonesStats{}
|
||||
|
||||
sess := x.Where("is_closed = ?", false)
|
||||
if repoCond.IsValid() {
|
||||
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
|
||||
}
|
||||
stats.OpenCount, err = sess.Count(new(Milestone))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sess = x.Where("is_closed = ?", true)
|
||||
if repoCond.IsValid() {
|
||||
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
|
||||
}
|
||||
stats.ClosedCount, err = sess.Count(new(Milestone))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func countRepoMilestones(e Engine, repoID int64) (int64, error) {
|
||||
return e.
|
||||
Where("repo_id=?", repoID).
|
||||
Count(new(Milestone))
|
||||
}
|
||||
|
||||
func countRepoClosedMilestones(e Engine, repoID int64) (int64, error) {
|
||||
return e.
|
||||
Where("repo_id=? AND is_closed=?", repoID, true).
|
||||
Count(new(Milestone))
|
||||
}
|
||||
|
||||
// CountRepoClosedMilestones returns number of closed milestones in given repository.
|
||||
func CountRepoClosedMilestones(repoID int64) (int64, error) {
|
||||
return countRepoClosedMilestones(x, repoID)
|
||||
}
|
||||
|
||||
// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
|
||||
func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
|
||||
sess := x.Where("is_closed = ?", isClosed)
|
||||
if repoCond.IsValid() {
|
||||
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
|
||||
}
|
||||
|
||||
countsSlice := make([]*struct {
|
||||
RepoID int64
|
||||
Count int64
|
||||
}, 0, 10)
|
||||
if err := sess.GroupBy("repo_id").
|
||||
Select("repo_id AS repo_id, COUNT(*) AS count").
|
||||
Table("milestone").
|
||||
Find(&countsSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
countMap := make(map[int64]int64, len(countsSlice))
|
||||
for _, c := range countsSlice {
|
||||
countMap[c.RepoID] = c.Count
|
||||
}
|
||||
return countMap, nil
|
||||
}
|
||||
|
||||
func updateRepoMilestoneNum(e Engine, repoID int64) error {
|
||||
_, err := e.Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?",
|
||||
repoID,
|
||||
repoID,
|
||||
true,
|
||||
repoID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateMilestoneTotalNum(e Engine, milestoneID int64) (err error) {
|
||||
if _, err = e.Exec("UPDATE `milestone` SET num_issues=(SELECT count(*) FROM issue WHERE milestone_id=?) WHERE id=?",
|
||||
milestoneID,
|
||||
milestoneID,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return updateMilestoneCompleteness(e, milestoneID)
|
||||
}
|
||||
|
||||
func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) {
|
||||
if _, err = e.Exec("UPDATE `milestone` SET num_closed_issues=(SELECT count(*) FROM issue WHERE milestone_id=? AND is_closed=?) WHERE id=?",
|
||||
milestoneID,
|
||||
true,
|
||||
milestoneID,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return updateMilestoneCompleteness(e, milestoneID)
|
||||
}
|
||||
|
||||
// _____ _ _ _____ _
|
||||
// |_ _| __ __ _ ___| | _____ __| |_ _(_)_ __ ___ ___ ___
|
||||
// | || '__/ _` |/ __| |/ / _ \/ _` | | | | | '_ ` _ \ / _ \/ __|
|
||||
// | || | | (_| | (__| < __/ (_| | | | | | | | | | | __/\__ \
|
||||
// |_||_| \__,_|\___|_|\_\___|\__,_| |_| |_|_| |_| |_|\___||___/
|
||||
//
|
||||
|
||||
func (milestones MilestoneList) loadTotalTrackedTimes(e Engine) error {
|
||||
type totalTimesByMilestone struct {
|
||||
MilestoneID int64
|
||||
Time int64
|
||||
}
|
||||
if len(milestones) == 0 {
|
||||
return nil
|
||||
}
|
||||
var trackedTimes = make(map[int64]int64, len(milestones))
|
||||
|
||||
// Get total tracked time by milestone_id
|
||||
rows, err := e.Table("issue").
|
||||
Join("INNER", "milestone", "issue.milestone_id = milestone.id").
|
||||
Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
|
||||
Where("tracked_time.deleted = ?", false).
|
||||
Select("milestone_id, sum(time) as time").
|
||||
In("milestone_id", milestones.getMilestoneIDs()).
|
||||
GroupBy("milestone_id").
|
||||
Rows(new(totalTimesByMilestone))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var totalTime totalTimesByMilestone
|
||||
err = rows.Scan(&totalTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trackedTimes[totalTime.MilestoneID] = totalTime.Time
|
||||
}
|
||||
|
||||
for _, milestone := range milestones {
|
||||
milestone.TotalTrackedTime = trackedTimes[milestone.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Milestone) loadTotalTrackedTime(e Engine) error {
|
||||
type totalTimesByMilestone struct {
|
||||
MilestoneID int64
|
||||
Time int64
|
||||
}
|
||||
totalTime := &totalTimesByMilestone{MilestoneID: m.ID}
|
||||
has, err := e.Table("issue").
|
||||
Join("INNER", "milestone", "issue.milestone_id = milestone.id").
|
||||
Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
|
||||
Where("tracked_time.deleted = ?", false).
|
||||
Select("milestone_id, sum(time) as time").
|
||||
Where("milestone_id = ?", m.ID).
|
||||
GroupBy("milestone_id").
|
||||
Get(totalTime)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return nil
|
||||
}
|
||||
m.TotalTrackedTime = totalTime.Time
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
|
||||
func (milestones MilestoneList) LoadTotalTrackedTimes() error {
|
||||
return milestones.loadTotalTrackedTimes(x)
|
||||
}
|
||||
|
||||
// LoadTotalTrackedTime loads the tracked time for the milestone
|
||||
func (m *Milestone) LoadTotalTrackedTime() error {
|
||||
return m.loadTotalTrackedTime(x)
|
||||
}
|
||||
|
||||
+112
-50
@@ -7,12 +7,12 @@ package models
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func TestMilestone_State(t *testing.T) {
|
||||
@@ -20,28 +20,6 @@ func TestMilestone_State(t *testing.T) {
|
||||
assert.Equal(t, api.StateClosed, (&Milestone{IsClosed: true}).State())
|
||||
}
|
||||
|
||||
func TestMilestone_APIFormat(t *testing.T) {
|
||||
milestone := &Milestone{
|
||||
ID: 3,
|
||||
RepoID: 4,
|
||||
Name: "milestoneName",
|
||||
Content: "milestoneContent",
|
||||
IsClosed: false,
|
||||
NumOpenIssues: 5,
|
||||
NumClosedIssues: 6,
|
||||
DeadlineUnix: timeutil.TimeStamp(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()),
|
||||
}
|
||||
assert.Equal(t, api.Milestone{
|
||||
ID: milestone.ID,
|
||||
State: api.StateOpen,
|
||||
Title: milestone.Name,
|
||||
Description: milestone.Content,
|
||||
OpenIssues: milestone.NumOpenIssues,
|
||||
ClosedIssues: milestone.NumClosedIssues,
|
||||
Deadline: milestone.DeadlineUnix.AsTimePtr(),
|
||||
}, *milestone.APIFormat())
|
||||
}
|
||||
|
||||
func TestNewMilestone(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
milestone := &Milestone{
|
||||
@@ -71,7 +49,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
test := func(repoID int64, state api.StateType) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||
milestones, err := GetMilestonesByRepoID(repo.ID, state)
|
||||
milestones, err := GetMilestonesByRepoID(repo.ID, state, ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
var n int
|
||||
@@ -105,7 +83,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
|
||||
test(3, api.StateClosed)
|
||||
test(3, api.StateAll)
|
||||
|
||||
milestones, err := GetMilestonesByRepoID(NonexistentID, api.StateOpen)
|
||||
milestones, err := GetMilestonesByRepoID(NonexistentID, api.StateOpen, ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, milestones, 0)
|
||||
}
|
||||
@@ -158,10 +136,11 @@ func TestUpdateMilestone(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
|
||||
milestone.Name = "newMilestoneName"
|
||||
milestone.Name = " newMilestoneName "
|
||||
milestone.Content = "newMilestoneContent"
|
||||
assert.NoError(t, UpdateMilestone(milestone))
|
||||
AssertExistsAndLoadBean(t, milestone)
|
||||
assert.NoError(t, UpdateMilestone(milestone, milestone.IsClosed))
|
||||
milestone = AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
|
||||
assert.EqualValues(t, "newMilestoneName", milestone.Name)
|
||||
CheckConsistencyFor(t, &Milestone{})
|
||||
}
|
||||
|
||||
@@ -199,25 +178,6 @@ func TestCountRepoClosedMilestones(t *testing.T) {
|
||||
assert.EqualValues(t, 0, count)
|
||||
}
|
||||
|
||||
func TestMilestoneStats(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||
open, closed, err := MilestoneStats(repoID)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, open)
|
||||
assert.EqualValues(t, repo.NumClosedMilestones, closed)
|
||||
}
|
||||
test(1)
|
||||
test(2)
|
||||
test(3)
|
||||
|
||||
open, closed, err := MilestoneStats(NonexistentID)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, open)
|
||||
assert.EqualValues(t, 0, closed)
|
||||
}
|
||||
|
||||
func TestChangeMilestoneStatus(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
|
||||
@@ -238,14 +198,14 @@ func TestUpdateMilestoneClosedNum(t *testing.T) {
|
||||
|
||||
issue.IsClosed = true
|
||||
issue.ClosedUnix = timeutil.TimeStampNow()
|
||||
_, err := x.Cols("is_closed", "closed_unix").Update(issue)
|
||||
_, err := x.ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID))
|
||||
CheckConsistencyFor(t, &Milestone{})
|
||||
|
||||
issue.IsClosed = false
|
||||
issue.ClosedUnix = 0
|
||||
_, err = x.Cols("is_closed", "closed_unix").Update(issue)
|
||||
_, err = x.ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID))
|
||||
CheckConsistencyFor(t, &Milestone{})
|
||||
@@ -287,5 +247,107 @@ func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) {
|
||||
|
||||
assert.NoError(t, miles.LoadTotalTrackedTimes())
|
||||
|
||||
assert.Equal(t, miles[0].TotalTrackedTime, int64(3662))
|
||||
assert.Equal(t, int64(3682), miles[0].TotalTrackedTime)
|
||||
}
|
||||
|
||||
func TestCountMilestonesByRepoIDs(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
milestonesCount := func(repoID int64) (int, int) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||
return repo.NumOpenMilestones, repo.NumClosedMilestones
|
||||
}
|
||||
repo1OpenCount, repo1ClosedCount := milestonesCount(1)
|
||||
repo2OpenCount, repo2ClosedCount := milestonesCount(2)
|
||||
|
||||
openCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), false)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo1OpenCount, openCounts[1])
|
||||
assert.EqualValues(t, repo2OpenCount, openCounts[2])
|
||||
|
||||
closedCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), true)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
|
||||
assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
|
||||
}
|
||||
|
||||
func TestGetMilestonesByRepoIDs(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
||||
test := func(sortType string, sortCond func(*Milestone) int) {
|
||||
for _, page := range []int{0, 1} {
|
||||
openMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
|
||||
values := make([]int, len(openMilestones))
|
||||
for i, milestone := range openMilestones {
|
||||
values[i] = sortCond(milestone)
|
||||
}
|
||||
assert.True(t, sort.IntsAreSorted(values))
|
||||
|
||||
closedMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
|
||||
values = make([]int, len(closedMilestones))
|
||||
for i, milestone := range closedMilestones {
|
||||
values[i] = sortCond(milestone)
|
||||
}
|
||||
assert.True(t, sort.IntsAreSorted(values))
|
||||
}
|
||||
}
|
||||
test("furthestduedate", func(milestone *Milestone) int {
|
||||
return -int(milestone.DeadlineUnix)
|
||||
})
|
||||
test("leastcomplete", func(milestone *Milestone) int {
|
||||
return milestone.Completeness
|
||||
})
|
||||
test("mostcomplete", func(milestone *Milestone) int {
|
||||
return -milestone.Completeness
|
||||
})
|
||||
test("leastissues", func(milestone *Milestone) int {
|
||||
return milestone.NumIssues
|
||||
})
|
||||
test("mostissues", func(milestone *Milestone) int {
|
||||
return -milestone.NumIssues
|
||||
})
|
||||
test("soonestduedate", func(milestone *Milestone) int {
|
||||
return int(milestone.DeadlineUnix)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadTotalTrackedTime(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
|
||||
|
||||
assert.NoError(t, milestone.LoadTotalTrackedTime())
|
||||
|
||||
assert.Equal(t, int64(3682), milestone.TotalTrackedTime)
|
||||
}
|
||||
|
||||
func TestGetMilestonesStats(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
test := func(repoID int64) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||
stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": repoID}))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, stats.OpenCount)
|
||||
assert.EqualValues(t, repo.NumClosedMilestones, stats.ClosedCount)
|
||||
}
|
||||
test(1)
|
||||
test(2)
|
||||
test(3)
|
||||
|
||||
stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": NonexistentID}))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, stats.OpenCount)
|
||||
assert.EqualValues(t, 0, stats.ClosedCount)
|
||||
|
||||
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
||||
|
||||
milestoneStats, err := GetMilestonesStatsByRepoCond(builder.In("repo_id", []int64{repo1.ID, repo2.ID}))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount)
|
||||
assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount)
|
||||
}
|
||||
|
||||
+105
-22
@@ -17,38 +17,83 @@ import (
|
||||
|
||||
// Reaction represents a reactions on issues and comments.
|
||||
type Reaction struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
CommentID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
User *User `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
CommentID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
OriginalAuthorID int64 `xorm:"INDEX UNIQUE(s) NOT NULL DEFAULT(0)"`
|
||||
OriginalAuthor string
|
||||
User *User `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
// FindReactionsOptions describes the conditions to Find reactions
|
||||
type FindReactionsOptions struct {
|
||||
ListOptions
|
||||
IssueID int64
|
||||
CommentID int64
|
||||
UserID int64
|
||||
Reaction string
|
||||
}
|
||||
|
||||
func (opts *FindReactionsOptions) toConds() builder.Cond {
|
||||
//If Issue ID is set add to Query
|
||||
var cond = builder.NewCond()
|
||||
if opts.IssueID > 0 {
|
||||
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
|
||||
}
|
||||
//If CommentID is > 0 add to Query
|
||||
//If it is 0 Query ignore CommentID to select
|
||||
//If it is -1 it explicit search of Issue Reactions where CommentID = 0
|
||||
if opts.CommentID > 0 {
|
||||
cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
|
||||
} else if opts.CommentID == -1 {
|
||||
cond = cond.And(builder.Eq{"reaction.comment_id": 0})
|
||||
}
|
||||
if opts.UserID > 0 {
|
||||
cond = cond.And(builder.Eq{
|
||||
"reaction.user_id": opts.UserID,
|
||||
"reaction.original_author_id": 0,
|
||||
})
|
||||
}
|
||||
if opts.Reaction != "" {
|
||||
cond = cond.And(builder.Eq{"reaction.type": opts.Reaction})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
// FindCommentReactions returns a ReactionList of all reactions from an comment
|
||||
func FindCommentReactions(comment *Comment) (ReactionList, error) {
|
||||
return findReactions(x, FindReactionsOptions{
|
||||
IssueID: comment.IssueID,
|
||||
CommentID: comment.ID})
|
||||
}
|
||||
|
||||
// FindIssueReactions returns a ReactionList of all reactions from an issue
|
||||
func FindIssueReactions(issue *Issue, listOptions ListOptions) (ReactionList, error) {
|
||||
return findReactions(x, FindReactionsOptions{
|
||||
ListOptions: listOptions,
|
||||
IssueID: issue.ID,
|
||||
CommentID: -1,
|
||||
})
|
||||
}
|
||||
|
||||
func findReactions(e Engine, opts FindReactionsOptions) ([]*Reaction, error) {
|
||||
e = e.
|
||||
Where(opts.toConds()).
|
||||
In("reaction.`type`", setting.UI.Reactions).
|
||||
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
|
||||
if opts.Page != 0 {
|
||||
e = opts.setEnginePagination(e)
|
||||
|
||||
reactions := make([]*Reaction, 0, opts.PageSize)
|
||||
return reactions, e.Find(&reactions)
|
||||
}
|
||||
|
||||
reactions := make([]*Reaction, 0, 10)
|
||||
sess := e.Where(opts.toConds())
|
||||
return reactions, sess.
|
||||
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id").
|
||||
Find(&reactions)
|
||||
return reactions, e.Find(&reactions)
|
||||
}
|
||||
|
||||
func createReaction(e *xorm.Session, opts *ReactionOptions) (*Reaction, error) {
|
||||
@@ -57,9 +102,25 @@ func createReaction(e *xorm.Session, opts *ReactionOptions) (*Reaction, error) {
|
||||
UserID: opts.Doer.ID,
|
||||
IssueID: opts.Issue.ID,
|
||||
}
|
||||
findOpts := FindReactionsOptions{
|
||||
IssueID: opts.Issue.ID,
|
||||
CommentID: -1, // reaction to issue only
|
||||
Reaction: opts.Type,
|
||||
UserID: opts.Doer.ID,
|
||||
}
|
||||
if opts.Comment != nil {
|
||||
reaction.CommentID = opts.Comment.ID
|
||||
findOpts.CommentID = opts.Comment.ID
|
||||
}
|
||||
|
||||
existingR, err := findReactions(e, findOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(existingR) > 0 {
|
||||
return existingR[0], ErrReactionAlreadyExist{Reaction: opts.Type}
|
||||
}
|
||||
|
||||
if _, err := e.Insert(reaction); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -76,19 +137,23 @@ type ReactionOptions struct {
|
||||
}
|
||||
|
||||
// CreateReaction creates reaction for issue or comment.
|
||||
func CreateReaction(opts *ReactionOptions) (reaction *Reaction, err error) {
|
||||
func CreateReaction(opts *ReactionOptions) (*Reaction, error) {
|
||||
if !setting.UI.ReactionsMap[opts.Type] {
|
||||
return nil, ErrForbiddenIssueReaction{opts.Type}
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
if err := sess.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reaction, err = createReaction(sess, opts)
|
||||
reaction, err := createReaction(sess, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return reaction, err
|
||||
}
|
||||
|
||||
if err = sess.Commit(); err != nil {
|
||||
if err := sess.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reaction, nil
|
||||
@@ -122,7 +187,7 @@ func deleteReaction(e *xorm.Session, opts *ReactionOptions) error {
|
||||
if opts.Comment != nil {
|
||||
reaction.CommentID = opts.Comment.ID
|
||||
}
|
||||
_, err := e.Delete(reaction)
|
||||
_, err := e.Where("original_author_id = 0").Delete(reaction)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -160,6 +225,19 @@ func DeleteCommentReaction(doer *User, issue *Issue, comment *Comment, content s
|
||||
})
|
||||
}
|
||||
|
||||
// LoadUser load user of reaction
|
||||
func (r *Reaction) LoadUser() (*User, error) {
|
||||
if r.User != nil {
|
||||
return r.User, nil
|
||||
}
|
||||
user, err := getUserByID(x, r.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.User = user
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ReactionList represents list of reactions
|
||||
type ReactionList []*Reaction
|
||||
|
||||
@@ -169,7 +247,7 @@ func (list ReactionList) HasUser(userID int64) bool {
|
||||
return false
|
||||
}
|
||||
for _, reaction := range list {
|
||||
if reaction.UserID == userID {
|
||||
if reaction.OriginalAuthor == "" && reaction.UserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -188,6 +266,9 @@ func (list ReactionList) GroupByType() map[string]ReactionList {
|
||||
func (list ReactionList) getUserIDs() []int64 {
|
||||
userIDs := make(map[int64]struct{}, len(list))
|
||||
for _, reaction := range list {
|
||||
if reaction.OriginalAuthor != "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := userIDs[reaction.UserID]; !ok {
|
||||
userIDs[reaction.UserID] = struct{}{}
|
||||
}
|
||||
@@ -195,7 +276,7 @@ func (list ReactionList) getUserIDs() []int64 {
|
||||
return keysInt64(userIDs)
|
||||
}
|
||||
|
||||
func (list ReactionList) loadUsers(e Engine) ([]*User, error) {
|
||||
func (list ReactionList) loadUsers(e Engine, repo *Repository) ([]*User, error) {
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -210,7 +291,9 @@ func (list ReactionList) loadUsers(e Engine) ([]*User, error) {
|
||||
}
|
||||
|
||||
for _, reaction := range list {
|
||||
if user, ok := userMaps[reaction.UserID]; ok {
|
||||
if reaction.OriginalAuthor != "" {
|
||||
reaction.User = NewReplaceUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
|
||||
} else if user, ok := userMaps[reaction.UserID]; ok {
|
||||
reaction.User = user
|
||||
} else {
|
||||
reaction.User = NewGhostUser()
|
||||
@@ -220,8 +303,8 @@ func (list ReactionList) loadUsers(e Engine) ([]*User, error) {
|
||||
}
|
||||
|
||||
// LoadUsers loads reactions' all users
|
||||
func (list ReactionList) LoadUsers() ([]*User, error) {
|
||||
return list.loadUsers(x)
|
||||
func (list ReactionList) LoadUsers(repo *Repository) ([]*User, error) {
|
||||
return list.loadUsers(x, repo)
|
||||
}
|
||||
|
||||
// GetFirstUsers returns first reacted user display names separated by comma
|
||||
|
||||
@@ -50,9 +50,10 @@ func TestIssueAddDuplicateReaction(t *testing.T) {
|
||||
Type: "heart",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, reaction)
|
||||
assert.Equal(t, ErrReactionAlreadyExist{Reaction: "heart"}, err)
|
||||
|
||||
AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1.ID})
|
||||
existingR := AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1.ID}).(*Reaction)
|
||||
assert.Equal(t, existingR.ID, reaction.ID)
|
||||
}
|
||||
|
||||
func TestIssueDeleteReaction(t *testing.T) {
|
||||
@@ -81,22 +82,22 @@ func TestIssueReactionCount(t *testing.T) {
|
||||
user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
|
||||
ghost := NewGhostUser()
|
||||
|
||||
issue1 := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
|
||||
|
||||
addReaction(t, user1, issue1, nil, "heart")
|
||||
addReaction(t, user2, issue1, nil, "heart")
|
||||
addReaction(t, user3, issue1, nil, "heart")
|
||||
addReaction(t, user3, issue1, nil, "+1")
|
||||
addReaction(t, user4, issue1, nil, "+1")
|
||||
addReaction(t, user4, issue1, nil, "heart")
|
||||
addReaction(t, ghost, issue1, nil, "-1")
|
||||
addReaction(t, user1, issue, nil, "heart")
|
||||
addReaction(t, user2, issue, nil, "heart")
|
||||
addReaction(t, user3, issue, nil, "heart")
|
||||
addReaction(t, user3, issue, nil, "+1")
|
||||
addReaction(t, user4, issue, nil, "+1")
|
||||
addReaction(t, user4, issue, nil, "heart")
|
||||
addReaction(t, ghost, issue, nil, "-1")
|
||||
|
||||
err := issue1.loadReactions(x)
|
||||
err := issue.loadReactions(x)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, issue1.Reactions, 7)
|
||||
assert.Len(t, issue.Reactions, 7)
|
||||
|
||||
reactions := issue1.Reactions.GroupByType()
|
||||
reactions := issue.Reactions.GroupByType()
|
||||
assert.Len(t, reactions["heart"], 4)
|
||||
assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
|
||||
assert.Equal(t, user1.DisplayName()+", "+user2.DisplayName(), reactions["heart"].GetFirstUsers())
|
||||
@@ -129,9 +130,9 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
|
||||
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
user3 := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
|
||||
user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
|
||||
ghost := NewGhostUser()
|
||||
|
||||
issue1 := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
|
||||
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: issue1.RepoID}).(*Repository)
|
||||
|
||||
comment1 := AssertExistsAndLoadBean(t, &Comment{ID: 1}).(*Comment)
|
||||
|
||||
@@ -139,14 +140,13 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
|
||||
addReaction(t, user2, issue1, comment1, "heart")
|
||||
addReaction(t, user3, issue1, comment1, "heart")
|
||||
addReaction(t, user4, issue1, comment1, "+1")
|
||||
addReaction(t, ghost, issue1, comment1, "heart")
|
||||
|
||||
err := comment1.LoadReactions()
|
||||
err := comment1.LoadReactions(repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comment1.Reactions, 5)
|
||||
assert.Len(t, comment1.Reactions, 4)
|
||||
|
||||
reactions := comment1.Reactions.GroupByType()
|
||||
assert.Len(t, reactions["heart"], 4)
|
||||
assert.Len(t, reactions["heart"], 3)
|
||||
assert.Len(t, reactions["+1"], 1)
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ func TestIssueCommentReactionCount(t *testing.T) {
|
||||
comment1 := AssertExistsAndLoadBean(t, &Comment{ID: 1}).(*Comment)
|
||||
|
||||
addReaction(t, user1, issue1, comment1, "heart")
|
||||
DeleteCommentReaction(user1, issue1, comment1, "heart")
|
||||
assert.NoError(t, DeleteCommentReaction(user1, issue1, comment1, "heart"))
|
||||
|
||||
AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1.ID, CommentID: comment1.ID})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,9 @@ type Stopwatch struct {
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
// Stopwatches is a List ful of Stopwatch
|
||||
type Stopwatches []Stopwatch
|
||||
|
||||
func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
|
||||
sw = new(Stopwatch)
|
||||
exists, err = e.
|
||||
@@ -28,6 +32,21 @@ func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool,
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserStopwatches return list of all stopwatches of a user
|
||||
func GetUserStopwatches(userID int64, listOptions ListOptions) (*Stopwatches, error) {
|
||||
sws := new(Stopwatches)
|
||||
sess := x.Where("stopwatch.user_id = ?", userID)
|
||||
if listOptions.Page != 0 {
|
||||
sess = listOptions.setSessionPagination(sess)
|
||||
}
|
||||
|
||||
err := sess.Find(sws)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sws, nil
|
||||
}
|
||||
|
||||
// StopwatchExists returns true if the stopwatch exists
|
||||
func StopwatchExists(userID int64, issueID int64) bool {
|
||||
_, exists, _ := getStopwatch(x, userID, issueID)
|
||||
@@ -160,3 +179,28 @@ func SecToTime(duration int64) string {
|
||||
|
||||
return hrs
|
||||
}
|
||||
|
||||
// APIFormat convert Stopwatch type to api.StopWatch type
|
||||
func (sw *Stopwatch) APIFormat() (api.StopWatch, error) {
|
||||
issue, err := getIssueByID(x, sw.IssueID)
|
||||
if err != nil {
|
||||
return api.StopWatch{}, err
|
||||
}
|
||||
return api.StopWatch{
|
||||
Created: sw.CreatedUnix.AsTime(),
|
||||
IssueIndex: issue.Index,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// APIFormat convert Stopwatches type to api.StopWatches type
|
||||
func (sws Stopwatches) APIFormat() (api.StopWatches, error) {
|
||||
result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
|
||||
for _, sw := range sws {
|
||||
apiSW, err := sw.APIFormat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, apiSW)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
|
||||
+69
-17
@@ -61,15 +61,17 @@ func TestGetIssuesByIDs(t *testing.T) {
|
||||
testSuccess([]int64{1, 2, 3}, []int64{NonexistentID})
|
||||
}
|
||||
|
||||
func TestGetParticipantsByIssueID(t *testing.T) {
|
||||
func TestGetParticipantIDsByIssue(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
checkParticipants := func(issueID int64, userIDs []int) {
|
||||
participants, err := GetParticipantsByIssueID(issueID)
|
||||
issue, err := GetIssueByID(issueID)
|
||||
assert.NoError(t, err)
|
||||
participants, err := issue.getParticipantIDsByIssue(x)
|
||||
if assert.NoError(t, err) {
|
||||
participantsIDs := make([]int, len(participants))
|
||||
for i, u := range participants {
|
||||
participantsIDs[i] = int(u.ID)
|
||||
for i, uid := range participants {
|
||||
participantsIDs[i] = int(uid)
|
||||
}
|
||||
sort.Ints(participantsIDs)
|
||||
sort.Ints(userIDs)
|
||||
@@ -81,7 +83,7 @@ func TestGetParticipantsByIssueID(t *testing.T) {
|
||||
// User 2 only labeled issue1 (see fixtures/comment.yml)
|
||||
// Users 3 and 5 made actual comments (see fixtures/comment.yml)
|
||||
// User 3 is inactive, thus not active participant
|
||||
checkParticipants(1, []int{5})
|
||||
checkParticipants(1, []int{1, 5})
|
||||
}
|
||||
|
||||
func TestIssue_ClearLabels(t *testing.T) {
|
||||
@@ -139,24 +141,30 @@ func TestIssues(t *testing.T) {
|
||||
IssuesOptions{
|
||||
RepoIDs: []int64{1, 3},
|
||||
SortType: "oldest",
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
ListOptions: ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
},
|
||||
},
|
||||
[]int64{1, 2, 3, 5},
|
||||
},
|
||||
{
|
||||
IssuesOptions{
|
||||
LabelIDs: []int64{1},
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
ListOptions: ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
},
|
||||
},
|
||||
[]int64{2, 1},
|
||||
},
|
||||
{
|
||||
IssuesOptions{
|
||||
LabelIDs: []int64{1, 2},
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
ListOptions: ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
},
|
||||
},
|
||||
[]int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests
|
||||
},
|
||||
@@ -180,7 +188,7 @@ func TestGetUserIssueStats(t *testing.T) {
|
||||
{
|
||||
UserIssueStatsOptions{
|
||||
UserID: 1,
|
||||
RepoID: 1,
|
||||
RepoIDs: []int64{1},
|
||||
FilterMode: FilterModeAll,
|
||||
},
|
||||
IssueStats{
|
||||
@@ -245,6 +253,20 @@ func TestGetUserIssueStats(t *testing.T) {
|
||||
ClosedCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
UserIssueStatsOptions{
|
||||
UserID: 1,
|
||||
FilterMode: FilterModeCreate,
|
||||
IssueIDs: []int64{1},
|
||||
},
|
||||
IssueStats{
|
||||
YourRepositoriesCount: 0,
|
||||
AssignCount: 1,
|
||||
CreateCount: 1,
|
||||
OpenCount: 1,
|
||||
ClosedCount: 0,
|
||||
},
|
||||
},
|
||||
} {
|
||||
stats, err := GetUserIssueStats(test.Opts)
|
||||
if !assert.NoError(t, err) {
|
||||
@@ -259,7 +281,7 @@ func TestIssue_loadTotalTimes(t *testing.T) {
|
||||
ms, err := GetIssueByID(2)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, ms.loadTotalTimes(x))
|
||||
assert.Equal(t, int64(3662), ms.TotalTrackedTime)
|
||||
assert.Equal(t, int64(3682), ms.TotalTrackedTime)
|
||||
}
|
||||
|
||||
func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
|
||||
@@ -276,8 +298,8 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
|
||||
|
||||
total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, total)
|
||||
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
|
||||
assert.EqualValues(t, 5, total)
|
||||
assert.EqualValues(t, []int64{1, 2, 3, 5, 11}, ids)
|
||||
|
||||
// issue1's comment id 2
|
||||
total, ids, err = SearchIssueIDsByKeyword("good", []int64{1}, 10, 0)
|
||||
@@ -286,6 +308,36 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
|
||||
assert.EqualValues(t, []int64{1}, ids)
|
||||
}
|
||||
|
||||
func TestGetRepoIDsForIssuesOptions(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
for _, test := range []struct {
|
||||
Opts IssuesOptions
|
||||
ExpectedRepoIDs []int64
|
||||
}{
|
||||
{
|
||||
IssuesOptions{
|
||||
AssigneeID: 2,
|
||||
},
|
||||
[]int64{3},
|
||||
},
|
||||
{
|
||||
IssuesOptions{
|
||||
RepoIDs: []int64{1, 2},
|
||||
},
|
||||
[]int64{1, 2},
|
||||
},
|
||||
} {
|
||||
repoIDs, err := GetRepoIDsForIssuesOptions(&test.Opts, user)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, repoIDs, len(test.ExpectedRepoIDs)) {
|
||||
for i, repoID := range repoIDs {
|
||||
assert.EqualValues(t, test.ExpectedRepoIDs[i], repoID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testInsertIssue(t *testing.T, title, content string) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
@@ -305,8 +357,8 @@ func testInsertIssue(t *testing.T, title, content string) {
|
||||
assert.True(t, has)
|
||||
assert.EqualValues(t, issue.Title, newIssue.Title)
|
||||
assert.EqualValues(t, issue.Content, newIssue.Content)
|
||||
// there are 4 issues and max index is 4 on repository 1, so this one should 5
|
||||
assert.EqualValues(t, 5, newIssue.Index)
|
||||
// there are 5 issues and max index is 5 on repository 1, so this one should 6
|
||||
assert.EqualValues(t, 6, newIssue.Index)
|
||||
|
||||
_, err = x.ID(issue.ID).Delete(new(Issue))
|
||||
assert.NoError(t, err)
|
||||
|
||||
+218
-37
@@ -8,49 +8,80 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// TrackedTime represents a time that was spent for a specific issue.
|
||||
type TrackedTime struct {
|
||||
ID int64 `xorm:"pk autoincr" json:"id"`
|
||||
IssueID int64 `xorm:"INDEX" json:"issue_id"`
|
||||
UserID int64 `xorm:"INDEX" json:"user_id"`
|
||||
Created time.Time `xorm:"-" json:"created"`
|
||||
CreatedUnix int64 `xorm:"created" json:"-"`
|
||||
Time int64 `json:"time"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
User *User `xorm:"-"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64 `xorm:"created"`
|
||||
Time int64 `xorm:"NOT NULL"`
|
||||
Deleted bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
// TrackedTimeList is a List of TrackedTime's
|
||||
type TrackedTimeList []*TrackedTime
|
||||
|
||||
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||
func (t *TrackedTime) AfterLoad() {
|
||||
t.Created = time.Unix(t.CreatedUnix, 0).In(setting.DefaultUILocation)
|
||||
}
|
||||
|
||||
// APIFormat converts TrackedTime to API format
|
||||
func (t *TrackedTime) APIFormat() *api.TrackedTime {
|
||||
return &api.TrackedTime{
|
||||
ID: t.ID,
|
||||
IssueID: t.IssueID,
|
||||
UserID: t.UserID,
|
||||
Time: t.Time,
|
||||
Created: t.Created,
|
||||
// LoadAttributes load Issue, User
|
||||
func (t *TrackedTime) LoadAttributes() (err error) {
|
||||
return t.loadAttributes(x)
|
||||
}
|
||||
|
||||
func (t *TrackedTime) loadAttributes(e Engine) (err error) {
|
||||
if t.Issue == nil {
|
||||
t.Issue, err = getIssueByID(e, t.IssueID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = t.Issue.loadRepo(e)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if t.User == nil {
|
||||
t.User, err = getUserByID(e, t.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadAttributes load Issue, User
|
||||
func (tl TrackedTimeList) LoadAttributes() (err error) {
|
||||
for _, t := range tl {
|
||||
if err = t.LoadAttributes(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
|
||||
type FindTrackedTimesOptions struct {
|
||||
IssueID int64
|
||||
UserID int64
|
||||
RepositoryID int64
|
||||
MilestoneID int64
|
||||
ListOptions
|
||||
IssueID int64
|
||||
UserID int64
|
||||
RepositoryID int64
|
||||
MilestoneID int64
|
||||
CreatedAfterUnix int64
|
||||
CreatedBeforeUnix int64
|
||||
}
|
||||
|
||||
// ToCond will convert each condition into a xorm-Cond
|
||||
func (opts *FindTrackedTimesOptions) ToCond() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
cond := builder.NewCond().And(builder.Eq{"tracked_time.deleted": false})
|
||||
if opts.IssueID != 0 {
|
||||
cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
|
||||
}
|
||||
@@ -63,45 +94,95 @@ func (opts *FindTrackedTimesOptions) ToCond() builder.Cond {
|
||||
if opts.MilestoneID != 0 {
|
||||
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
|
||||
}
|
||||
|
||||
// ToSession will convert the given options to a xorm Session by using the conditions from ToCond and joining with issue table if required
|
||||
func (opts *FindTrackedTimesOptions) ToSession(e Engine) *xorm.Session {
|
||||
func (opts *FindTrackedTimesOptions) ToSession(e Engine) Engine {
|
||||
sess := e
|
||||
if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
|
||||
return e.Join("INNER", "issue", "issue.id = tracked_time.issue_id").Where(opts.ToCond())
|
||||
sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
|
||||
}
|
||||
return x.Where(opts.ToCond())
|
||||
|
||||
sess = sess.Where(opts.ToCond())
|
||||
|
||||
if opts.Page != 0 {
|
||||
sess = opts.setEnginePagination(sess)
|
||||
}
|
||||
|
||||
return sess
|
||||
}
|
||||
|
||||
// GetTrackedTimes returns all tracked times that fit to the given options.
|
||||
func GetTrackedTimes(options FindTrackedTimesOptions) (trackedTimes []*TrackedTime, err error) {
|
||||
err = options.ToSession(x).Find(&trackedTimes)
|
||||
func getTrackedTimes(e Engine, options FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) {
|
||||
err = options.ToSession(e).Find(&trackedTimes)
|
||||
return
|
||||
}
|
||||
|
||||
// GetTrackedTimes returns all tracked times that fit to the given options.
|
||||
func GetTrackedTimes(opts FindTrackedTimesOptions) (TrackedTimeList, error) {
|
||||
return getTrackedTimes(x, opts)
|
||||
}
|
||||
|
||||
func getTrackedSeconds(e Engine, opts FindTrackedTimesOptions) (trackedSeconds int64, err error) {
|
||||
return opts.ToSession(e).SumInt(&TrackedTime{}, "time")
|
||||
}
|
||||
|
||||
// GetTrackedSeconds return sum of seconds
|
||||
func GetTrackedSeconds(opts FindTrackedTimesOptions) (int64, error) {
|
||||
return getTrackedSeconds(x, opts)
|
||||
}
|
||||
|
||||
// AddTime will add the given time (in seconds) to the issue
|
||||
func AddTime(user *User, issue *Issue, time int64) (*TrackedTime, error) {
|
||||
tt := &TrackedTime{
|
||||
IssueID: issue.ID,
|
||||
UserID: user.ID,
|
||||
Time: time,
|
||||
}
|
||||
if _, err := x.Insert(tt); err != nil {
|
||||
func AddTime(user *User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := issue.loadRepo(x); err != nil {
|
||||
|
||||
t, err := addTime(sess, user, issue, amount, created)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := CreateComment(&CreateCommentOptions{
|
||||
|
||||
if err := issue.loadRepo(sess); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := createComment(sess, &CreateCommentOptions{
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Doer: user,
|
||||
Content: SecToTime(time),
|
||||
Content: SecToTime(amount),
|
||||
Type: CommentTypeAddTimeManual,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, sess.Commit()
|
||||
}
|
||||
|
||||
func addTime(e Engine, user *User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) {
|
||||
if created.IsZero() {
|
||||
created = time.Now()
|
||||
}
|
||||
tt := &TrackedTime{
|
||||
IssueID: issue.ID,
|
||||
UserID: user.ID,
|
||||
Time: amount,
|
||||
Created: created,
|
||||
}
|
||||
if _, err := e.Insert(tt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tt, nil
|
||||
}
|
||||
|
||||
@@ -131,3 +212,103 @@ func TotalTimes(options FindTrackedTimesOptions) (map[*User]string, error) {
|
||||
}
|
||||
return totalTimes, nil
|
||||
}
|
||||
|
||||
// DeleteIssueUserTimes deletes times for issue
|
||||
func DeleteIssueUserTimes(issue *Issue, user *User) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := FindTrackedTimesOptions{
|
||||
IssueID: issue.ID,
|
||||
UserID: user.ID,
|
||||
}
|
||||
|
||||
removedTime, err := deleteTimes(sess, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if removedTime == 0 {
|
||||
return ErrNotExist{}
|
||||
}
|
||||
|
||||
if err := issue.loadRepo(sess); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := createComment(sess, &CreateCommentOptions{
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Doer: user,
|
||||
Content: "- " + SecToTime(removedTime),
|
||||
Type: CommentTypeDeleteTimeManual,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// DeleteTime delete a specific Time
|
||||
func DeleteTime(t *TrackedTime) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.loadAttributes(sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deleteTime(sess, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := createComment(sess, &CreateCommentOptions{
|
||||
Issue: t.Issue,
|
||||
Repo: t.Issue.Repo,
|
||||
Doer: t.User,
|
||||
Content: "- " + SecToTime(t.Time),
|
||||
Type: CommentTypeDeleteTimeManual,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func deleteTimes(e Engine, opts FindTrackedTimesOptions) (removedTime int64, err error) {
|
||||
|
||||
removedTime, err = getTrackedSeconds(e, opts)
|
||||
if err != nil || removedTime == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = opts.ToSession(e).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true})
|
||||
return
|
||||
}
|
||||
|
||||
func deleteTime(e Engine, t *TrackedTime) error {
|
||||
if t.Deleted {
|
||||
return ErrNotExist{ID: t.ID}
|
||||
}
|
||||
t.Deleted = true
|
||||
_, err := e.ID(t.ID).Cols("deleted").Update(t)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id
|
||||
func GetTrackedTimeByID(id int64) (*TrackedTime, error) {
|
||||
time := new(TrackedTime)
|
||||
has, err := x.ID(id).Get(time)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrNotExist{ID: id}
|
||||
}
|
||||
return time, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// Copyright 2019 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -16,14 +21,14 @@ func TestAddTime(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
//3661 = 1h 1min 1s
|
||||
trackedTime, err := AddTime(user3, issue1, 3661)
|
||||
trackedTime, err := AddTime(user3, issue1, 3661, time.Now())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), trackedTime.UserID)
|
||||
assert.Equal(t, int64(1), trackedTime.IssueID)
|
||||
assert.Equal(t, int64(3661), trackedTime.Time)
|
||||
|
||||
tt := AssertExistsAndLoadBean(t, &TrackedTime{UserID: 3, IssueID: 1}).(*TrackedTime)
|
||||
assert.Equal(t, tt.Time, int64(3661))
|
||||
assert.Equal(t, int64(3661), tt.Time)
|
||||
|
||||
comment := AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment)
|
||||
assert.Equal(t, comment.Content, "1h 1min 1s")
|
||||
@@ -36,7 +41,7 @@ func TestGetTrackedTimes(t *testing.T) {
|
||||
times, err := GetTrackedTimes(FindTrackedTimesOptions{IssueID: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 1)
|
||||
assert.Equal(t, times[0].Time, int64(400))
|
||||
assert.Equal(t, int64(400), times[0].Time)
|
||||
|
||||
times, err = GetTrackedTimes(FindTrackedTimesOptions{IssueID: -1})
|
||||
assert.NoError(t, err)
|
||||
@@ -45,8 +50,8 @@ func TestGetTrackedTimes(t *testing.T) {
|
||||
// by User
|
||||
times, err = GetTrackedTimes(FindTrackedTimesOptions{UserID: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 1)
|
||||
assert.Equal(t, times[0].Time, int64(400))
|
||||
assert.Len(t, times, 3)
|
||||
assert.Equal(t, int64(400), times[0].Time)
|
||||
|
||||
times, err = GetTrackedTimes(FindTrackedTimesOptions{UserID: 3})
|
||||
assert.NoError(t, err)
|
||||
@@ -55,15 +60,15 @@ func TestGetTrackedTimes(t *testing.T) {
|
||||
// by Repo
|
||||
times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 1)
|
||||
assert.Equal(t, times[0].Time, int64(1))
|
||||
assert.Len(t, times, 3)
|
||||
assert.Equal(t, int64(1), times[0].Time)
|
||||
issue, err := GetIssueByID(times[0].IssueID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, issue.RepoID, int64(2))
|
||||
|
||||
times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 4)
|
||||
assert.Len(t, times, 5)
|
||||
|
||||
times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 10})
|
||||
assert.NoError(t, err)
|
||||
@@ -83,10 +88,15 @@ func TestTotalTimes(t *testing.T) {
|
||||
|
||||
total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, total, 1)
|
||||
assert.Len(t, total, 2)
|
||||
for user, time := range total {
|
||||
assert.Equal(t, int64(2), user.ID)
|
||||
assert.Equal(t, "1h 1min 2s", time)
|
||||
if user.ID == 2 {
|
||||
assert.Equal(t, "1h 1min 2s", time)
|
||||
} else if user.ID == 1 {
|
||||
assert.Equal(t, "20s", time)
|
||||
} else {
|
||||
assert.Error(t, assert.AnError)
|
||||
}
|
||||
}
|
||||
|
||||
total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 5})
|
||||
@@ -99,5 +109,5 @@ func TestTotalTimes(t *testing.T) {
|
||||
|
||||
total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, total, 0)
|
||||
assert.Len(t, total, 2)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func Test_newIssueUsers(t *testing.T) {
|
||||
newIssue := &Issue{
|
||||
RepoID: repo.ID,
|
||||
PosterID: 4,
|
||||
Index: 5,
|
||||
Index: 6,
|
||||
Title: "newTestIssueTitle",
|
||||
Content: "newTestIssueContent",
|
||||
}
|
||||
|
||||
+55
-41
@@ -4,7 +4,9 @@
|
||||
|
||||
package models
|
||||
|
||||
import "code.gitea.io/gitea/modules/timeutil"
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// IssueWatch is connection request for receiving issue notification.
|
||||
type IssueWatch struct {
|
||||
@@ -46,11 +48,13 @@ func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIssueWatch returns an issue watch by user and issue
|
||||
// GetIssueWatch returns all IssueWatch objects from db by user and issue
|
||||
// the current Web-UI need iw object for watchers AND explicit non-watchers
|
||||
func GetIssueWatch(userID, issueID int64) (iw *IssueWatch, exists bool, err error) {
|
||||
return getIssueWatch(x, userID, issueID)
|
||||
}
|
||||
|
||||
// Return watcher AND explicit non-watcher if entry in db exist
|
||||
func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool, err error) {
|
||||
iw = new(IssueWatch)
|
||||
exists, err = e.
|
||||
@@ -60,55 +64,65 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool
|
||||
return
|
||||
}
|
||||
|
||||
// GetIssueWatchers returns watchers/unwatchers of a given issue
|
||||
func GetIssueWatchers(issueID int64) (IssueWatchList, error) {
|
||||
return getIssueWatchers(x, issueID)
|
||||
// CheckIssueWatch check if an user is watching an issue
|
||||
// it takes participants and repo watch into account
|
||||
func CheckIssueWatch(user *User, issue *Issue) (bool, error) {
|
||||
iw, exist, err := getIssueWatch(x, user.ID, issue.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if exist {
|
||||
return iw.IsWatching, nil
|
||||
}
|
||||
w, err := getWatch(x, user.ID, issue.RepoID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return isWatchMode(w.Mode) || IsUserParticipantsOfIssue(user, issue), nil
|
||||
}
|
||||
|
||||
func getIssueWatchers(e Engine, issueID int64) (watches IssueWatchList, err error) {
|
||||
err = e.
|
||||
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
|
||||
// but avoids joining with `user` for performance reasons
|
||||
// User permissions must be verified elsewhere if required
|
||||
func GetIssueWatchersIDs(issueID int64, watching bool) ([]int64, error) {
|
||||
return getIssueWatchersIDs(x, issueID, watching)
|
||||
}
|
||||
|
||||
func getIssueWatchersIDs(e Engine, issueID int64, watching bool) ([]int64, error) {
|
||||
ids := make([]int64, 0, 64)
|
||||
return ids, e.Table("issue_watch").
|
||||
Where("issue_id=?", issueID).
|
||||
And("is_watching = ?", watching).
|
||||
Select("user_id").
|
||||
Find(&ids)
|
||||
}
|
||||
|
||||
// GetIssueWatchers returns watchers/unwatchers of a given issue
|
||||
func GetIssueWatchers(issueID int64, listOptions ListOptions) (IssueWatchList, error) {
|
||||
return getIssueWatchers(x, issueID, listOptions)
|
||||
}
|
||||
|
||||
func getIssueWatchers(e Engine, issueID int64, listOptions ListOptions) (IssueWatchList, error) {
|
||||
sess := e.
|
||||
Where("`issue_watch`.issue_id = ?", issueID).
|
||||
And("`issue_watch`.is_watching = ?", true).
|
||||
And("`user`.is_active = ?", true).
|
||||
And("`user`.prohibit_login = ?", false).
|
||||
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id").
|
||||
Find(&watches)
|
||||
return
|
||||
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = listOptions.setSessionPagination(sess)
|
||||
watches := make([]*IssueWatch, 0, listOptions.PageSize)
|
||||
return watches, sess.Find(&watches)
|
||||
}
|
||||
watches := make([]*IssueWatch, 0, 8)
|
||||
return watches, sess.Find(&watches)
|
||||
}
|
||||
|
||||
func removeIssueWatchersByRepoID(e Engine, userID int64, repoID int64) error {
|
||||
iw := &IssueWatch{
|
||||
IsWatching: false,
|
||||
}
|
||||
_, err := e.
|
||||
Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID).
|
||||
Cols("is_watching", "updated_unix").
|
||||
Where("`issue_watch`.user_id = ?", userID).
|
||||
Update(iw)
|
||||
Delete(new(IssueWatch))
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadWatchUsers return watching users
|
||||
func (iwl IssueWatchList) LoadWatchUsers() (users UserList, err error) {
|
||||
return iwl.loadWatchUsers(x)
|
||||
}
|
||||
|
||||
func (iwl IssueWatchList) loadWatchUsers(e Engine) (users UserList, err error) {
|
||||
if len(iwl) == 0 {
|
||||
return []*User{}, nil
|
||||
}
|
||||
|
||||
var userIDs = make([]int64, 0, len(iwl))
|
||||
for _, iw := range iwl {
|
||||
if iw.IsWatching {
|
||||
userIDs = append(userIDs, iw.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(userIDs) == 0 {
|
||||
return []*User{}, nil
|
||||
}
|
||||
|
||||
err = e.In("id", userIDs).Find(&users)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+20
-12
@@ -15,42 +15,50 @@ func TestCreateOrUpdateIssueWatch(t *testing.T) {
|
||||
|
||||
assert.NoError(t, CreateOrUpdateIssueWatch(3, 1, true))
|
||||
iw := AssertExistsAndLoadBean(t, &IssueWatch{UserID: 3, IssueID: 1}).(*IssueWatch)
|
||||
assert.Equal(t, true, iw.IsWatching)
|
||||
assert.True(t, iw.IsWatching)
|
||||
|
||||
assert.NoError(t, CreateOrUpdateIssueWatch(1, 1, false))
|
||||
iw = AssertExistsAndLoadBean(t, &IssueWatch{UserID: 1, IssueID: 1}).(*IssueWatch)
|
||||
assert.Equal(t, false, iw.IsWatching)
|
||||
assert.False(t, iw.IsWatching)
|
||||
}
|
||||
|
||||
func TestGetIssueWatch(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
_, exists, err := GetIssueWatch(9, 1)
|
||||
assert.Equal(t, true, exists)
|
||||
assert.True(t, exists)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, exists, err = GetIssueWatch(2, 2)
|
||||
assert.Equal(t, true, exists)
|
||||
iw, exists, err := GetIssueWatch(2, 2)
|
||||
assert.True(t, exists)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, false, iw.IsWatching)
|
||||
|
||||
_, exists, err = GetIssueWatch(3, 1)
|
||||
assert.Equal(t, false, exists)
|
||||
assert.False(t, exists)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetIssueWatchers(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
iws, err := GetIssueWatchers(1)
|
||||
iws, err := GetIssueWatchers(1, ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
// Watcher is inactive, thus 0
|
||||
assert.Equal(t, 0, len(iws))
|
||||
assert.Len(t, iws, 0)
|
||||
|
||||
iws, err = GetIssueWatchers(2)
|
||||
iws, err = GetIssueWatchers(2, ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(iws))
|
||||
// Watcher is explicit not watching
|
||||
assert.Len(t, iws, 0)
|
||||
|
||||
iws, err = GetIssueWatchers(5)
|
||||
iws, err = GetIssueWatchers(5, ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(iws))
|
||||
// Issue has no Watchers
|
||||
assert.Len(t, iws, 0)
|
||||
|
||||
iws, err = GetIssueWatchers(7, ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
// Issue has one watcher
|
||||
assert.Len(t, iws, 1)
|
||||
}
|
||||
|
||||
+140
-50
@@ -5,6 +5,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
|
||||
@@ -23,43 +25,30 @@ type crossReferencesContext struct {
|
||||
Doer *User
|
||||
OrigIssue *Issue
|
||||
OrigComment *Comment
|
||||
RemoveOld bool
|
||||
}
|
||||
|
||||
func newCrossReference(e *xorm.Session, ctx *crossReferencesContext, xref *crossReference) error {
|
||||
var refCommentID int64
|
||||
if ctx.OrigComment != nil {
|
||||
refCommentID = ctx.OrigComment.ID
|
||||
}
|
||||
_, err := createComment(e, &CreateCommentOptions{
|
||||
Type: ctx.Type,
|
||||
Doer: ctx.Doer,
|
||||
Repo: xref.Issue.Repo,
|
||||
Issue: xref.Issue,
|
||||
RefRepoID: ctx.OrigIssue.RepoID,
|
||||
RefIssueID: ctx.OrigIssue.ID,
|
||||
RefCommentID: refCommentID,
|
||||
RefAction: xref.Action,
|
||||
RefIsPull: xref.Issue.IsPull,
|
||||
})
|
||||
return err
|
||||
func findOldCrossReferences(e Engine, issueID int64, commentID int64) ([]*Comment, error) {
|
||||
active := make([]*Comment, 0, 10)
|
||||
return active, e.Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens).
|
||||
And("`ref_issue_id` = ?", issueID).
|
||||
And("`ref_comment_id` = ?", commentID).
|
||||
Find(&active)
|
||||
}
|
||||
|
||||
func neuterCrossReferences(e Engine, issueID int64, commentID int64) error {
|
||||
active := make([]*Comment, 0, 10)
|
||||
sess := e.Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens)
|
||||
if issueID != 0 {
|
||||
sess = sess.And("`ref_issue_id` = ?", issueID)
|
||||
}
|
||||
if commentID != 0 {
|
||||
sess = sess.And("`ref_comment_id` = ?", commentID)
|
||||
}
|
||||
if err := sess.Find(&active); err != nil || len(active) == 0 {
|
||||
active, err := findOldCrossReferences(e, issueID, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids := make([]int64, len(active))
|
||||
for i, c := range active {
|
||||
ids[i] = c.ID
|
||||
}
|
||||
return neuterCrossReferencesIds(e, ids)
|
||||
}
|
||||
|
||||
func neuterCrossReferencesIds(e Engine, ids []int64) error {
|
||||
_, err := e.In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered})
|
||||
return err
|
||||
}
|
||||
@@ -72,7 +61,7 @@ func neuterCrossReferences(e Engine, issueID int64, commentID int64) error {
|
||||
// \/ \/ \/
|
||||
//
|
||||
|
||||
func (issue *Issue) addCrossReferences(e *xorm.Session, doer *User) error {
|
||||
func (issue *Issue) addCrossReferences(e *xorm.Session, doer *User, removeOld bool) error {
|
||||
var commentType CommentType
|
||||
if issue.IsPull {
|
||||
commentType = CommentTypePullRef
|
||||
@@ -83,6 +72,7 @@ func (issue *Issue) addCrossReferences(e *xorm.Session, doer *User) error {
|
||||
Type: commentType,
|
||||
Doer: doer,
|
||||
OrigIssue: issue,
|
||||
RemoveOld: removeOld,
|
||||
}
|
||||
return issue.createCrossReferences(e, ctx, issue.Title, issue.Content)
|
||||
}
|
||||
@@ -92,8 +82,53 @@ func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesC
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.RemoveOld {
|
||||
var commentID int64
|
||||
if ctx.OrigComment != nil {
|
||||
commentID = ctx.OrigComment.ID
|
||||
}
|
||||
active, err := findOldCrossReferences(e, ctx.OrigIssue.ID, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids := make([]int64, 0, len(active))
|
||||
for _, c := range active {
|
||||
found := false
|
||||
for i, x := range xreflist {
|
||||
if x.Issue.ID == c.IssueID && x.Action == c.RefAction {
|
||||
found = true
|
||||
xreflist = append(xreflist[:i], xreflist[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
if err = neuterCrossReferencesIds(e, ids); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, xref := range xreflist {
|
||||
if err = newCrossReference(e, ctx, xref); err != nil {
|
||||
var refCommentID int64
|
||||
if ctx.OrigComment != nil {
|
||||
refCommentID = ctx.OrigComment.ID
|
||||
}
|
||||
var opts = &CreateCommentOptions{
|
||||
Type: ctx.Type,
|
||||
Doer: ctx.Doer,
|
||||
Repo: xref.Issue.Repo,
|
||||
Issue: xref.Issue,
|
||||
RefRepoID: ctx.OrigIssue.RepoID,
|
||||
RefIssueID: ctx.OrigIssue.ID,
|
||||
RefCommentID: refCommentID,
|
||||
RefAction: xref.Action,
|
||||
RefIsPull: ctx.OrigIssue.IsPull,
|
||||
}
|
||||
_, err := createComment(e, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -103,9 +138,10 @@ func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesC
|
||||
func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) {
|
||||
xreflist := make([]*crossReference, 0, 5)
|
||||
var (
|
||||
refRepo *Repository
|
||||
refIssue *Issue
|
||||
err error
|
||||
refRepo *Repository
|
||||
refIssue *Issue
|
||||
refAction references.XRefAction
|
||||
err error
|
||||
)
|
||||
|
||||
allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...)
|
||||
@@ -127,15 +163,13 @@ func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesCont
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if refIssue, err = ctx.OrigIssue.findReferencedIssue(e, ctx, refRepo, ref.Index); err != nil {
|
||||
if refIssue, refAction, err = ctx.OrigIssue.verifyReferencedIssue(e, ctx, refRepo, ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if refIssue != nil {
|
||||
xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{
|
||||
Issue: refIssue,
|
||||
// FIXME: currently ignore keywords
|
||||
// Action: ref.Action,
|
||||
Action: references.XRefActionNone,
|
||||
Issue: refIssue,
|
||||
Action: refAction,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -158,29 +192,47 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross
|
||||
return append(list, xref)
|
||||
}
|
||||
|
||||
func (issue *Issue) findReferencedIssue(e Engine, ctx *crossReferencesContext, repo *Repository, index int64) (*Issue, error) {
|
||||
refIssue := &Issue{RepoID: repo.ID, Index: index}
|
||||
// verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what
|
||||
func (issue *Issue) verifyReferencedIssue(e Engine, ctx *crossReferencesContext, repo *Repository,
|
||||
ref references.IssueReference) (*Issue, references.XRefAction, error) {
|
||||
|
||||
refIssue := &Issue{RepoID: repo.ID, Index: ref.Index}
|
||||
refAction := ref.Action
|
||||
|
||||
if has, _ := e.Get(refIssue); !has {
|
||||
return nil, nil
|
||||
return nil, references.XRefActionNone, nil
|
||||
}
|
||||
if err := refIssue.loadRepo(e); err != nil {
|
||||
return nil, err
|
||||
return nil, references.XRefActionNone, err
|
||||
}
|
||||
// Check user permissions
|
||||
if refIssue.RepoID != ctx.OrigIssue.RepoID {
|
||||
|
||||
// Close/reopen actions can only be set from pull requests to issues
|
||||
if refIssue.IsPull || !issue.IsPull {
|
||||
refAction = references.XRefActionNone
|
||||
}
|
||||
|
||||
// Check doer permissions; set action to None if the doer can't change the destination
|
||||
if refIssue.RepoID != ctx.OrigIssue.RepoID || ref.Action != references.XRefActionNone {
|
||||
perm, err := getUserRepoPermission(e, refIssue.Repo, ctx.Doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, references.XRefActionNone, err
|
||||
}
|
||||
if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
|
||||
return nil, nil
|
||||
return nil, references.XRefActionNone, nil
|
||||
}
|
||||
// Accept close/reopening actions only if the poster is able to close the
|
||||
// referenced issue manually at this moment. The only exception is
|
||||
// the poster of a new PR referencing an issue on the same repo: then the merger
|
||||
// should be responsible for checking whether the reference should resolve.
|
||||
if ref.Action != references.XRefActionNone &&
|
||||
ctx.Doer.ID != refIssue.PosterID &&
|
||||
!perm.CanWriteIssuesOrPulls(refIssue.IsPull) &&
|
||||
(refIssue.RepoID != ctx.OrigIssue.RepoID || ctx.OrigComment != nil) {
|
||||
refAction = references.XRefActionNone
|
||||
}
|
||||
}
|
||||
return refIssue, nil
|
||||
}
|
||||
|
||||
func (issue *Issue) neuterCrossReferences(e Engine) error {
|
||||
return neuterCrossReferences(e, issue.ID, 0)
|
||||
return refIssue, refAction, nil
|
||||
}
|
||||
|
||||
// _________ __
|
||||
@@ -191,7 +243,7 @@ func (issue *Issue) neuterCrossReferences(e Engine) error {
|
||||
// \/ \/ \/ \/ \/
|
||||
//
|
||||
|
||||
func (comment *Comment) addCrossReferences(e *xorm.Session, doer *User) error {
|
||||
func (comment *Comment) addCrossReferences(e *xorm.Session, doer *User, removeOld bool) error {
|
||||
if comment.Type != CommentTypeCode && comment.Type != CommentTypeComment {
|
||||
return nil
|
||||
}
|
||||
@@ -203,12 +255,13 @@ func (comment *Comment) addCrossReferences(e *xorm.Session, doer *User) error {
|
||||
Doer: doer,
|
||||
OrigIssue: comment.Issue,
|
||||
OrigComment: comment,
|
||||
RemoveOld: removeOld,
|
||||
}
|
||||
return comment.Issue.createCrossReferences(e, ctx, "", comment.Content)
|
||||
}
|
||||
|
||||
func (comment *Comment) neuterCrossReferences(e Engine) error {
|
||||
return neuterCrossReferences(e, 0, comment.ID)
|
||||
return neuterCrossReferences(e, comment.IssueID, comment.ID)
|
||||
}
|
||||
|
||||
// LoadRefComment loads comment that created this reference from database
|
||||
@@ -273,3 +326,40 @@ func (comment *Comment) RefIssueIdent() string {
|
||||
// FIXME: check this name for cross-repository references (#7901 if it gets merged)
|
||||
return "#" + com.ToStr(comment.RefIssue.Index)
|
||||
}
|
||||
|
||||
// __________ .__ .__ __________ __
|
||||
// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_
|
||||
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
|
||||
// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | |
|
||||
// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__|
|
||||
// \/ \/ |__| \/ \/
|
||||
|
||||
// ResolveCrossReferences will return the list of references to close/reopen by this PR
|
||||
func (pr *PullRequest) ResolveCrossReferences() ([]*Comment, error) {
|
||||
unfiltered := make([]*Comment, 0, 5)
|
||||
if err := x.
|
||||
Where("ref_repo_id = ? AND ref_issue_id = ?", pr.Issue.RepoID, pr.Issue.ID).
|
||||
In("ref_action", []references.XRefAction{references.XRefActionCloses, references.XRefActionReopens}).
|
||||
OrderBy("id").
|
||||
Find(&unfiltered); err != nil {
|
||||
return nil, fmt.Errorf("get reference: %v", err)
|
||||
}
|
||||
|
||||
refs := make([]*Comment, 0, len(unfiltered))
|
||||
for _, ref := range unfiltered {
|
||||
found := false
|
||||
for i, r := range refs {
|
||||
if r.IssueID == ref.IssueID {
|
||||
// Keep only the latest
|
||||
refs[i] = ref
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2019 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestXRef_AddCrossReferences(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Issue #1 to test against
|
||||
itarget := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||
|
||||
// PR to close issue #1
|
||||
content := fmt.Sprintf("content2, closes #%d", itarget.Index)
|
||||
pr := testCreateIssue(t, 1, 2, "title2", content, true)
|
||||
ref := AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: 0}).(*Comment)
|
||||
assert.Equal(t, CommentTypePullRef, ref.Type)
|
||||
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||
assert.Equal(t, true, ref.RefIsPull)
|
||||
assert.Equal(t, references.XRefActionCloses, ref.RefAction)
|
||||
|
||||
// Comment on PR to reopen issue #1
|
||||
content = fmt.Sprintf("content2, reopens #%d", itarget.Index)
|
||||
c := testCreateComment(t, 1, 2, pr.ID, content)
|
||||
ref = AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID}).(*Comment)
|
||||
assert.Equal(t, CommentTypeCommentRef, ref.Type)
|
||||
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||
assert.Equal(t, true, ref.RefIsPull)
|
||||
assert.Equal(t, references.XRefActionReopens, ref.RefAction)
|
||||
|
||||
// Issue mentioning issue #1
|
||||
content = fmt.Sprintf("content3, mentions #%d", itarget.Index)
|
||||
i := testCreateIssue(t, 1, 2, "title3", content, false)
|
||||
ref = AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
|
||||
assert.Equal(t, CommentTypeIssueRef, ref.Type)
|
||||
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||
assert.Equal(t, false, ref.RefIsPull)
|
||||
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||
|
||||
// Issue #4 to test against
|
||||
itarget = testCreateIssue(t, 3, 3, "title4", "content4", false)
|
||||
|
||||
// Cross-reference to issue #4 by admin
|
||||
content = fmt.Sprintf("content5, mentions user3/repo3#%d", itarget.Index)
|
||||
i = testCreateIssue(t, 2, 1, "title5", content, false)
|
||||
ref = AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
|
||||
assert.Equal(t, CommentTypeIssueRef, ref.Type)
|
||||
assert.Equal(t, i.RepoID, ref.RefRepoID)
|
||||
assert.Equal(t, false, ref.RefIsPull)
|
||||
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||
|
||||
// Cross-reference to issue #4 with no permission
|
||||
content = fmt.Sprintf("content6, mentions user3/repo3#%d", itarget.Index)
|
||||
i = testCreateIssue(t, 4, 5, "title6", content, false)
|
||||
AssertNotExistsBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0})
|
||||
}
|
||||
|
||||
func TestXRef_NeuterCrossReferences(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Issue #1 to test against
|
||||
itarget := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||
|
||||
// Issue mentioning issue #1
|
||||
title := fmt.Sprintf("title2, mentions #%d", itarget.Index)
|
||||
i := testCreateIssue(t, 1, 2, title, "content2", false)
|
||||
ref := AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
|
||||
assert.Equal(t, CommentTypeIssueRef, ref.Type)
|
||||
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||
|
||||
d := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
i.Title = "title2, no mentions"
|
||||
assert.NoError(t, i.ChangeTitle(d, title))
|
||||
|
||||
ref = AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
|
||||
assert.Equal(t, CommentTypeIssueRef, ref.Type)
|
||||
assert.Equal(t, references.XRefActionNeutered, ref.RefAction)
|
||||
}
|
||||
|
||||
func TestXRef_ResolveCrossReferences(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
d := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
i1 := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||
i2 := testCreateIssue(t, 1, 2, "title2", "content2", false)
|
||||
i3 := testCreateIssue(t, 1, 2, "title3", "content3", false)
|
||||
_, err := i3.ChangeStatus(d, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index))
|
||||
rp := AssertExistsAndLoadBean(t, &Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0}).(*Comment)
|
||||
|
||||
c1 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i2.Index))
|
||||
r1 := AssertExistsAndLoadBean(t, &Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID}).(*Comment)
|
||||
|
||||
// Must be ignored
|
||||
c2 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("mentions #%d", i2.Index))
|
||||
AssertExistsAndLoadBean(t, &Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID})
|
||||
|
||||
// Must be superseded by c4/r4
|
||||
c3 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("reopens #%d", i3.Index))
|
||||
AssertExistsAndLoadBean(t, &Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID})
|
||||
|
||||
c4 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i3.Index))
|
||||
r4 := AssertExistsAndLoadBean(t, &Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}).(*Comment)
|
||||
|
||||
refs, err := pr.ResolveCrossReferences()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, refs, 3)
|
||||
assert.Equal(t, rp.ID, refs[0].ID, "bad ref rp: %+v", refs[0])
|
||||
assert.Equal(t, r1.ID, refs[1].ID, "bad ref r1: %+v", refs[1])
|
||||
assert.Equal(t, r4.ID, refs[2].ID, "bad ref r4: %+v", refs[2])
|
||||
}
|
||||
|
||||
func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispull bool) *Issue {
|
||||
r := AssertExistsAndLoadBean(t, &Repository{ID: repo}).(*Repository)
|
||||
d := AssertExistsAndLoadBean(t, &User{ID: doer}).(*User)
|
||||
i := &Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: ispull}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
assert.NoError(t, sess.Begin())
|
||||
_, err := sess.SetExpr("`index`", "coalesce(MAX(`index`),0)+1").Where("repo_id=?", repo).Insert(i)
|
||||
assert.NoError(t, err)
|
||||
i, err = getIssueByID(sess, i.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, i.addCrossReferences(sess, d, false))
|
||||
assert.NoError(t, sess.Commit())
|
||||
return i
|
||||
}
|
||||
|
||||
func testCreatePR(t *testing.T, repo, doer int64, title, content string) *PullRequest {
|
||||
r := AssertExistsAndLoadBean(t, &Repository{ID: repo}).(*Repository)
|
||||
d := AssertExistsAndLoadBean(t, &User{ID: doer}).(*User)
|
||||
i := &Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: true}
|
||||
pr := &PullRequest{HeadRepoID: repo, BaseRepoID: repo, HeadBranch: "head", BaseBranch: "base", Status: PullRequestStatusMergeable}
|
||||
assert.NoError(t, NewPullRequest(r, i, nil, nil, pr))
|
||||
pr.Issue = i
|
||||
return pr
|
||||
}
|
||||
|
||||
func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *Comment {
|
||||
d := AssertExistsAndLoadBean(t, &User{ID: doer}).(*User)
|
||||
i := AssertExistsAndLoadBean(t, &Issue{ID: issue}).(*Issue)
|
||||
c := &Comment{Type: CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
assert.NoError(t, sess.Begin())
|
||||
_, err := sess.Insert(c)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, c.addCrossReferences(sess, d, false))
|
||||
assert.NoError(t, sess.Commit())
|
||||
return c
|
||||
}
|
||||
+6
-2
@@ -1,3 +1,7 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
@@ -159,7 +163,7 @@ func LFSObjectAccessible(user *User, oid string) (bool, error) {
|
||||
count, err := x.Count(&LFSMetaObject{Oid: oid})
|
||||
return (count > 0), err
|
||||
}
|
||||
cond := accessibleRepositoryCondition(user.ID)
|
||||
cond := accessibleRepositoryCondition(user)
|
||||
count, err := x.Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Oid: oid})
|
||||
return (count > 0), err
|
||||
}
|
||||
@@ -182,7 +186,7 @@ func LFSAutoAssociate(metas []*LFSMetaObject, user *User, repoID int64) error {
|
||||
cond := builder.NewCond()
|
||||
if !user.IsAdmin {
|
||||
cond = builder.In("`lfs_meta_object`.repository_id",
|
||||
builder.Select("`repository`.id").From("repository").Where(accessibleRepositoryCondition(user.ID)))
|
||||
builder.Select("`repository`.id").From("repository").Where(accessibleRepositoryCondition(user)))
|
||||
}
|
||||
newMetas := make([]*LFSMetaObject, 0, len(metas))
|
||||
if err := sess.Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas); err != nil {
|
||||
|
||||
+21
-4
@@ -49,7 +49,7 @@ func (l *LFSLock) AfterLoad(session *xorm.Session) {
|
||||
}
|
||||
|
||||
func cleanPath(p string) string {
|
||||
return path.Clean(p)
|
||||
return path.Clean("/" + p)[1:]
|
||||
}
|
||||
|
||||
// APIFormat convert a Release to lfs.LFSLock
|
||||
@@ -71,6 +71,8 @@ func CreateLFSLock(lock *LFSLock) (*LFSLock, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lock.Path = cleanPath(lock.Path)
|
||||
|
||||
l, err := GetLFSLock(lock.Repo, lock.Path)
|
||||
if err == nil {
|
||||
return l, ErrLFSLockAlreadyExist{lock.RepoID, lock.Path}
|
||||
@@ -110,9 +112,24 @@ func GetLFSLockByID(id int64) (*LFSLock, error) {
|
||||
}
|
||||
|
||||
// GetLFSLockByRepoID returns a list of locks of repository.
|
||||
func GetLFSLockByRepoID(repoID int64) (locks []*LFSLock, err error) {
|
||||
err = x.Where("repo_id = ?", repoID).Find(&locks)
|
||||
return
|
||||
func GetLFSLockByRepoID(repoID int64, page, pageSize int) ([]*LFSLock, error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if page >= 0 && pageSize > 0 {
|
||||
start := 0
|
||||
if page > 0 {
|
||||
start = (page - 1) * pageSize
|
||||
}
|
||||
sess.Limit(pageSize, start)
|
||||
}
|
||||
lfsLocks := make([]*LFSLock, 0, pageSize)
|
||||
return lfsLocks, sess.Find(&lfsLocks, &LFSLock{RepoID: repoID})
|
||||
}
|
||||
|
||||
// CountLFSLockByRepoID returns a count of all LFSLocks associated with a repository.
|
||||
func CountLFSLockByRepoID(repoID int64) (int64, error) {
|
||||
return x.Count(&LFSLock{RepoID: repoID})
|
||||
}
|
||||
|
||||
// DeleteLFSLockByID deletes a lock by given ID.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// ListOptions options to paginate results
|
||||
type ListOptions struct {
|
||||
PageSize int
|
||||
Page int // start from 1
|
||||
}
|
||||
|
||||
func (opts ListOptions) getPaginatedSession() *xorm.Session {
|
||||
opts.setDefaultValues()
|
||||
|
||||
return x.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||
}
|
||||
|
||||
func (opts ListOptions) setSessionPagination(sess *xorm.Session) *xorm.Session {
|
||||
opts.setDefaultValues()
|
||||
|
||||
if opts.PageSize <= 0 {
|
||||
return sess
|
||||
}
|
||||
return sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||
}
|
||||
|
||||
func (opts ListOptions) setEnginePagination(e Engine) Engine {
|
||||
opts.setDefaultValues()
|
||||
|
||||
return e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||
}
|
||||
|
||||
func (opts ListOptions) setDefaultValues() {
|
||||
if opts.PageSize <= 0 {
|
||||
opts.PageSize = setting.API.DefaultPagingNum
|
||||
}
|
||||
if opts.PageSize > setting.API.MaxResponseItems {
|
||||
opts.PageSize = setting.API.MaxResponseItems
|
||||
}
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"xorm.io/core"
|
||||
xormlog "xorm.io/xorm/log"
|
||||
)
|
||||
|
||||
// XORMLogBridge a logger bridge from Logger to xorm
|
||||
@@ -19,7 +19,7 @@ type XORMLogBridge struct {
|
||||
}
|
||||
|
||||
// NewXORMLogger inits a log bridge for xorm
|
||||
func NewXORMLogger(showSQL bool) core.ILogger {
|
||||
func NewXORMLogger(showSQL bool) xormlog.Logger {
|
||||
return &XORMLogBridge{
|
||||
showSQL: showSQL,
|
||||
logger: log.GetLogger("xorm"),
|
||||
@@ -72,22 +72,22 @@ func (l *XORMLogBridge) Warnf(format string, v ...interface{}) {
|
||||
}
|
||||
|
||||
// Level get logger level
|
||||
func (l *XORMLogBridge) Level() core.LogLevel {
|
||||
func (l *XORMLogBridge) Level() xormlog.LogLevel {
|
||||
switch l.logger.GetLevel() {
|
||||
case log.TRACE, log.DEBUG:
|
||||
return core.LOG_DEBUG
|
||||
return xormlog.LOG_DEBUG
|
||||
case log.INFO:
|
||||
return core.LOG_INFO
|
||||
return xormlog.LOG_INFO
|
||||
case log.WARN:
|
||||
return core.LOG_WARNING
|
||||
return xormlog.LOG_WARNING
|
||||
case log.ERROR, log.CRITICAL:
|
||||
return core.LOG_ERR
|
||||
return xormlog.LOG_ERR
|
||||
}
|
||||
return core.LOG_OFF
|
||||
return xormlog.LOG_OFF
|
||||
}
|
||||
|
||||
// SetLevel set the logger level
|
||||
func (l *XORMLogBridge) SetLevel(lvl core.LogLevel) {
|
||||
func (l *XORMLogBridge) SetLevel(lvl xormlog.LogLevel) {
|
||||
}
|
||||
|
||||
// ShowSQL set if record SQL
|
||||
|
||||
+145
-47
@@ -1,4 +1,5 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 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.
|
||||
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/auth/ldap"
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/unknwon/com"
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/convert"
|
||||
)
|
||||
|
||||
// LoginType represents an login type.
|
||||
@@ -38,6 +38,7 @@ const (
|
||||
LoginPAM // 4
|
||||
LoginDLDAP // 5
|
||||
LoginOAuth2 // 6
|
||||
LoginSSPI // 7
|
||||
)
|
||||
|
||||
// LoginNames contains the name of LoginType values.
|
||||
@@ -47,6 +48,7 @@ var LoginNames = map[LoginType]string{
|
||||
LoginSMTP: "SMTP",
|
||||
LoginPAM: "PAM",
|
||||
LoginOAuth2: "OAuth2",
|
||||
LoginSSPI: "SPNEGO with SSPI",
|
||||
}
|
||||
|
||||
// SecurityProtocolNames contains the name of SecurityProtocol values.
|
||||
@@ -58,10 +60,11 @@ var SecurityProtocolNames = map[ldap.SecurityProtocol]string{
|
||||
|
||||
// Ensure structs implemented interface.
|
||||
var (
|
||||
_ core.Conversion = &LDAPConfig{}
|
||||
_ core.Conversion = &SMTPConfig{}
|
||||
_ core.Conversion = &PAMConfig{}
|
||||
_ core.Conversion = &OAuth2Config{}
|
||||
_ convert.Conversion = &LDAPConfig{}
|
||||
_ convert.Conversion = &SMTPConfig{}
|
||||
_ convert.Conversion = &PAMConfig{}
|
||||
_ convert.Conversion = &OAuth2Config{}
|
||||
_ convert.Conversion = &SSPIConfig{}
|
||||
)
|
||||
|
||||
// LDAPConfig holds configuration for LDAP login source.
|
||||
@@ -139,14 +142,33 @@ func (cfg *OAuth2Config) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
// SSPIConfig holds configuration for SSPI single sign-on.
|
||||
type SSPIConfig struct {
|
||||
AutoCreateUsers bool
|
||||
AutoActivateUsers bool
|
||||
StripDomainNames bool
|
||||
SeparatorReplacement string
|
||||
DefaultLanguage string
|
||||
}
|
||||
|
||||
// FromDB fills up an SSPIConfig from serialized format.
|
||||
func (cfg *SSPIConfig) FromDB(bs []byte) error {
|
||||
return json.Unmarshal(bs, cfg)
|
||||
}
|
||||
|
||||
// ToDB exports an SSPIConfig to a serialized format.
|
||||
func (cfg *SSPIConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
// LoginSource represents an external way for authorizing users.
|
||||
type LoginSource struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type LoginType
|
||||
Name string `xorm:"UNIQUE"`
|
||||
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
Cfg core.Conversion `xorm:"TEXT"`
|
||||
Name string `xorm:"UNIQUE"`
|
||||
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
Cfg convert.Conversion `xorm:"TEXT"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
@@ -175,6 +197,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
|
||||
source.Cfg = new(PAMConfig)
|
||||
case LoginOAuth2:
|
||||
source.Cfg = new(OAuth2Config)
|
||||
case LoginSSPI:
|
||||
source.Cfg = new(SSPIConfig)
|
||||
default:
|
||||
panic("unrecognized login source type: " + com.ToStr(*val))
|
||||
}
|
||||
@@ -211,6 +235,11 @@ func (source *LoginSource) IsOAuth2() bool {
|
||||
return source.Type == LoginOAuth2
|
||||
}
|
||||
|
||||
// IsSSPI returns true of this source is of the SSPI type.
|
||||
func (source *LoginSource) IsSSPI() bool {
|
||||
return source.Type == LoginSSPI
|
||||
}
|
||||
|
||||
// HasTLS returns true of this source supports TLS.
|
||||
func (source *LoginSource) HasTLS() bool {
|
||||
return ((source.IsLDAP() || source.IsDLDAP()) &&
|
||||
@@ -263,10 +292,15 @@ func (source *LoginSource) OAuth2() *OAuth2Config {
|
||||
return source.Cfg.(*OAuth2Config)
|
||||
}
|
||||
|
||||
// SSPI returns SSPIConfig for this source, if of SSPI type.
|
||||
func (source *LoginSource) SSPI() *SSPIConfig {
|
||||
return source.Cfg.(*SSPIConfig)
|
||||
}
|
||||
|
||||
// CreateLoginSource inserts a LoginSource in the DB if not already
|
||||
// existing with the given name.
|
||||
func CreateLoginSource(source *LoginSource) error {
|
||||
has, err := x.Get(&LoginSource{Name: source.Name})
|
||||
has, err := x.Where("name=?", source.Name).Exist(new(LoginSource))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
@@ -299,6 +333,38 @@ func LoginSources() ([]*LoginSource, error) {
|
||||
return auths, x.Find(&auths)
|
||||
}
|
||||
|
||||
// LoginSourcesByType returns all sources of the specified type
|
||||
func LoginSourcesByType(loginType LoginType) ([]*LoginSource, error) {
|
||||
sources := make([]*LoginSource, 0, 1)
|
||||
if err := x.Where("type = ?", loginType).Find(&sources); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
// ActiveLoginSources returns all active sources of the specified type
|
||||
func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
|
||||
sources := make([]*LoginSource, 0, 1)
|
||||
if err := x.Where("is_actived = ? and type = ?", true, loginType).Find(&sources); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
// IsSSPIEnabled returns true if there is at least one activated login
|
||||
// source of type LoginSSPI
|
||||
func IsSSPIEnabled() bool {
|
||||
if !HasEngine {
|
||||
return false
|
||||
}
|
||||
sources, err := ActiveLoginSources(LoginSSPI)
|
||||
if err != nil {
|
||||
log.Error("ActiveLoginSources: %v", err)
|
||||
return false
|
||||
}
|
||||
return len(sources) > 0
|
||||
}
|
||||
|
||||
// GetLoginSourceByID returns login source by given ID.
|
||||
func GetLoginSourceByID(id int64) (*LoginSource, error) {
|
||||
source := new(LoginSource)
|
||||
@@ -388,13 +454,9 @@ func composeFullName(firstname, surname, username string) string {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
|
||||
)
|
||||
|
||||
// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
|
||||
// and create a local user if success when enabled.
|
||||
func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
|
||||
func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*User, error) {
|
||||
sr := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
|
||||
if sr == nil {
|
||||
// User not in LDAP, do nothing
|
||||
@@ -403,7 +465,38 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoR
|
||||
|
||||
var isAttributeSSHPublicKeySet = len(strings.TrimSpace(source.LDAP().AttributeSSHPublicKey)) > 0
|
||||
|
||||
if !autoRegister {
|
||||
// Update User admin flag if exist
|
||||
if isExist, err := IsUserExist(0, sr.Username); err != nil {
|
||||
return nil, err
|
||||
} else if isExist {
|
||||
if user == nil {
|
||||
user, err = GetUserByName(sr.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user != nil && !user.ProhibitLogin {
|
||||
cols := make([]string, 0)
|
||||
if len(source.LDAP().AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
|
||||
// Change existing admin flag only if AdminFilter option is set
|
||||
user.IsAdmin = sr.IsAdmin
|
||||
cols = append(cols, "is_admin")
|
||||
}
|
||||
if !user.IsAdmin && len(source.LDAP().RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
|
||||
// Change existing restricted flag only if RestrictedFilter option is set
|
||||
user.IsRestricted = sr.IsRestricted
|
||||
cols = append(cols, "is_restricted")
|
||||
}
|
||||
if len(cols) > 0 {
|
||||
err = UpdateUserCols(user, cols...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
if isAttributeSSHPublicKeySet && synchronizeLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
|
||||
return user, RewriteAllPublicKeys()
|
||||
}
|
||||
@@ -415,25 +508,22 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoR
|
||||
if len(sr.Username) == 0 {
|
||||
sr.Username = login
|
||||
}
|
||||
// Validate username make sure it satisfies requirement.
|
||||
if alphaDashDotPattern.MatchString(sr.Username) {
|
||||
return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", sr.Username)
|
||||
}
|
||||
|
||||
if len(sr.Mail) == 0 {
|
||||
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
|
||||
}
|
||||
|
||||
user = &User{
|
||||
LowerName: strings.ToLower(sr.Username),
|
||||
Name: sr.Username,
|
||||
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
||||
Email: sr.Mail,
|
||||
LoginType: source.Type,
|
||||
LoginSource: source.ID,
|
||||
LoginName: login,
|
||||
IsActive: true,
|
||||
IsAdmin: sr.IsAdmin,
|
||||
LowerName: strings.ToLower(sr.Username),
|
||||
Name: sr.Username,
|
||||
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
||||
Email: sr.Mail,
|
||||
LoginType: source.Type,
|
||||
LoginSource: source.ID,
|
||||
LoginName: login,
|
||||
IsActive: true,
|
||||
IsAdmin: sr.IsAdmin,
|
||||
IsRestricted: sr.IsRestricted,
|
||||
}
|
||||
|
||||
err := CreateUser(user)
|
||||
@@ -514,7 +604,7 @@ func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error {
|
||||
|
||||
// LoginViaSMTP queries if login/password is valid against the SMTP,
|
||||
// and create a local user if success when enabled.
|
||||
func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPConfig, autoRegister bool) (*User, error) {
|
||||
func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPConfig) (*User, error) {
|
||||
// Verify allowed domains.
|
||||
if len(cfg.AllowedDomains) > 0 {
|
||||
idx := strings.Index(login, "@")
|
||||
@@ -545,7 +635,7 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !autoRegister {
|
||||
if user != nil {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -577,33 +667,41 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC
|
||||
|
||||
// LoginViaPAM queries if login/password is valid against the PAM,
|
||||
// and create a local user if success when enabled.
|
||||
func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig, autoRegister bool) (*User, error) {
|
||||
if err := pam.Auth(cfg.ServiceName, login, password); err != nil {
|
||||
func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig) (*User, error) {
|
||||
pamLogin, err := pam.Auth(cfg.ServiceName, login, password)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Authentication failure") {
|
||||
return nil, ErrUserNotExist{0, login, 0}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !autoRegister {
|
||||
if user != nil {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Allow PAM sources with `@` in their name, like from Active Directory
|
||||
username := pamLogin
|
||||
idx := strings.Index(pamLogin, "@")
|
||||
if idx > -1 {
|
||||
username = pamLogin[:idx]
|
||||
}
|
||||
|
||||
user = &User{
|
||||
LowerName: strings.ToLower(login),
|
||||
Name: login,
|
||||
Email: login,
|
||||
LowerName: strings.ToLower(username),
|
||||
Name: username,
|
||||
Email: pamLogin,
|
||||
Passwd: password,
|
||||
LoginType: LoginPAM,
|
||||
LoginSource: sourceID,
|
||||
LoginName: login,
|
||||
LoginName: login, // This is what the user typed in
|
||||
IsActive: true,
|
||||
}
|
||||
return user, CreateUser(user)
|
||||
}
|
||||
|
||||
// ExternalUserLogin attempts a login using external source types.
|
||||
func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
|
||||
func ExternalUserLogin(user *User, login, password string, source *LoginSource) (*User, error) {
|
||||
if !source.IsActived {
|
||||
return nil, ErrLoginSourceNotActived
|
||||
}
|
||||
@@ -611,11 +709,11 @@ func ExternalUserLogin(user *User, login, password string, source *LoginSource,
|
||||
var err error
|
||||
switch source.Type {
|
||||
case LoginLDAP, LoginDLDAP:
|
||||
user, err = LoginViaLDAP(user, login, password, source, autoRegister)
|
||||
user, err = LoginViaLDAP(user, login, password, source)
|
||||
case LoginSMTP:
|
||||
user, err = LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig), autoRegister)
|
||||
user, err = LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig))
|
||||
case LoginPAM:
|
||||
user, err = LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister)
|
||||
user, err = LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig))
|
||||
default:
|
||||
return nil, ErrUnsupportedLoginType
|
||||
}
|
||||
@@ -695,7 +793,7 @@ func UserSignIn(username, password string) (*User, error) {
|
||||
return nil, ErrLoginSourceNotExist{user.LoginSource}
|
||||
}
|
||||
|
||||
return ExternalUserLogin(user, user.LoginName, password, &source, false)
|
||||
return ExternalUserLogin(user, user.LoginName, password, &source)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,11 +803,11 @@ func UserSignIn(username, password string) (*User, error) {
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
if source.IsOAuth2() {
|
||||
// don't try to authenticate against OAuth2 sources
|
||||
if source.IsOAuth2() || source.IsSSPI() {
|
||||
// don't try to authenticate against OAuth2 and SSPI sources here
|
||||
continue
|
||||
}
|
||||
authUser, err := ExternalUserLogin(nil, username, password, source, true)
|
||||
authUser, err := ExternalUserLogin(nil, username, password, source)
|
||||
if err == nil {
|
||||
return authUser, nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
|
||||
+66
-10
@@ -4,7 +4,12 @@
|
||||
|
||||
package models
|
||||
|
||||
import "xorm.io/xorm"
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// InsertMilestones creates milestones of repository.
|
||||
func InsertMilestones(ms ...*Milestone) (err error) {
|
||||
@@ -59,8 +64,20 @@ func insertIssue(sess *xorm.Session, issue *Issue) error {
|
||||
})
|
||||
labelIDs = append(labelIDs, label.ID)
|
||||
}
|
||||
if _, err := sess.Insert(issueLabels); err != nil {
|
||||
return err
|
||||
if len(issueLabels) > 0 {
|
||||
if _, err := sess.Insert(issueLabels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, reaction := range issue.Reactions {
|
||||
reaction.IssueID = issue.ID
|
||||
}
|
||||
|
||||
if len(issue.Reactions) > 0 {
|
||||
if _, err := sess.Insert(issue.Reactions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cols := make([]string, 0)
|
||||
@@ -130,9 +147,22 @@ func InsertIssueComments(comments []*Comment) error {
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := sess.NoAutoTime().Insert(comments); err != nil {
|
||||
return err
|
||||
for _, comment := range comments {
|
||||
if _, err := sess.NoAutoTime().Insert(comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, reaction := range comment.Reactions {
|
||||
reaction.IssueID = comment.IssueID
|
||||
reaction.CommentID = comment.ID
|
||||
}
|
||||
if len(comment.Reactions) > 0 {
|
||||
if _, err := sess.Insert(comment.Reactions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for issueID := range issueIDs {
|
||||
if _, err := sess.Exec("UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ?) WHERE id = ?", issueID, issueID); err != nil {
|
||||
return err
|
||||
@@ -173,14 +203,40 @@ func InsertReleases(rels ...*Release) error {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < len(rel.Attachments); i++ {
|
||||
rel.Attachments[i].ReleaseID = rel.ID
|
||||
}
|
||||
if len(rel.Attachments) > 0 {
|
||||
for i := range rel.Attachments {
|
||||
rel.Attachments[i].ReleaseID = rel.ID
|
||||
}
|
||||
|
||||
if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil {
|
||||
return err
|
||||
if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func migratedIssueCond(tp structs.GitServiceType) builder.Cond {
|
||||
return builder.In("issue_id",
|
||||
builder.Select("issue.id").
|
||||
From("issue").
|
||||
InnerJoin("repository", "issue.repo_id = repository.id").
|
||||
Where(builder.Eq{
|
||||
"repository.original_service_type": tp,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateReviewsMigrationsByType updates reviews' migrations information via given git service type and original id and poster id
|
||||
func UpdateReviewsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
|
||||
_, err := x.Table("review").
|
||||
Where("original_author_id = ?", originalAuthorID).
|
||||
And(migratedIssueCond(tp)).
|
||||
Update(map[string]interface{}{
|
||||
"reviewer_id": posterID,
|
||||
"original_author": "",
|
||||
"original_author_id": 0,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
+139
-729
@@ -6,28 +6,17 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
gouuid "github.com/satori/go.uuid"
|
||||
"github.com/unknwon/com"
|
||||
ini "gopkg.in/ini.v1"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
const minDBVersion = 4
|
||||
const minDBVersion = 70 // Gitea 1.5.3
|
||||
|
||||
// Migration describes on migration from lower version to high version
|
||||
type Migration interface {
|
||||
@@ -61,151 +50,31 @@ type Version struct {
|
||||
Version int64
|
||||
}
|
||||
|
||||
func emptyMigration(x *xorm.Engine) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is a sequence of migrations. Add new migrations to the bottom of the list.
|
||||
// If you want to "retire" a migration, remove it from the top of the list and
|
||||
// update minDBVersion accordingly
|
||||
var migrations = []Migration{
|
||||
// v0 -> v4: before 0.6.0 -> 0.7.33
|
||||
NewMigration("fix locale file load panic", fixLocaleFileLoadPanic), // V4 -> V5:v0.6.0
|
||||
NewMigration("trim action compare URL prefix", trimCommitActionAppURLPrefix), // V5 -> V6:v0.6.3
|
||||
NewMigration("generate issue-label from issue", issueToIssueLabel), // V6 -> V7:v0.6.4
|
||||
NewMigration("refactor attachment table", attachmentRefactor), // V7 -> V8:v0.6.4
|
||||
NewMigration("rename pull request fields", renamePullRequestFields), // V8 -> V9:v0.6.16
|
||||
NewMigration("clean up migrate repo info", cleanUpMigrateRepoInfo), // V9 -> V10:v0.6.20
|
||||
NewMigration("generate rands and salt for organizations", generateOrgRandsAndSalt), // V10 -> V11:v0.8.5
|
||||
NewMigration("convert date to unix timestamp", convertDateToUnix), // V11 -> V12:v0.9.2
|
||||
NewMigration("convert LDAP UseSSL option to SecurityProtocol", ldapUseSSLToSecurityProtocol), // V12 -> V13:v0.9.37
|
||||
|
||||
// v13 -> v14:v0.9.87
|
||||
NewMigration("set comment updated with created", setCommentUpdatedWithCreated),
|
||||
// v14 -> v15
|
||||
NewMigration("create user column diff view style", createUserColumnDiffViewStyle),
|
||||
// v15 -> v16
|
||||
NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn),
|
||||
// V16 -> v17
|
||||
NewMigration("create repo unit table and add units for all repos", addUnitsToTables),
|
||||
// v17 -> v18
|
||||
NewMigration("set protect branches updated with created", setProtectedBranchUpdatedWithCreated),
|
||||
// v18 -> v19
|
||||
NewMigration("add external login user", addExternalLoginUser),
|
||||
// v19 -> v20
|
||||
NewMigration("generate and migrate Git hooks", generateAndMigrateGitHooks),
|
||||
// v20 -> v21
|
||||
NewMigration("use new avatar path name for security reason", useNewNameAvatars),
|
||||
// v21 -> v22
|
||||
NewMigration("rewrite authorized_keys file via new format", useNewPublickeyFormat),
|
||||
// v22 -> v23
|
||||
NewMigration("generate and migrate wiki Git hooks", generateAndMigrateWikiGitHooks),
|
||||
// v23 -> v24
|
||||
NewMigration("add user openid table", addUserOpenID),
|
||||
// v24 -> v25
|
||||
NewMigration("change the key_id and primary_key_id type", changeGPGKeysColumns),
|
||||
// v25 -> v26
|
||||
NewMigration("add show field in user openid table", addUserOpenIDShow),
|
||||
// v26 -> v27
|
||||
NewMigration("generate and migrate repo and wiki Git hooks", generateAndMigrateGitHookChains),
|
||||
// v27 -> v28
|
||||
NewMigration("change mirror interval from hours to time.Duration", convertIntervalToDuration),
|
||||
// v28 -> v29
|
||||
NewMigration("add field for repo size", addRepoSize),
|
||||
// v29 -> v30
|
||||
NewMigration("add commit status table", addCommitStatus),
|
||||
// v30 -> 31
|
||||
NewMigration("add primary key to external login user", addExternalLoginUserPK),
|
||||
// v31 -> 32
|
||||
NewMigration("add field for login source synchronization", addLoginSourceSyncEnabledColumn),
|
||||
// v32 -> v33
|
||||
NewMigration("add units for team", addUnitsToRepoTeam),
|
||||
// v33 -> v34
|
||||
NewMigration("remove columns from action", removeActionColumns),
|
||||
// v34 -> v35
|
||||
NewMigration("give all units to owner teams", giveAllUnitsToOwnerTeams),
|
||||
// v35 -> v36
|
||||
NewMigration("adds comment to an action", addCommentIDToAction),
|
||||
// v36 -> v37
|
||||
NewMigration("regenerate git hooks", regenerateGitHooks36),
|
||||
// v37 -> v38
|
||||
NewMigration("unescape user full names", unescapeUserFullNames),
|
||||
// v38 -> v39
|
||||
NewMigration("remove commits and settings unit types", removeCommitsUnitType),
|
||||
// v39 -> v40
|
||||
NewMigration("add tags to releases and sync existing repositories", releaseAddColumnIsTagAndSyncTags),
|
||||
// v40 -> v41
|
||||
NewMigration("fix protected branch can push value to false", fixProtectedBranchCanPushValue),
|
||||
// v41 -> v42
|
||||
NewMigration("remove duplicate unit types", removeDuplicateUnitTypes),
|
||||
// v42 -> v43
|
||||
NewMigration("empty step", emptyMigration),
|
||||
// v43 -> v44
|
||||
NewMigration("empty step", emptyMigration),
|
||||
// v44 -> v45
|
||||
NewMigration("empty step", emptyMigration),
|
||||
// v45 -> v46
|
||||
NewMigration("remove index column from repo_unit table", removeIndexColumnFromRepoUnitTable),
|
||||
// v46 -> v47
|
||||
NewMigration("remove organization watch repositories", removeOrganizationWatchRepo),
|
||||
// v47 -> v48
|
||||
NewMigration("add deleted branches", addDeletedBranch),
|
||||
// v48 -> v49
|
||||
NewMigration("add repo indexer status", addRepoIndexerStatus),
|
||||
// v49 -> v50
|
||||
NewMigration("adds time tracking and stopwatches", addTimetracking),
|
||||
// v50 -> v51
|
||||
NewMigration("migrate protected branch struct", migrateProtectedBranchStruct),
|
||||
// v51 -> v52
|
||||
NewMigration("add default value to user prohibit_login", addDefaultValueToUserProhibitLogin),
|
||||
// v52 -> v53
|
||||
NewMigration("add lfs lock table", addLFSLock),
|
||||
// v53 -> v54
|
||||
NewMigration("add reactions", addReactions),
|
||||
// v54 -> v55
|
||||
NewMigration("add pull request options", addPullRequestOptions),
|
||||
// v55 -> v56
|
||||
NewMigration("add writable deploy keys", addModeToDeploKeys),
|
||||
// v56 -> v57
|
||||
NewMigration("remove is_owner, num_teams columns from org_user", removeIsOwnerColumnFromOrgUser),
|
||||
// v57 -> v58
|
||||
NewMigration("add closed_unix column for issues", addIssueClosedTime),
|
||||
// v58 -> v59
|
||||
NewMigration("add label descriptions", addLabelsDescriptions),
|
||||
// v59 -> v60
|
||||
NewMigration("add merge whitelist for protected branches", addProtectedBranchMergeWhitelist),
|
||||
// v60 -> v61
|
||||
NewMigration("add is_fsck_enabled column for repos", addFsckEnabledToRepo),
|
||||
// v61 -> v62
|
||||
NewMigration("add size column for attachments", addSizeToAttachment),
|
||||
// v62 -> v63
|
||||
NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP),
|
||||
// v63 -> v64
|
||||
NewMigration("add language column for user setting", addLanguageSetting),
|
||||
// v64 -> v65
|
||||
NewMigration("add multiple assignees", addMultipleAssignees),
|
||||
// v65 -> v66
|
||||
NewMigration("add u2f", addU2FReg),
|
||||
// v66 -> v67
|
||||
NewMigration("add login source id column for public_key table", addLoginSourceIDToPublicKeyTable),
|
||||
// v67 -> v68
|
||||
NewMigration("remove stale watches", removeStaleWatches),
|
||||
// v68 -> V69
|
||||
NewMigration("Reformat and remove incorrect topics", reformatAndRemoveIncorrectTopics),
|
||||
// v69 -> v70
|
||||
NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
|
||||
// Gitea 1.5.3 ends at v70
|
||||
|
||||
// v70 -> v71
|
||||
NewMigration("add issue_dependencies", addIssueDependencies),
|
||||
// v71 -> v72
|
||||
NewMigration("protect each scratch token", addScratchHash),
|
||||
// v72 -> v73
|
||||
NewMigration("add review", addReview),
|
||||
|
||||
// Gitea 1.6.4 ends at v73
|
||||
|
||||
// v73 -> v74
|
||||
NewMigration("add must_change_password column for users table", addMustChangePassword),
|
||||
// v74 -> v75
|
||||
NewMigration("add approval whitelists to protected branches", addApprovalWhitelistsToProtectedBranches),
|
||||
// v75 -> v76
|
||||
NewMigration("clear nonused data which not deleted when user was deleted", clearNonusedData),
|
||||
|
||||
// Gitea 1.7.6 ends at v76
|
||||
|
||||
// v76 -> v77
|
||||
NewMigration("add pull request rebase with merge commit", addPullRequestRebaseWithMerge),
|
||||
// v77 -> v78
|
||||
@@ -218,6 +87,9 @@ var migrations = []Migration{
|
||||
NewMigration("add is locked to issues", addIsLockedToIssues),
|
||||
// v81 -> v82
|
||||
NewMigration("update U2F counter type", changeU2FCounterType),
|
||||
|
||||
// Gitea 1.8.3 ends at v82
|
||||
|
||||
// v82 -> v83
|
||||
NewMigration("hot fix for wrong release sha1 on release table", fixReleaseSha1OnReleaseTable),
|
||||
// v83 -> v84
|
||||
@@ -230,6 +102,9 @@ var migrations = []Migration{
|
||||
NewMigration("add http method to webhook", addHTTPMethodToWebhook),
|
||||
// v87 -> v88
|
||||
NewMigration("add avatar field to repository", addAvatarFieldToRepository),
|
||||
|
||||
// Gitea 1.9.6 ends at v88
|
||||
|
||||
// v88 -> v89
|
||||
NewMigration("add commit status context field to commit_status", addCommitStatusContext),
|
||||
// v89 -> v90
|
||||
@@ -252,6 +127,9 @@ var migrations = []Migration{
|
||||
NewMigration("add repo_admin_change_team_access to user", addRepoAdminChangeTeamAccessColumnForUser),
|
||||
// v98 -> v99
|
||||
NewMigration("add original author name and id on migrated release", addOriginalAuthorOnMigratedReleases),
|
||||
|
||||
// Gitea 1.10.3 ends at v99
|
||||
|
||||
// v99 -> v100
|
||||
NewMigration("add task table and status column for repository table", addTaskTable),
|
||||
// v100 -> v101
|
||||
@@ -266,6 +144,126 @@ var migrations = []Migration{
|
||||
NewMigration("remove unnecessary columns from label", removeLabelUneededCols),
|
||||
// v105 -> v106
|
||||
NewMigration("add includes_all_repositories to teams", addTeamIncludesAllRepositories),
|
||||
// v106 -> v107
|
||||
NewMigration("add column `mode` to table watch", addModeColumnToWatch),
|
||||
// v107 -> v108
|
||||
NewMigration("Add template options to repository", addTemplateToRepo),
|
||||
// v108 -> v109
|
||||
NewMigration("Add comment_id on table notification", addCommentIDOnNotification),
|
||||
// v109 -> v110
|
||||
NewMigration("add can_create_org_repo to team", addCanCreateOrgRepoColumnForTeam),
|
||||
// v110 -> v111
|
||||
NewMigration("change review content type to text", changeReviewContentToText),
|
||||
// v111 -> v112
|
||||
NewMigration("update branch protection for can push and whitelist enable", addBranchProtectionCanPushAndEnableWhitelist),
|
||||
// v112 -> v113
|
||||
NewMigration("remove release attachments which repository deleted", removeAttachmentMissedRepo),
|
||||
// v113 -> v114
|
||||
NewMigration("new feature: change target branch of pull requests", featureChangeTargetBranch),
|
||||
// v114 -> v115
|
||||
NewMigration("Remove authentication credentials from stored URL", sanitizeOriginalURL),
|
||||
// v115 -> v116
|
||||
NewMigration("add user_id prefix to existing user avatar name", renameExistingUserAvatarName),
|
||||
// v116 -> v117
|
||||
NewMigration("Extend TrackedTimes", extendTrackedTimes),
|
||||
// v117 -> v118
|
||||
NewMigration("Add block on rejected reviews branch protection", addBlockOnRejectedReviews),
|
||||
// v118 -> v119
|
||||
NewMigration("Add commit id and stale to reviews", addReviewCommitAndStale),
|
||||
// v119 -> v120
|
||||
NewMigration("Fix migrated repositories' git service type", fixMigratedRepositoryServiceType),
|
||||
// v120 -> v121
|
||||
NewMigration("Add owner_name on table repository", addOwnerNameOnRepository),
|
||||
// v121 -> v122
|
||||
NewMigration("add is_restricted column for users table", addIsRestricted),
|
||||
// v122 -> v123
|
||||
NewMigration("Add Require Signed Commits to ProtectedBranch", addRequireSignedCommits),
|
||||
// v123 -> v124
|
||||
NewMigration("Add original informations for reactions", addReactionOriginals),
|
||||
// v124 -> v125
|
||||
NewMigration("Add columns to user and repository", addUserRepoMissingColumns),
|
||||
// v125 -> v126
|
||||
NewMigration("Add some columns on review for migration", addReviewMigrateInfo),
|
||||
// v126 -> v127
|
||||
NewMigration("Fix topic repository count", fixTopicRepositoryCount),
|
||||
// v127 -> v128
|
||||
NewMigration("add repository code language statistics", addLanguageStats),
|
||||
// v128 -> v129
|
||||
NewMigration("fix merge base for pull requests", fixMergeBase),
|
||||
// v129 -> v130
|
||||
NewMigration("remove dependencies from deleted repositories", purgeUnusedDependencies),
|
||||
// v130 -> v131
|
||||
NewMigration("Expand webhooks for more granularity", expandWebhooks),
|
||||
// v131 -> v132
|
||||
NewMigration("Add IsSystemWebhook column to webhooks table", addSystemWebhookColumn),
|
||||
// v132 -> v133
|
||||
NewMigration("Add Branch Protection Protected Files Column", addBranchProtectionProtectedFilesColumn),
|
||||
// v133 -> v134
|
||||
NewMigration("Add EmailHash Table", addEmailHashTable),
|
||||
// v134 -> v135
|
||||
NewMigration("Refix merge base for merged pull requests", refixMergeBase),
|
||||
// v135 -> v136
|
||||
NewMigration("Add OrgID column to Labels table", addOrgIDLabelColumn),
|
||||
// v136 -> v137
|
||||
NewMigration("Add CommitsAhead and CommitsBehind Column to PullRequest Table", addCommitDivergenceToPulls),
|
||||
// v137 -> v138
|
||||
NewMigration("Add Branch Protection Block Outdated Branch", addBlockOnOutdatedBranch),
|
||||
// v138 -> v139
|
||||
NewMigration("Add ResolveDoerID to Comment table", addResolveDoerIDCommentColumn),
|
||||
// v139 -> v140
|
||||
NewMigration("prepend refs/heads/ to issue refs", prependRefsHeadsToIssueRefs),
|
||||
// v140 -> v141
|
||||
NewMigration("Save detected language file size to database instead of percent", fixLanguageStatsToSaveSize),
|
||||
// v141 -> v142
|
||||
NewMigration("Add KeepActivityPrivate to User table", addKeepActivityPrivateUserColumn),
|
||||
// v142 -> v143
|
||||
NewMigration("Ensure Repository.IsArchived is not null", setIsArchivedToFalse),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
func GetCurrentDBVersion(x *xorm.Engine) (int64, error) {
|
||||
if err := x.Sync(new(Version)); err != nil {
|
||||
return -1, fmt.Errorf("sync: %v", err)
|
||||
}
|
||||
|
||||
currentVersion := &Version{ID: 1}
|
||||
has, err := x.Get(currentVersion)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("get: %v", err)
|
||||
}
|
||||
if !has {
|
||||
return -1, nil
|
||||
}
|
||||
return currentVersion.Version, nil
|
||||
}
|
||||
|
||||
// ExpectedVersion returns the expected db version
|
||||
func ExpectedVersion() int64 {
|
||||
return int64(minDBVersion + len(migrations))
|
||||
}
|
||||
|
||||
// EnsureUpToDate will check if the db is at the correct version
|
||||
func EnsureUpToDate(x *xorm.Engine) error {
|
||||
currentDB, err := GetCurrentDBVersion(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if currentDB < 0 {
|
||||
return fmt.Errorf("Database has not been initialised")
|
||||
}
|
||||
|
||||
if minDBVersion > currentDB {
|
||||
return fmt.Errorf("DB version %d (<= %d) is too old for auto-migration. Upgrade to Gitea 1.6.4 first then upgrade to this version", currentDB, minDBVersion)
|
||||
}
|
||||
|
||||
expected := ExpectedVersion()
|
||||
|
||||
if currentDB != expected {
|
||||
return fmt.Errorf(`Current database version %d is not equal to the expected version %d. Please run "gitea [--config /path/to/app.ini] migrate" to update the database version`, currentDB, expected)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate database to current version
|
||||
@@ -292,7 +290,7 @@ func Migrate(x *xorm.Engine) error {
|
||||
v := currentVersion.Version
|
||||
if minDBVersion > v {
|
||||
log.Fatal(`Gitea no longer supports auto-migration from your previously installed version.
|
||||
Please try to upgrade to a lower version (>= v0.6.0) first, then upgrade to current version.`)
|
||||
Please try upgrading to a lower version first (suggested v1.6.4), then upgrade to this version.`)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -470,591 +468,3 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fixLocaleFileLoadPanic(_ *xorm.Engine) error {
|
||||
cfg, err := ini.Load(setting.CustomConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load custom config: %v", err)
|
||||
}
|
||||
|
||||
cfg.DeleteSection("i18n")
|
||||
if err = cfg.SaveTo(setting.CustomConf); err != nil {
|
||||
return fmt.Errorf("save custom config: %v", err)
|
||||
}
|
||||
|
||||
setting.Langs = strings.Split(strings.Replace(strings.Join(setting.Langs, ","), "fr-CA", "fr-FR", 1), ",")
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimCommitActionAppURLPrefix(x *xorm.Engine) error {
|
||||
type PushCommit struct {
|
||||
Sha1 string
|
||||
Message string
|
||||
AuthorEmail string
|
||||
AuthorName string
|
||||
}
|
||||
|
||||
type PushCommits struct {
|
||||
Len int
|
||||
Commits []*PushCommit
|
||||
CompareURL string `json:"CompareUrl"`
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Content string `xorm:"TEXT"`
|
||||
}
|
||||
|
||||
results, err := x.Query("SELECT `id`,`content` FROM `action` WHERE `op_type`=?", 5)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select commit actions: %v", err)
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pushCommits *PushCommits
|
||||
for _, action := range results {
|
||||
actID := com.StrTo(string(action["id"])).MustInt64()
|
||||
if actID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pushCommits = new(PushCommits)
|
||||
if err = json.Unmarshal(action["content"], pushCommits); err != nil {
|
||||
return fmt.Errorf("unmarshal action content[%d]: %v", actID, err)
|
||||
}
|
||||
|
||||
infos := strings.Split(pushCommits.CompareURL, "/")
|
||||
if len(infos) <= 4 {
|
||||
continue
|
||||
}
|
||||
pushCommits.CompareURL = strings.Join(infos[len(infos)-4:], "/")
|
||||
|
||||
p, err := json.Marshal(pushCommits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal action content[%d]: %v", actID, err)
|
||||
}
|
||||
|
||||
if _, err = sess.ID(actID).Update(&Action{
|
||||
Content: string(p),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("update action[%d]: %v", actID, err)
|
||||
}
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func issueToIssueLabel(x *xorm.Engine) error {
|
||||
type IssueLabel struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"UNIQUE(s)"`
|
||||
LabelID int64 `xorm:"UNIQUE(s)"`
|
||||
}
|
||||
|
||||
issueLabels := make([]*IssueLabel, 0, 50)
|
||||
results, err := x.Query("SELECT `id`,`label_ids` FROM `issue`")
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such column") ||
|
||||
strings.Contains(err.Error(), "Unknown column") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("select issues: %v", err)
|
||||
}
|
||||
for _, issue := range results {
|
||||
issueID := com.StrTo(issue["id"]).MustInt64()
|
||||
|
||||
// Just in case legacy code can have duplicated IDs for same label.
|
||||
mark := make(map[int64]bool)
|
||||
for _, idStr := range strings.Split(string(issue["label_ids"]), "|") {
|
||||
labelID := com.StrTo(strings.TrimPrefix(idStr, "$")).MustInt64()
|
||||
if labelID == 0 || mark[labelID] {
|
||||
continue
|
||||
}
|
||||
|
||||
mark[labelID] = true
|
||||
issueLabels = append(issueLabels, &IssueLabel{
|
||||
IssueID: issueID,
|
||||
LabelID: labelID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = sess.Sync2(new(IssueLabel)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
} else if _, err = sess.Insert(issueLabels); err != nil {
|
||||
return fmt.Errorf("insert issue-labels: %v", err)
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func attachmentRefactor(x *xorm.Engine) error {
|
||||
type Attachment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UUID string `xorm:"uuid INDEX"`
|
||||
|
||||
// For rename purpose.
|
||||
Path string `xorm:"-"`
|
||||
NewPath string `xorm:"-"`
|
||||
}
|
||||
|
||||
results, err := x.Query("SELECT * FROM `attachment`")
|
||||
if err != nil {
|
||||
return fmt.Errorf("select attachments: %v", err)
|
||||
}
|
||||
|
||||
attachments := make([]*Attachment, 0, len(results))
|
||||
for _, attach := range results {
|
||||
if !com.IsExist(string(attach["path"])) {
|
||||
// If the attachment is already missing, there is no point to update it.
|
||||
continue
|
||||
}
|
||||
attachments = append(attachments, &Attachment{
|
||||
ID: com.StrTo(attach["id"]).MustInt64(),
|
||||
UUID: gouuid.NewV4().String(),
|
||||
Path: string(attach["path"]),
|
||||
})
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = sess.Sync2(new(Attachment)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
|
||||
// Note: Roll back for rename can be a dead loop,
|
||||
// so produces a backup file.
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("# old path -> new path\n")
|
||||
|
||||
// Update database first because this is where error happens the most often.
|
||||
for _, attach := range attachments {
|
||||
if _, err = sess.ID(attach.ID).Update(attach); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attach.NewPath = path.Join(setting.AttachmentPath, attach.UUID[0:1], attach.UUID[1:2], attach.UUID)
|
||||
buf.WriteString(attach.Path)
|
||||
buf.WriteString("\t")
|
||||
buf.WriteString(attach.NewPath)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
// Then rename attachments.
|
||||
isSucceed := true
|
||||
defer func() {
|
||||
if isSucceed {
|
||||
return
|
||||
}
|
||||
|
||||
dumpPath := path.Join(setting.LogRootPath, "attachment_path.dump")
|
||||
ioutil.WriteFile(dumpPath, buf.Bytes(), 0666)
|
||||
log.Info("Failed to rename some attachments, old and new paths are saved into: %s", dumpPath)
|
||||
}()
|
||||
for _, attach := range attachments {
|
||||
if err = os.MkdirAll(path.Dir(attach.NewPath), os.ModePerm); err != nil {
|
||||
isSucceed = false
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Rename(attach.Path, attach.NewPath); err != nil {
|
||||
isSucceed = false
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func renamePullRequestFields(x *xorm.Engine) (err error) {
|
||||
type PullRequest struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
PullID int64 `xorm:"INDEX"`
|
||||
PullIndex int64
|
||||
HeadBarcnh string
|
||||
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Index int64
|
||||
HeadBranch string
|
||||
}
|
||||
|
||||
if err = x.Sync(new(PullRequest)); err != nil {
|
||||
return fmt.Errorf("sync: %v", err)
|
||||
}
|
||||
|
||||
results, err := x.Query("SELECT `id`,`pull_id`,`pull_index`,`head_barcnh` FROM `pull_request`")
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such column") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("select pull requests: %v", err)
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pull *PullRequest
|
||||
for _, pr := range results {
|
||||
pull = &PullRequest{
|
||||
ID: com.StrTo(pr["id"]).MustInt64(),
|
||||
IssueID: com.StrTo(pr["pull_id"]).MustInt64(),
|
||||
Index: com.StrTo(pr["pull_index"]).MustInt64(),
|
||||
HeadBranch: string(pr["head_barcnh"]),
|
||||
}
|
||||
if pull.Index == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err = sess.ID(pull.ID).Update(pull); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func cleanUpMigrateRepoInfo(x *xorm.Engine) (err error) {
|
||||
type (
|
||||
User struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
LowerName string
|
||||
}
|
||||
Repository struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64
|
||||
LowerName string
|
||||
}
|
||||
)
|
||||
|
||||
repos := make([]*Repository, 0, 25)
|
||||
if err = x.Where("is_mirror=?", false).Find(&repos); err != nil {
|
||||
return fmt.Errorf("select all non-mirror repositories: %v", err)
|
||||
}
|
||||
var user *User
|
||||
for _, repo := range repos {
|
||||
user = &User{ID: repo.OwnerID}
|
||||
has, err := x.Get(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get owner of repository[%d - %d]: %v", repo.ID, repo.OwnerID, err)
|
||||
} else if !has {
|
||||
continue
|
||||
}
|
||||
|
||||
configPath := filepath.Join(setting.RepoRootPath, user.LowerName, repo.LowerName+".git/config")
|
||||
|
||||
// In case repository file is somehow missing.
|
||||
if !com.IsFile(configPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg, err := ini.Load(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open config file: %v", err)
|
||||
}
|
||||
cfg.DeleteSection("remote \"origin\"")
|
||||
if err = cfg.SaveToIndent(configPath, "\t"); err != nil {
|
||||
return fmt.Errorf("save config file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateOrgRandsAndSalt(x *xorm.Engine) (err error) {
|
||||
type User struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Rands string `xorm:"VARCHAR(10)"`
|
||||
Salt string `xorm:"VARCHAR(10)"`
|
||||
}
|
||||
|
||||
orgs := make([]*User, 0, 10)
|
||||
if err = x.Where("type=1").And("rands=''").Find(&orgs); err != nil {
|
||||
return fmt.Errorf("select all organizations: %v", err)
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
if org.Rands, err = generate.GetRandomString(10); err != nil {
|
||||
return err
|
||||
}
|
||||
if org.Salt, err = generate.GetRandomString(10); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = sess.ID(org.ID).Update(org); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// TAction defines the struct for migrating table action
|
||||
type TAction struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TAction) TableName() string { return "action" }
|
||||
|
||||
// TNotice defines the struct for migrating table notice
|
||||
type TNotice struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TNotice) TableName() string { return "notice" }
|
||||
|
||||
// TComment defines the struct for migrating table comment
|
||||
type TComment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TComment) TableName() string { return "comment" }
|
||||
|
||||
// TIssue defines the struct for migrating table issue
|
||||
type TIssue struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
DeadlineUnix int64
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TIssue) TableName() string { return "issue" }
|
||||
|
||||
// TMilestone defines the struct for migrating table milestone
|
||||
type TMilestone struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
DeadlineUnix int64
|
||||
ClosedDateUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TMilestone) TableName() string { return "milestone" }
|
||||
|
||||
// TAttachment defines the struct for migrating table attachment
|
||||
type TAttachment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TAttachment) TableName() string { return "attachment" }
|
||||
|
||||
// TLoginSource defines the struct for migrating table login_source
|
||||
type TLoginSource struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TLoginSource) TableName() string { return "login_source" }
|
||||
|
||||
// TPull defines the struct for migrating table pull_request
|
||||
type TPull struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
MergedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TPull) TableName() string { return "pull_request" }
|
||||
|
||||
// TRelease defines the struct for migrating table release
|
||||
type TRelease struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TRelease) TableName() string { return "release" }
|
||||
|
||||
// TRepo defines the struct for migrating table repository
|
||||
type TRepo struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TRepo) TableName() string { return "repository" }
|
||||
|
||||
// TMirror defines the struct for migrating table mirror
|
||||
type TMirror struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UpdatedUnix int64
|
||||
NextUpdateUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TMirror) TableName() string { return "mirror" }
|
||||
|
||||
// TPublicKey defines the struct for migrating table public_key
|
||||
type TPublicKey struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TPublicKey) TableName() string { return "public_key" }
|
||||
|
||||
// TDeployKey defines the struct for migrating table deploy_key
|
||||
type TDeployKey struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TDeployKey) TableName() string { return "deploy_key" }
|
||||
|
||||
// TAccessToken defines the struct for migrating table access_token
|
||||
type TAccessToken struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TAccessToken) TableName() string { return "access_token" }
|
||||
|
||||
// TUser defines the struct for migrating table user
|
||||
type TUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TUser) TableName() string { return "user" }
|
||||
|
||||
// TWebhook defines the struct for migrating table webhook
|
||||
type TWebhook struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (t *TWebhook) TableName() string { return "webhook" }
|
||||
|
||||
func convertDateToUnix(x *xorm.Engine) (err error) {
|
||||
log.Info("This migration could take up to minutes, please be patient.")
|
||||
type Bean struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Merged time.Time
|
||||
Deadline time.Time
|
||||
ClosedDate time.Time
|
||||
NextUpdate time.Time
|
||||
}
|
||||
|
||||
var tables = []struct {
|
||||
name string
|
||||
cols []string
|
||||
bean interface{}
|
||||
}{
|
||||
{"action", []string{"created"}, new(TAction)},
|
||||
{"notice", []string{"created"}, new(TNotice)},
|
||||
{"comment", []string{"created"}, new(TComment)},
|
||||
{"issue", []string{"deadline", "created", "updated"}, new(TIssue)},
|
||||
{"milestone", []string{"deadline", "closed_date"}, new(TMilestone)},
|
||||
{"attachment", []string{"created"}, new(TAttachment)},
|
||||
{"login_source", []string{"created", "updated"}, new(TLoginSource)},
|
||||
{"pull_request", []string{"merged"}, new(TPull)},
|
||||
{"release", []string{"created"}, new(TRelease)},
|
||||
{"repository", []string{"created", "updated"}, new(TRepo)},
|
||||
{"mirror", []string{"updated", "next_update"}, new(TMirror)},
|
||||
{"public_key", []string{"created", "updated"}, new(TPublicKey)},
|
||||
{"deploy_key", []string{"created", "updated"}, new(TDeployKey)},
|
||||
{"access_token", []string{"created", "updated"}, new(TAccessToken)},
|
||||
{"user", []string{"created", "updated"}, new(TUser)},
|
||||
{"webhook", []string{"created", "updated"}, new(TWebhook)},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
log.Info("Converting table: %s", table.name)
|
||||
if err = x.Sync2(table.bean); err != nil {
|
||||
return fmt.Errorf("Sync [table: %s]: %v", table.name, err)
|
||||
}
|
||||
|
||||
offset := 0
|
||||
for {
|
||||
beans := make([]*Bean, 0, 100)
|
||||
if err = x.Table(table.name).Asc("id").Limit(100, offset).Find(&beans); err != nil {
|
||||
return fmt.Errorf("select beans [table: %s, offset: %d]: %v", table.name, offset, err)
|
||||
}
|
||||
log.Trace("Table [%s]: offset: %d, beans: %d", table.name, offset, len(beans))
|
||||
if len(beans) == 0 {
|
||||
break
|
||||
}
|
||||
offset += 100
|
||||
|
||||
baseSQL := "UPDATE `" + table.name + "` SET "
|
||||
for _, bean := range beans {
|
||||
valSQLs := make([]string, 0, len(table.cols))
|
||||
for _, col := range table.cols {
|
||||
fieldSQL := ""
|
||||
fieldSQL += col + "_unix = "
|
||||
|
||||
switch col {
|
||||
case "deadline":
|
||||
if bean.Deadline.IsZero() {
|
||||
continue
|
||||
}
|
||||
fieldSQL += com.ToStr(bean.Deadline.Unix())
|
||||
case "created":
|
||||
fieldSQL += com.ToStr(bean.Created.Unix())
|
||||
case "updated":
|
||||
fieldSQL += com.ToStr(bean.Updated.Unix())
|
||||
case "closed_date":
|
||||
fieldSQL += com.ToStr(bean.ClosedDate.Unix())
|
||||
case "merged":
|
||||
fieldSQL += com.ToStr(bean.Merged.Unix())
|
||||
case "next_update":
|
||||
fieldSQL += com.ToStr(bean.NextUpdate.Unix())
|
||||
}
|
||||
|
||||
valSQLs = append(valSQLs, fieldSQL)
|
||||
}
|
||||
|
||||
if len(valSQLs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err = x.Exec(baseSQL + strings.Join(valSQLs, ",") + " WHERE id = " + com.ToStr(bean.ID)); err != nil {
|
||||
return fmt.Errorf("update bean [table: %s, id: %d]: %v", table.name, bean.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019 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"
|
||||
)
|
||||
|
||||
// RepoWatchMode specifies what kind of watch the user has on a repository
|
||||
type RepoWatchMode int8
|
||||
|
||||
// Watch is connection request for receiving repository notification.
|
||||
type Watch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Mode RepoWatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
|
||||
}
|
||||
|
||||
func addModeColumnToWatch(x *xorm.Engine) (err error) {
|
||||
if err = x.Sync2(new(Watch)); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = x.Exec("UPDATE `watch` SET `mode` = 1")
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2019 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 addTemplateToRepo(x *xorm.Engine) error {
|
||||
|
||||
type Repository struct {
|
||||
IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
TemplateID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(Repository))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2019 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 addCommentIDOnNotification(x *xorm.Engine) error {
|
||||
type Notification struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CommentID int64
|
||||
}
|
||||
|
||||
return x.Sync2(new(Notification))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2019 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 addCanCreateOrgRepoColumnForTeam(x *xorm.Engine) error {
|
||||
type Team struct {
|
||||
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(Team))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2019 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"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
func changeReviewContentToText(x *xorm.Engine) error {
|
||||
switch x.Dialect().URI().DBType {
|
||||
case schemas.MYSQL:
|
||||
_, err := x.Exec("ALTER TABLE review MODIFY COLUMN content TEXT")
|
||||
return err
|
||||
case schemas.ORACLE:
|
||||
_, err := x.Exec("ALTER TABLE review MODIFY content TEXT")
|
||||
return err
|
||||
case schemas.MSSQL:
|
||||
_, err := x.Exec("ALTER TABLE review ALTER COLUMN content TEXT")
|
||||
return err
|
||||
case schemas.POSTGRES:
|
||||
_, err := x.Exec("ALTER TABLE review ALTER COLUMN content TYPE TEXT")
|
||||
return err
|
||||
default:
|
||||
// SQLite doesn't support ALTER COLUMN, and it seem to already make String to _TEXT_ default so no migration needed
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2019 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 (
|
||||
"code.gitea.io/gitea/models"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
|
||||
|
||||
type ProtectedBranch struct {
|
||||
CanPush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
type Review struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Official bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Sync2(new(ProtectedBranch)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sess.Sync2(new(Review)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("UPDATE `protected_branch` SET `enable_whitelist` = ? WHERE enable_whitelist IS NULL", false); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := sess.Exec("UPDATE `protected_branch` SET `can_push` = `enable_whitelist`"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := sess.Exec("UPDATE `protected_branch` SET `enable_approvals_whitelist` = ? WHERE `required_approvals` > ?", true, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pageSize int64 = 20
|
||||
qresult, err := sess.QueryInterface("SELECT max(id) as max_id FROM issue")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var totalIssues int64
|
||||
totalIssues, ok := qresult[0]["max_id"].(int64)
|
||||
if !ok {
|
||||
// If there are no issues at all we ignore it
|
||||
return nil
|
||||
}
|
||||
totalPages := totalIssues / pageSize
|
||||
|
||||
// Find latest review of each user in each pull request, and set official field if appropriate
|
||||
reviews := []*models.Review{}
|
||||
var page int64
|
||||
for page = 0; page <= totalPages; page++ {
|
||||
if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id > ? AND issue_id <= ? AND type in (?, ?) GROUP BY issue_id, reviewer_id)",
|
||||
page*pageSize, (page+1)*pageSize, models.ReviewTypeApprove, models.ReviewTypeReject).
|
||||
Find(&reviews); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, review := range reviews {
|
||||
if err := review.LoadAttributes(); err != nil {
|
||||
// Error might occur if user or issue doesn't exist, ignore it.
|
||||
continue
|
||||
}
|
||||
official, err := models.IsOfficialReviewer(review.Issue, review.Reviewer)
|
||||
if err != nil {
|
||||
// Branch might not be proteced or other error, ignore it.
|
||||
continue
|
||||
}
|
||||
review.Official = official
|
||||
|
||||
if _, err := sess.ID(review.ID).Cols("official").Update(review); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2019 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func removeAttachmentMissedRepo(x *xorm.Engine) error {
|
||||
type Attachment struct {
|
||||
UUID string `xorm:"uuid"`
|
||||
}
|
||||
var start int
|
||||
attachments := make([]*Attachment, 0, 50)
|
||||
for {
|
||||
err := x.Select("uuid").Where(builder.NotIn("release_id", builder.Select("id").From("`release`"))).
|
||||
And("release_id > 0").
|
||||
OrderBy("id").Limit(50, start).Find(&attachments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < len(attachments); i++ {
|
||||
uuid := attachments[i].UUID
|
||||
if err = os.RemoveAll(path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid)); err != nil {
|
||||
fmt.Printf("Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(attachments) < 50 {
|
||||
break
|
||||
}
|
||||
start += 50
|
||||
attachments = attachments[:0]
|
||||
}
|
||||
|
||||
_, err := x.Exec("DELETE FROM attachment WHERE release_id > 0 AND release_id NOT IN (SELECT id FROM `release`)")
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2019 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 (
|
||||
"fmt"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func featureChangeTargetBranch(x *xorm.Engine) error {
|
||||
type Comment struct {
|
||||
OldRef string
|
||||
NewRef string
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(Comment)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019 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 (
|
||||
"net/url"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func sanitizeOriginalURL(x *xorm.Engine) error {
|
||||
|
||||
type Repository struct {
|
||||
ID int64
|
||||
OriginalURL string `xorm:"VARCHAR(2048)"`
|
||||
}
|
||||
|
||||
var last int
|
||||
const batchSize = 50
|
||||
for {
|
||||
var results = make([]Repository, 0, batchSize)
|
||||
err := x.Where("original_url <> '' AND original_url IS NOT NULL").
|
||||
And("original_service_type = 0 OR original_service_type IS NULL").
|
||||
OrderBy("id").
|
||||
Limit(batchSize, last).
|
||||
Find(&results)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) == 0 {
|
||||
break
|
||||
}
|
||||
last += len(results)
|
||||
|
||||
for _, res := range results {
|
||||
u, err := url.Parse(res.OriginalURL)
|
||||
if err != nil {
|
||||
// it is ok to continue here, we only care about fixing URLs that we can read
|
||||
continue
|
||||
}
|
||||
u.User = nil
|
||||
originalURL := u.String()
|
||||
_, err = x.Exec("UPDATE repository SET original_url = ? WHERE id = ?", originalURL, res.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2019 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 (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func renameExistingUserAvatarName(x *xorm.Engine) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
type User struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
LowerName string `xorm:"UNIQUE NOT NULL"`
|
||||
Avatar string
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
count, err := x.Count(new(User))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("%d User Avatar(s) to migrate ...", count)
|
||||
|
||||
deleteList := make(map[string]struct{})
|
||||
start := 0
|
||||
migrated := 0
|
||||
for {
|
||||
if err := sess.Begin(); err != nil {
|
||||
return fmt.Errorf("session.Begin: %v", err)
|
||||
}
|
||||
users := make([]*User, 0, 50)
|
||||
if err := sess.Table("user").Asc("id").Limit(50, start).Find(&users); err != nil {
|
||||
return fmt.Errorf("select users from id [%d]: %v", start, err)
|
||||
}
|
||||
if len(users) == 0 {
|
||||
_ = sess.Rollback()
|
||||
break
|
||||
}
|
||||
|
||||
log.Info("select users [%d - %d]", start, start+len(users))
|
||||
start += 50
|
||||
|
||||
for _, user := range users {
|
||||
oldAvatar := user.Avatar
|
||||
|
||||
if stat, err := os.Stat(filepath.Join(setting.AvatarUploadPath, oldAvatar)); err != nil || !stat.Mode().IsRegular() {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("Error: \"%s\" is not a regular file", oldAvatar)
|
||||
}
|
||||
log.Warn("[user: %s] os.Stat: %v", user.LowerName, err)
|
||||
// avatar doesn't exist in the storage
|
||||
// no need to move avatar and update database
|
||||
// we can just skip this
|
||||
continue
|
||||
}
|
||||
|
||||
newAvatar, err := copyOldAvatarToNewLocation(user.ID, oldAvatar)
|
||||
if err != nil {
|
||||
_ = sess.Rollback()
|
||||
return fmt.Errorf("[user: %s] %v", user.LowerName, err)
|
||||
} else if newAvatar == oldAvatar {
|
||||
continue
|
||||
}
|
||||
|
||||
user.Avatar = newAvatar
|
||||
if _, err := sess.ID(user.ID).Cols("avatar").Update(user); err != nil {
|
||||
_ = sess.Rollback()
|
||||
return fmt.Errorf("[user: %s] user table update: %v", user.LowerName, err)
|
||||
}
|
||||
|
||||
deleteList[filepath.Join(setting.AvatarUploadPath, oldAvatar)] = struct{}{}
|
||||
migrated++
|
||||
select {
|
||||
case <-ticker.C:
|
||||
log.Info(
|
||||
"%d/%d (%2.0f%%) User Avatar(s) migrated (%d old avatars to be deleted) in %d batches. %d Remaining ...",
|
||||
migrated,
|
||||
count,
|
||||
float64(migrated)/float64(count)*100,
|
||||
len(deleteList),
|
||||
int(math.Ceil(float64(migrated)/float64(50))),
|
||||
count-int64(migrated))
|
||||
default:
|
||||
}
|
||||
}
|
||||
if err := sess.Commit(); err != nil {
|
||||
_ = sess.Rollback()
|
||||
return fmt.Errorf("commit session: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
deleteCount := len(deleteList)
|
||||
log.Info("Deleting %d old avatars ...", deleteCount)
|
||||
i := 0
|
||||
for file := range deleteList {
|
||||
if err := os.Remove(file); err != nil {
|
||||
log.Warn("os.Remove: %v", err)
|
||||
}
|
||||
i++
|
||||
select {
|
||||
case <-ticker.C:
|
||||
log.Info(
|
||||
"%d/%d (%2.0f%%) Old User Avatar(s) deleted. %d Remaining ...",
|
||||
i,
|
||||
deleteCount,
|
||||
float64(i)/float64(deleteCount)*100,
|
||||
deleteCount-i)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Completed migrating %d User Avatar(s) and deleting %d Old Avatars", count, deleteCount)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyOldAvatarToNewLocation copies oldAvatar to newAvatarLocation
|
||||
// and returns newAvatar location
|
||||
func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) {
|
||||
fr, err := os.Open(filepath.Join(setting.AvatarUploadPath, oldAvatar))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("os.Open: %v", err)
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(fr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ioutil.ReadAll: %v", err)
|
||||
}
|
||||
|
||||
newAvatar := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", userID, md5.Sum(data)))))
|
||||
if newAvatar == oldAvatar {
|
||||
return newAvatar, nil
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(setting.AvatarUploadPath, newAvatar), data, 0666); err != nil {
|
||||
return "", fmt.Errorf("ioutil.WriteFile: %v", err)
|
||||
}
|
||||
|
||||
return newAvatar, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2019 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 extendTrackedTimes(x *xorm.Engine) error {
|
||||
|
||||
type TrackedTime struct {
|
||||
Time int64 `xorm:"NOT NULL"`
|
||||
Deleted bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM tracked_time WHERE time IS NULL"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sess.Sync2(new(TrackedTime)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
@@ -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 addBlockOnRejectedReviews(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(ProtectedBranch))
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019 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 addReviewCommitAndStale(x *xorm.Engine) error {
|
||||
type Review struct {
|
||||
CommitID string `xorm:"VARCHAR(40)"`
|
||||
Stale bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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 fixMigratedRepositoryServiceType(x *xorm.Engine) error {
|
||||
// structs.GithubService:
|
||||
// GithubService = 2
|
||||
_, err := x.Exec("UPDATE repository SET original_service_type = ? WHERE original_url LIKE 'https://github.com/%'", 2)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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 addOwnerNameOnRepository(x *xorm.Engine) error {
|
||||
type Repository struct {
|
||||
OwnerName string
|
||||
}
|
||||
if err := x.Sync2(new(Repository)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := x.Exec("UPDATE repository SET owner_name = (SELECT name FROM `user` WHERE `user`.id = repository.owner_id)")
|
||||
return err
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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 addRequireSignedCommits(x *xorm.Engine) error {
|
||||
|
||||
type ProtectedBranch struct {
|
||||
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(ProtectedBranch))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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 addReactionOriginals(x *xorm.Engine) error {
|
||||
type Reaction struct {
|
||||
OriginalAuthorID int64 `xorm:"INDEX NOT NULL DEFAULT(0)"`
|
||||
OriginalAuthor string
|
||||
}
|
||||
|
||||
return x.Sync2(new(Reaction))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 addUserRepoMissingColumns(x *xorm.Engine) error {
|
||||
|
||||
type VisibleType int
|
||||
type User struct {
|
||||
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'pbkdf2'"`
|
||||
Visibility VisibleType `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
IsArchived bool `xorm:"INDEX"`
|
||||
Topics []string `xorm:"TEXT JSON"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(User), new(Repository))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addReviewMigrateInfo(x *xorm.Engine) error {
|
||||
type Review struct {
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(Review)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user