db: refactor "action" table to use GORM (#7054)

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
This commit is contained in:
Joe Chen
2022-06-25 18:07:39 +08:00
committed by GitHub
parent 9df4e3ae3c
commit 083c3ee659
70 changed files with 3312 additions and 1204 deletions

View File

@@ -56,7 +56,7 @@ jobs:
strategy:
matrix:
go-version: [ 1.16.x, 1.17.x, 1.18.x ]
platform: [ ubuntu-latest, macos-latest, windows-latest ]
platform: [ ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
@@ -89,6 +89,46 @@ jobs:
View the job run at: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Running tests with race detection consumes too much memory on Windows,
# see https://github.com/golang/go/issues/46099 for details.
test-windows:
name: Test
strategy:
matrix:
go-version: [ 1.16.x, 1.17.x, 1.18.x ]
platform: [ windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests with coverage
run: go test -v -coverprofile=coverage -covermode=atomic ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v1.5.0
with:
file: ./coverage
flags: unittests
- name: Send email on failure
uses: dawidd6/action-send-mail@v3
if: ${{ failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
with:
server_address: smtp.mailgun.org
server_port: 465
username: ${{ secrets.SMTP_USERNAME }}
password: ${{ secrets.SMTP_PASSWORD }}
subject: GitHub Actions (${{ github.repository }}) job result
to: github-actions-8ce6454@unknwon.io
from: GitHub Actions (${{ github.repository }})
reply_to: noreply@unknwon.io
body: |
The job "${{ github.job }}" of ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} completed with "${{ job.status }}".
View the job run at: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
postgres:
name: Postgres
strategy:
@@ -151,7 +191,7 @@ jobs:
MYSQL_PORT: 3306
sqlite-go:
name: SQLite (Go)
name: SQLite - Go
strategy:
matrix:
go-version: [ 1.16.x, 1.17.x, 1.18.x ]

View File

@@ -31,6 +31,30 @@ Indexes:
"idx_access_token_user_id" (uid)
```
# Table "action"
```
FIELD | COLUMN | POSTGRESQL | MYSQL | SQLITE3
---------------+----------------+--------------------------------+--------------------------------+---------------------------------
ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER
UserID | user_id | BIGINT | BIGINT | INTEGER
OpType | op_type | BIGINT | BIGINT | INTEGER
ActUserID | act_user_id | BIGINT | BIGINT | INTEGER
ActUserName | act_user_name | TEXT | LONGTEXT | TEXT
RepoID | repo_id | BIGINT | BIGINT | INTEGER
RepoUserName | repo_user_name | TEXT | LONGTEXT | TEXT
RepoName | repo_name | TEXT | LONGTEXT | TEXT
RefName | ref_name | TEXT | LONGTEXT | TEXT
IsPrivate | is_private | BOOLEAN NOT NULL DEFAULT FALSE | BOOLEAN NOT NULL DEFAULT FALSE | NUMERIC NOT NULL DEFAULT FALSE
Content | content | TEXT | LONGTEXT | TEXT
CreatedUnix | created_unix | BIGINT | BIGINT | INTEGER
Primary keys: id
Indexes:
"idx_action_repo_id" (repo_id)
"idx_action_user_id" (user_id)
```
# Table "lfs_object"
```

View File

@@ -8,6 +8,14 @@ import (
"testing"
)
func SetMockApp(t *testing.T, opts AppOpts) {
before := App
App = opts
t.Cleanup(func() {
App = before
})
}
func SetMockServer(t *testing.T, opts ServerOpts) {
before := Server
Server = opts
@@ -15,3 +23,27 @@ func SetMockServer(t *testing.T, opts ServerOpts) {
Server = before
})
}
func SetMockSSH(t *testing.T, opts SSHOpts) {
before := SSH
SSH = opts
t.Cleanup(func() {
SSH = before
})
}
func SetMockRepository(t *testing.T, opts RepositoryOpts) {
before := Repository
Repository = opts
t.Cleanup(func() {
Repository = before
})
}
func SetMockUI(t *testing.T, opts UIOpts) {
before := UI
UI = opts
t.Cleanup(func() {
UI = before
})
}

View File

@@ -13,6 +13,9 @@ import (
)
// README: This file contains static values that should only be set at initialization time.
//
// ⚠️ WARNING: After changing any options, do not forget to update template of
// "/admin/config" page as well.
// HasMinWinSvc is whether the application is built with Windows Service support.
//
@@ -30,67 +33,7 @@ var (
// CustomConf returns the absolute path of custom configuration file that is used.
var CustomConf string
// ⚠️ WARNING: After changing the following section, do not forget to update template of
// "/admin/config" page as well.
var (
// Application settings
App struct {
// ⚠️ WARNING: Should only be set by the main package (i.e. "gogs.go").
Version string `ini:"-"`
BrandName string
RunUser string
RunMode string
}
// SSH settings
SSH struct {
Disabled bool `ini:"DISABLE_SSH"`
Domain string `ini:"SSH_DOMAIN"`
Port int `ini:"SSH_PORT"`
RootPath string `ini:"SSH_ROOT_PATH"`
KeygenPath string `ini:"SSH_KEYGEN_PATH"`
KeyTestPath string `ini:"SSH_KEY_TEST_PATH"`
MinimumKeySizeCheck bool
MinimumKeySizes map[string]int `ini:"-"` // Load from [ssh.minimum_key_sizes]
RewriteAuthorizedKeysAtStart bool
StartBuiltinServer bool `ini:"START_SSH_SERVER"`
ListenHost string `ini:"SSH_LISTEN_HOST"`
ListenPort int `ini:"SSH_LISTEN_PORT"`
ServerCiphers []string `ini:"SSH_SERVER_CIPHERS"`
ServerMACs []string `ini:"SSH_SERVER_MACS"`
}
// Repository settings
Repository struct {
Root string
ScriptType string
ANSICharset string `ini:"ANSI_CHARSET"`
ForcePrivate bool
MaxCreationLimit int
PreferredLicenses []string
DisableHTTPGit bool `ini:"DISABLE_HTTP_GIT"`
EnableLocalPathMigration bool
EnableRawFileRenderMode bool
CommitsFetchConcurrency int
// Repository editor settings
Editor struct {
LineWrapExtensions []string
PreviewableFileModes []string
} `ini:"repository.editor"`
// Repository upload settings
Upload struct {
Enabled bool
TempPath string
AllowedTypes []string `delim:"|"`
FileMaxSize int64
MaxFiles int
} `ini:"repository.upload"`
}
// Security settings
Security struct {
InstallLock bool
@@ -295,27 +238,6 @@ var (
MaxResponseItems int
}
// UI settings
UI struct {
ExplorePagingNum int
IssuePagingNum int
FeedMaxCommitNum int
ThemeColorMetaTag string
MaxDisplayFileSize int64
Admin struct {
UserPagingNum int
RepoPagingNum int
NoticePagingNum int
OrgPagingNum int
} `ini:"ui.admin"`
User struct {
RepoPagingNum int
NewsFeedPagingNum int
CommitsPagingNum int
} `ini:"ui.user"`
}
// Prometheus settings
Prometheus struct {
Enabled bool
@@ -334,6 +256,18 @@ var (
HasRobotsTxt bool
)
type AppOpts struct {
// ⚠️ WARNING: Should only be set by the main package (i.e. "gogs.go").
Version string `ini:"-"`
BrandName string
RunUser string
RunMode string
}
// Application settings
var App AppOpts
type ServerOpts struct {
ExternalURL string `ini:"EXTERNAL_URL"`
Domain string
@@ -365,6 +299,58 @@ type ServerOpts struct {
// Server settings
var Server ServerOpts
type SSHOpts struct {
Disabled bool `ini:"DISABLE_SSH"`
Domain string `ini:"SSH_DOMAIN"`
Port int `ini:"SSH_PORT"`
RootPath string `ini:"SSH_ROOT_PATH"`
KeygenPath string `ini:"SSH_KEYGEN_PATH"`
KeyTestPath string `ini:"SSH_KEY_TEST_PATH"`
MinimumKeySizeCheck bool
MinimumKeySizes map[string]int `ini:"-"` // Load from [ssh.minimum_key_sizes]
RewriteAuthorizedKeysAtStart bool
StartBuiltinServer bool `ini:"START_SSH_SERVER"`
ListenHost string `ini:"SSH_LISTEN_HOST"`
ListenPort int `ini:"SSH_LISTEN_PORT"`
ServerCiphers []string `ini:"SSH_SERVER_CIPHERS"`
ServerMACs []string `ini:"SSH_SERVER_MACS"`
}
// SSH settings
var SSH SSHOpts
type RepositoryOpts struct {
Root string
ScriptType string
ANSICharset string `ini:"ANSI_CHARSET"`
ForcePrivate bool
MaxCreationLimit int
PreferredLicenses []string
DisableHTTPGit bool `ini:"DISABLE_HTTP_GIT"`
EnableLocalPathMigration bool
EnableRawFileRenderMode bool
CommitsFetchConcurrency int
// Repository editor settings
Editor struct {
LineWrapExtensions []string
PreviewableFileModes []string
} `ini:"repository.editor"`
// Repository upload settings
Upload struct {
Enabled bool
TempPath string
AllowedTypes []string `delim:"|"`
FileMaxSize int64
MaxFiles int
} `ini:"repository.upload"`
}
// Repository settings
var Repository RepositoryOpts
type DatabaseOpts struct {
Type string
Host string
@@ -389,6 +375,31 @@ type LFSOpts struct {
// LFS settings
var LFS LFSOpts
type UIUserOpts struct {
RepoPagingNum int
NewsFeedPagingNum int
CommitsPagingNum int
}
type UIOpts struct {
ExplorePagingNum int
IssuePagingNum int
FeedMaxCommitNum int
ThemeColorMetaTag string
MaxDisplayFileSize int64
Admin struct {
UserPagingNum int
RepoPagingNum int
NoticePagingNum int
OrgPagingNum int
} `ini:"ui.admin"`
User UIUserOpts `ini:"ui.user"`
}
// UI settings
var UI UIOpts
type i18nConf struct {
Langs []string `delim:","`
Names []string `delim:","`

View File

@@ -10,6 +10,7 @@ import (
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/repoutil"
)
// ServeGoGet does quick responses for appropriate go-get meta with status OK
@@ -52,7 +53,7 @@ func ServeGoGet() macaron.Handler {
`,
map[string]string{
"GoGetImport": path.Join(conf.Server.URL.Host, conf.Server.Subpath, ownerName, repoName),
"CloneLink": db.ComposeHTTPSCloneURL(ownerName, repoName),
"CloneLink": repoutil.HTTPSCloneURL(ownerName, repoName),
"GoDocDirectory": prefix + "{/dir}",
"GoDocFile": prefix + "{/dir}/{file}#L{line}",
"InsecureFlag": insecureFlag,

View File

@@ -18,6 +18,7 @@ import (
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/repoutil"
)
type PullRequest struct {
@@ -43,7 +44,7 @@ type Repository struct {
TreePath string
CommitID string
RepoLink string
CloneLink db.CloneLink
CloneLink repoutil.CloneLink
CommitsCount int64
Mirror *db.Mirror

View File

@@ -42,7 +42,7 @@ var AccessTokens AccessTokensStore
// AccessToken is a personal access token.
type AccessToken struct {
ID int64
ID int64 `gorm:"primarykey"`
UserID int64 `xorm:"uid" gorm:"column:uid;index"`
Name string
Sha1 string `gorm:"type:VARCHAR(40);unique"`
@@ -67,9 +67,11 @@ func (t *AccessToken) BeforeCreate(tx *gorm.DB) error {
// AfterFind implements the GORM query hook.
func (t *AccessToken) AfterFind(tx *gorm.DB) error {
t.Created = time.Unix(t.CreatedUnix, 0).Local()
t.Updated = time.Unix(t.UpdatedUnix, 0).Local()
t.HasUsed = t.Updated.After(t.Created)
t.HasRecentActivity = t.Updated.Add(7 * 24 * time.Hour).After(tx.NowFunc())
if t.UpdatedUnix > 0 {
t.Updated = time.Unix(t.UpdatedUnix, 0).Local()
t.HasUsed = t.Updated.After(t.Created)
t.HasRecentActivity = t.Updated.Add(7 * 24 * time.Hour).After(tx.NowFunc())
}
return nil
}

View File

@@ -46,7 +46,6 @@ func TestAccessTokens(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(AccessToken)}
@@ -95,7 +94,12 @@ func accessTokensCreate(t *testing.T, db *accessTokens) {
// Try create second access token with same name should fail
_, err = db.Create(ctx, token.UserID, token.Name)
wantErr := ErrAccessTokenAlreadyExist{args: errutil.Args{"userID": token.UserID, "name": token.Name}}
wantErr := ErrAccessTokenAlreadyExist{
args: errutil.Args{
"userID": token.UserID,
"name": token.Name,
},
}
assert.Equal(t, wantErr, err)
}
@@ -113,8 +117,6 @@ func accessTokensDeleteByID(t *testing.T, db *accessTokens) {
// We should be able to get it back
_, err = db.GetBySHA1(ctx, token.Sha1)
require.NoError(t, err)
_, err = db.GetBySHA1(ctx, token.Sha1)
require.NoError(t, err)
// Now delete this token with correct user ID
err = db.DeleteByID(ctx, token.UserID, token.ID)
@@ -122,7 +124,11 @@ func accessTokensDeleteByID(t *testing.T, db *accessTokens) {
// We should get token not found error
_, err = db.GetBySHA1(ctx, token.Sha1)
wantErr := ErrAccessTokenNotExist{args: errutil.Args{"sha": token.Sha1}}
wantErr := ErrAccessTokenNotExist{
args: errutil.Args{
"sha": token.Sha1,
},
}
assert.Equal(t, wantErr, err)
}
@@ -139,7 +145,11 @@ func accessTokensGetBySHA(t *testing.T, db *accessTokens) {
// Try to get a non-existent token
_, err = db.GetBySHA1(ctx, "bad_sha")
wantErr := ErrAccessTokenNotExist{args: errutil.Args{"sha": "bad_sha"}}
wantErr := ErrAccessTokenNotExist{
args: errutil.Args{
"sha": "bad_sha",
},
}
assert.Equal(t, wantErr, err)
}

View File

@@ -1,766 +0,0 @@
// Copyright 2014 The Gogs 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 db
import (
"fmt"
"path"
"strings"
"time"
"unicode"
jsoniter "github.com/json-iterator/go"
"github.com/unknwon/com"
log "unknwon.dev/clog/v2"
"xorm.io/xorm"
"github.com/gogs/git-module"
api "github.com/gogs/go-gogs-client"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/lazyregexp"
"gogs.io/gogs/internal/tool"
)
type ActionType int
// Note: To maintain backward compatibility only append to the end of list
const (
ACTION_CREATE_REPO ActionType = iota + 1 // 1
ACTION_RENAME_REPO // 2
ACTION_STAR_REPO // 3
ACTION_WATCH_REPO // 4
ACTION_COMMIT_REPO // 5
ACTION_CREATE_ISSUE // 6
ACTION_CREATE_PULL_REQUEST // 7
ACTION_TRANSFER_REPO // 8
ACTION_PUSH_TAG // 9
ACTION_COMMENT_ISSUE // 10
ACTION_MERGE_PULL_REQUEST // 11
ACTION_CLOSE_ISSUE // 12
ACTION_REOPEN_ISSUE // 13
ACTION_CLOSE_PULL_REQUEST // 14
ACTION_REOPEN_PULL_REQUEST // 15
ACTION_CREATE_BRANCH // 16
ACTION_DELETE_BRANCH // 17
ACTION_DELETE_TAG // 18
ACTION_FORK_REPO // 19
ACTION_MIRROR_SYNC_PUSH // 20
ACTION_MIRROR_SYNC_CREATE // 21
ACTION_MIRROR_SYNC_DELETE // 22
)
var (
// Same as Github. See https://help.github.com/articles/closing-issues-via-commit-messages
IssueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
IssueReopenKeywords = []string{"reopen", "reopens", "reopened"}
IssueCloseKeywordsPat = lazyregexp.New(assembleKeywordsPattern(IssueCloseKeywords))
IssueReopenKeywordsPat = lazyregexp.New(assembleKeywordsPattern(IssueReopenKeywords))
issueReferencePattern = lazyregexp.New(`(?i)(?:)(^| )\S*#\d+`)
)
func assembleKeywordsPattern(words []string) string {
return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
}
// Action represents user operation type and other information to repository,
// it implemented interface base.Actioner so that can be used in template render.
type Action struct {
ID int64
UserID int64 // Receiver user ID
OpType ActionType
ActUserID int64 // Doer user ID
ActUserName string // Doer user name
ActAvatar string `xorm:"-" json:"-"`
RepoID int64 `xorm:"INDEX"`
RepoUserName string
RepoName string
RefName string
IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
Content string `xorm:"TEXT"`
Created time.Time `xorm:"-" json:"-"`
CreatedUnix int64
}
func (a *Action) BeforeInsert() {
a.CreatedUnix = time.Now().Unix()
}
func (a *Action) AfterSet(colName string, _ xorm.Cell) {
switch colName {
case "created_unix":
a.Created = time.Unix(a.CreatedUnix, 0).Local()
}
}
func (a *Action) GetOpType() int {
return int(a.OpType)
}
func (a *Action) GetActUserName() string {
return a.ActUserName
}
func (a *Action) ShortActUserName() string {
return tool.EllipsisString(a.ActUserName, 20)
}
func (a *Action) GetRepoUserName() string {
return a.RepoUserName
}
func (a *Action) ShortRepoUserName() string {
return tool.EllipsisString(a.RepoUserName, 20)
}
func (a *Action) GetRepoName() string {
return a.RepoName
}
func (a *Action) ShortRepoName() string {
return tool.EllipsisString(a.RepoName, 33)
}
func (a *Action) GetRepoPath() string {
return path.Join(a.RepoUserName, a.RepoName)
}
func (a *Action) ShortRepoPath() string {
return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
}
func (a *Action) GetRepoLink() string {
if conf.Server.Subpath != "" {
return path.Join(conf.Server.Subpath, a.GetRepoPath())
}
return "/" + a.GetRepoPath()
}
func (a *Action) GetBranch() string {
return a.RefName
}
func (a *Action) GetContent() string {
return a.Content
}
func (a *Action) GetCreate() time.Time {
return a.Created
}
func (a *Action) GetIssueInfos() []string {
return strings.SplitN(a.Content, "|", 2)
}
func (a *Action) GetIssueTitle() string {
index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
issue, err := GetIssueByIndex(a.RepoID, index)
if err != nil {
log.Error("GetIssueByIndex: %v", err)
return "500 when get issue"
}
return issue.Title
}
func (a *Action) GetIssueContent() string {
index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
issue, err := GetIssueByIndex(a.RepoID, index)
if err != nil {
log.Error("GetIssueByIndex: %v", err)
return "500 when get issue"
}
return issue.Content
}
func newRepoAction(e Engine, doer, _ *User, repo *Repository) (err error) {
opType := ACTION_CREATE_REPO
if repo.IsFork {
opType = ACTION_FORK_REPO
}
return notifyWatchers(e, &Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: opType,
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
})
}
// NewRepoAction adds new action for creating repository.
func NewRepoAction(doer, owner *User, repo *Repository) (err error) {
return newRepoAction(x, doer, owner, repo)
}
func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{
ActUserID: actUser.ID,
ActUserName: actUser.Name,
OpType: ACTION_RENAME_REPO,
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
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)
}
func issueIndexTrimRight(c rune) bool {
return !unicode.IsDigit(c)
}
type PushCommit struct {
Sha1 string
Message string
AuthorEmail string
AuthorName string
CommitterEmail string
CommitterName string
Timestamp time.Time
}
type PushCommits struct {
Len int
Commits []*PushCommit
CompareURL string
avatars map[string]string
}
func NewPushCommits() *PushCommits {
return &PushCommits{
avatars: make(map[string]string),
}
}
func (pc *PushCommits) ToApiPayloadCommits(repoPath, repoURL string) ([]*api.PayloadCommit, error) {
commits := make([]*api.PayloadCommit, len(pc.Commits))
for i, commit := range pc.Commits {
authorUsername := ""
author, err := GetUserByEmail(commit.AuthorEmail)
if err == nil {
authorUsername = author.Name
} else if !IsErrUserNotExist(err) {
return nil, fmt.Errorf("get user by email: %v", err)
}
committerUsername := ""
committer, err := GetUserByEmail(commit.CommitterEmail)
if err == nil {
committerUsername = committer.Name
} else if !IsErrUserNotExist(err) {
return nil, fmt.Errorf("get user by email: %v", err)
}
nameStatus, err := git.ShowNameStatus(repoPath, commit.Sha1)
if err != nil {
return nil, fmt.Errorf("show name status [commit_sha1: %s]: %v", commit.Sha1, err)
}
commits[i] = &api.PayloadCommit{
ID: commit.Sha1,
Message: commit.Message,
URL: fmt.Sprintf("%s/commit/%s", repoURL, 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: nameStatus.Added,
Removed: nameStatus.Removed,
Modified: nameStatus.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 (pcs *PushCommits) AvatarLink(email string) string {
_, ok := pcs.avatars[email]
if !ok {
u, err := GetUserByEmail(email)
if err != nil {
pcs.avatars[email] = tool.AvatarLink(email)
if !IsErrUserNotExist(err) {
log.Error("get user by email: %v", err)
}
} else {
pcs.avatars[email] = u.RelAvatarLink()
}
}
return pcs.avatars[email]
}
// UpdateIssuesCommit checks if issues are manipulated by commit message.
func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) error {
// Commits are appended in the reverse order.
for i := len(commits) - 1; i >= 0; i-- {
c := commits[i]
refMarked := make(map[int64]bool)
for _, ref := range issueReferencePattern.FindAllString(c.Message, -1) {
ref = strings.TrimSpace(ref)
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
if ref == "" {
continue
}
// Add repo name if missing
if ref[0] == '#' {
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
} else if !strings.Contains(ref, "/") {
// FIXME: We don't support User#ID syntax yet
// return ErrNotImplemented
continue
}
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
if refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
msgLines := strings.Split(c.Message, "\n")
shortMsg := msgLines[0]
if len(msgLines) > 2 {
shortMsg += "..."
}
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, shortMsg)
if err = CreateRefComment(doer, repo, issue, message, c.Sha1); err != nil {
return err
}
}
refMarked = make(map[int64]bool)
// FIXME: can merge this one and next one to a common function.
for _, ref := range IssueCloseKeywordsPat.FindAllString(c.Message, -1) {
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
if ref == "" {
continue
}
// Add repo name if missing
if ref[0] == '#' {
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
} else if !strings.Contains(ref, "/") {
// FIXME: We don't support User#ID syntax yet
continue
}
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
if refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
if issue.RepoID != repo.ID || issue.IsClosed {
continue
}
if err = issue.ChangeStatus(doer, repo, true); err != nil {
return err
}
}
// It is conflict to have close and reopen at same time, so refsMarkd doesn't need to reinit here.
for _, ref := range IssueReopenKeywordsPat.FindAllString(c.Message, -1) {
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
if ref == "" {
continue
}
// Add repo name if missing
if ref[0] == '#' {
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
} else if !strings.Contains(ref, "/") {
// We don't support User#ID syntax yet
// return ErrNotImplemented
continue
}
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
if refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
if issue.RepoID != repo.ID || !issue.IsClosed {
continue
}
if err = issue.ChangeStatus(doer, repo, false); err != nil {
return err
}
}
}
return nil
}
type CommitRepoActionOptions struct {
PusherName string
RepoOwnerID int64
RepoName string
RefFullName string
OldCommitID string
NewCommitID string
Commits *PushCommits
}
// CommitRepoAction adds new commit action to the repository, and prepare corresponding webhooks.
func CommitRepoAction(opts CommitRepoActionOptions) error {
pusher, err := GetUserByName(opts.PusherName)
if err != nil {
return fmt.Errorf("GetUserByName [%s]: %v", opts.PusherName, err)
}
repo, err := GetRepositoryByName(opts.RepoOwnerID, opts.RepoName)
if err != nil {
return fmt.Errorf("GetRepositoryByName [owner_id: %d, name: %s]: %v", opts.RepoOwnerID, opts.RepoName, err)
}
// Change repository bare status and update last updated time.
repo.IsBare = false
if err = UpdateRepository(repo, false); err != nil {
return fmt.Errorf("UpdateRepository: %v", err)
}
isNewRef := opts.OldCommitID == git.EmptyID
isDelRef := opts.NewCommitID == git.EmptyID
opType := ACTION_COMMIT_REPO
// Check if it's tag push or branch.
if strings.HasPrefix(opts.RefFullName, git.RefsTags) {
opType = ACTION_PUSH_TAG
} else {
// if not the first commit, set the compare URL.
if !isNewRef && !isDelRef {
opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
}
// Only update issues via commits when internal issue tracker is enabled
if repo.EnableIssues && !repo.EnableExternalTracker {
if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits); err != nil {
log.Error("UpdateIssuesCommit: %v", err)
}
}
}
if len(opts.Commits.Commits) > conf.UI.FeedMaxCommitNum {
opts.Commits.Commits = opts.Commits.Commits[:conf.UI.FeedMaxCommitNum]
}
data, err := jsoniter.Marshal(opts.Commits)
if err != nil {
return fmt.Errorf("Marshal: %v", err)
}
refName := git.RefShortName(opts.RefFullName)
action := &Action{
ActUserID: pusher.ID,
ActUserName: pusher.Name,
Content: string(data),
RepoID: repo.ID,
RepoUserName: repo.MustOwner().Name,
RepoName: repo.Name,
RefName: refName,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
}
apiRepo := repo.APIFormat(nil)
apiPusher := pusher.APIFormat()
switch opType {
case ACTION_COMMIT_REPO: // Push
if isDelRef {
if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
Ref: refName,
RefType: "branch",
PusherType: api.PUSHER_TYPE_USER,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err)
}
action.OpType = ACTION_DELETE_BRANCH
if err = NotifyWatchers(action); err != nil {
return fmt.Errorf("NotifyWatchers.(delete branch): %v", err)
}
// Delete branch doesn't have anything to push or compare
return nil
}
compareURL := conf.Server.ExternalURL + opts.Commits.CompareURL
if isNewRef {
compareURL = ""
if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
Ref: refName,
RefType: "branch",
DefaultBranch: repo.DefaultBranch,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(new branch): %v", err)
}
action.OpType = ACTION_CREATE_BRANCH
if err = NotifyWatchers(action); err != nil {
return fmt.Errorf("NotifyWatchers.(new branch): %v", err)
}
}
commits, err := opts.Commits.ToApiPayloadCommits(repo.RepoPath(), repo.HTMLURL())
if err != nil {
return fmt.Errorf("ToApiPayloadCommits: %v", err)
}
if err = PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
Ref: opts.RefFullName,
Before: opts.OldCommitID,
After: opts.NewCommitID,
CompareURL: compareURL,
Commits: commits,
Repo: apiRepo,
Pusher: apiPusher,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(new commit): %v", err)
}
action.OpType = ACTION_COMMIT_REPO
if err = NotifyWatchers(action); err != nil {
return fmt.Errorf("NotifyWatchers.(new commit): %v", err)
}
case ACTION_PUSH_TAG: // Tag
if isDelRef {
if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
Ref: refName,
RefType: "tag",
PusherType: api.PUSHER_TYPE_USER,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
}
action.OpType = ACTION_DELETE_TAG
if err = NotifyWatchers(action); err != nil {
return fmt.Errorf("NotifyWatchers.(delete tag): %v", err)
}
return nil
}
if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
Ref: refName,
RefType: "tag",
Sha: opts.NewCommitID,
DefaultBranch: repo.DefaultBranch,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(new tag): %v", err)
}
action.OpType = ACTION_PUSH_TAG
if err = NotifyWatchers(action); err != nil {
return fmt.Errorf("NotifyWatchers.(new tag): %v", err)
}
}
return nil
}
func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: ACTION_TRANSFER_REPO,
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
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,
ActUserName: doer.Name,
OpType: ACTION_MERGE_PULL_REQUEST,
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
})
}
// MergePullRequestAction adds new action for merging pull request.
func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error {
return mergePullRequestAction(x, actUser, repo, pull)
}
func mirrorSyncAction(opType ActionType, repo *Repository, refName string, data []byte) error {
return NotifyWatchers(&Action{
ActUserID: repo.OwnerID,
ActUserName: repo.MustOwner().Name,
OpType: opType,
Content: string(data),
RepoID: repo.ID,
RepoUserName: repo.MustOwner().Name,
RepoName: repo.Name,
RefName: refName,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
})
}
type MirrorSyncPushActionOptions struct {
RefName string
OldCommitID string
NewCommitID string
Commits *PushCommits
}
// MirrorSyncPushAction adds new action for mirror synchronization of pushed commits.
func MirrorSyncPushAction(repo *Repository, opts MirrorSyncPushActionOptions) error {
if len(opts.Commits.Commits) > conf.UI.FeedMaxCommitNum {
opts.Commits.Commits = opts.Commits.Commits[:conf.UI.FeedMaxCommitNum]
}
apiCommits, err := opts.Commits.ToApiPayloadCommits(repo.RepoPath(), repo.HTMLURL())
if err != nil {
return fmt.Errorf("ToApiPayloadCommits: %v", err)
}
opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
apiPusher := repo.MustOwner().APIFormat()
if err := PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
Ref: opts.RefName,
Before: opts.OldCommitID,
After: opts.NewCommitID,
CompareURL: conf.Server.ExternalURL + opts.Commits.CompareURL,
Commits: apiCommits,
Repo: repo.APIFormat(nil),
Pusher: apiPusher,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks: %v", err)
}
data, err := jsoniter.Marshal(opts.Commits)
if err != nil {
return err
}
return mirrorSyncAction(ACTION_MIRROR_SYNC_PUSH, repo, opts.RefName, data)
}
// MirrorSyncCreateAction adds new action for mirror synchronization of new reference.
func MirrorSyncCreateAction(repo *Repository, refName string) error {
return mirrorSyncAction(ACTION_MIRROR_SYNC_CREATE, repo, refName, nil)
}
// MirrorSyncCreateAction adds new action for mirror synchronization of delete reference.
func MirrorSyncDeleteAction(repo *Repository, refName string) error {
return mirrorSyncAction(ACTION_MIRROR_SYNC_DELETE, repo, refName, nil)
}
// GetFeeds returns action list of given user in given context.
// actorID is the user who's requesting, ctxUserID is the user/org that is requested.
// actorID can be -1 when isProfile is true or to skip the permission check.
func GetFeeds(ctxUser *User, actorID, afterID int64, isProfile bool) ([]*Action, error) {
actions := make([]*Action, 0, conf.UI.User.NewsFeedPagingNum)
sess := x.Limit(conf.UI.User.NewsFeedPagingNum).Where("user_id = ?", ctxUser.ID).Desc("id")
if afterID > 0 {
sess.And("id < ?", afterID)
}
if isProfile {
sess.And("is_private = ?", false).And("act_user_id = ?", ctxUser.ID)
} else if actorID != -1 && ctxUser.IsOrganization() {
// FIXME: only need to get IDs here, not all fields of repository.
repos, _, err := ctxUser.GetUserRepositories(actorID, 1, ctxUser.NumRepos)
if err != nil {
return nil, fmt.Errorf("GetUserRepositories: %v", err)
}
var repoIDs []int64
for _, repo := range repos {
repoIDs = append(repoIDs, repo.ID)
}
if len(repoIDs) > 0 {
sess.In("repo_id", repoIDs)
}
}
err := sess.Find(&actions)
return actions, err
}

View File

@@ -1,41 +0,0 @@
// Copyright 2020 The Gogs 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 db
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_issueReferencePattern(t *testing.T) {
tests := []struct {
name string
message string
expStrings []string
}{
{
name: "no match",
message: "Hello world!",
expStrings: nil,
},
{
name: "contains issue numbers",
message: "#123 is fixed, and #456 is WIP",
expStrings: []string{"#123", " #456"},
},
{
name: "contains full issue references",
message: "#123 is fixed, and user/repo#456 is WIP",
expStrings: []string{"#123", " user/repo#456"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
strs := issueReferencePattern.FindAllString(test.message, -1)
assert.Equal(t, test.expStrings, strs)
})
}
}

962
internal/db/actions.go Normal file
View File

@@ -0,0 +1,962 @@
// Copyright 2020 The Gogs 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 db
import (
"context"
"fmt"
"path"
"strconv"
"strings"
"time"
"unicode"
"github.com/gogs/git-module"
api "github.com/gogs/go-gogs-client"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
"gorm.io/gorm"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/lazyregexp"
"gogs.io/gogs/internal/repoutil"
"gogs.io/gogs/internal/strutil"
"gogs.io/gogs/internal/testutil"
"gogs.io/gogs/internal/tool"
)
// ActionsStore is the persistent interface for actions.
//
// NOTE: All methods are sorted in alphabetical order.
type ActionsStore interface {
// CommitRepo creates actions for pushing commits to the repository. An action
// with the type ActionDeleteBranch is created if the push deletes a branch; an
// action with the type ActionCommitRepo is created for a regular push. If the
// regular push also creates a new branch, then another action with type
// ActionCreateBranch is created.
CommitRepo(ctx context.Context, opts CommitRepoOptions) error
// ListByOrganization returns actions of the organization viewable by the actor.
// Results are paginated if `afterID` is given.
ListByOrganization(ctx context.Context, orgID, actorID, afterID int64) ([]*Action, error)
// ListByUser returns actions of the user viewable by the actor. Results are
// paginated if `afterID` is given. The `isProfile` indicates whether repository
// permissions should be considered.
ListByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) ([]*Action, error)
// MergePullRequest creates an action for merging a pull request.
MergePullRequest(ctx context.Context, doer, owner *User, repo *Repository, pull *Issue) error
// MirrorSyncCreate creates an action for mirror synchronization of a new
// reference.
MirrorSyncCreate(ctx context.Context, owner *User, repo *Repository, refName string) error
// MirrorSyncDelete creates an action for mirror synchronization of a reference
// deletion.
MirrorSyncDelete(ctx context.Context, owner *User, repo *Repository, refName string) error
// MirrorSyncPush creates an action for mirror synchronization of pushed
// commits.
MirrorSyncPush(ctx context.Context, opts MirrorSyncPushOptions) error
// NewRepo creates an action for creating a new repository. The action type
// could be ActionCreateRepo or ActionForkRepo based on whether the repository
// is a fork.
NewRepo(ctx context.Context, doer, owner *User, repo *Repository) error
// PushTag creates an action for pushing tags to the repository. An action with
// the type ActionDeleteTag is created if the push deletes a tag. Otherwise, an
// action with the type ActionPushTag is created for a regular push.
PushTag(ctx context.Context, opts PushTagOptions) error
// RenameRepo creates an action for renaming a repository.
RenameRepo(ctx context.Context, doer, owner *User, oldRepoName string, repo *Repository) error
// TransferRepo creates an action for transferring a repository to a new owner.
TransferRepo(ctx context.Context, doer, oldOwner, newOwner *User, repo *Repository) error
}
var Actions ActionsStore
var _ ActionsStore = (*actions)(nil)
type actions struct {
*gorm.DB
}
// NewActionsStore returns a persistent interface for actions with given
// database connection.
func NewActionsStore(db *gorm.DB) ActionsStore {
return &actions{DB: db}
}
func (db *actions) listByOrganization(ctx context.Context, orgID, actorID, afterID int64) *gorm.DB {
/*
Equivalent SQL for PostgreSQL:
SELECT * FROM "action"
WHERE
user_id = @userID
AND (@skipAfter OR id < @afterID)
AND repo_id IN (
SELECT repository.id FROM "repository"
JOIN team_repo ON repository.id = team_repo.repo_id
WHERE team_repo.team_id IN (
SELECT team_id FROM "team_user"
WHERE
team_user.org_id = @orgID AND uid = @actorID)
OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
)
ORDER BY id DESC
LIMIT @limit
*/
return db.WithContext(ctx).
Where("user_id = ?", orgID).
Where(db.
// Not apply when afterID is not given
Where("?", afterID <= 0).
Or("id < ?", afterID),
).
Where("repo_id IN (?)",
db.Select("repository.id").
Table("repository").
Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
Where("team_repo.team_id IN (?)",
db.Select("team_id").
Table("team_user").
Where("team_user.org_id = ? AND uid = ?", orgID, actorID),
).
Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
).
Limit(conf.UI.User.NewsFeedPagingNum).
Order("id DESC")
}
func (db *actions) ListByOrganization(ctx context.Context, orgID, actorID, afterID int64) ([]*Action, error) {
actions := make([]*Action, 0, conf.UI.User.NewsFeedPagingNum)
return actions, db.listByOrganization(ctx, orgID, actorID, afterID).Find(&actions).Error
}
func (db *actions) listByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) *gorm.DB {
/*
Equivalent SQL for PostgreSQL:
SELECT * FROM "action"
WHERE
user_id = @userID
AND (@skipAfter OR id < @afterID)
AND (@includePrivate OR (is_private = FALSE AND act_user_id = @actorID))
ORDER BY id DESC
LIMIT @limit
*/
return db.WithContext(ctx).
Where("user_id = ?", userID).
Where(db.
// Not apply when afterID is not given
Where("?", afterID <= 0).
Or("id < ?", afterID),
).
Where(db.
// Not apply when in not profile page or the user is viewing own profile
Where("?", !isProfile || actorID == userID).
Or("is_private = ? AND act_user_id = ?", false, userID),
).
Limit(conf.UI.User.NewsFeedPagingNum).
Order("id DESC")
}
func (db *actions) ListByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) ([]*Action, error) {
actions := make([]*Action, 0, conf.UI.User.NewsFeedPagingNum)
return actions, db.listByUser(ctx, userID, actorID, afterID, isProfile).Find(&actions).Error
}
// notifyWatchers creates rows in action table for watchers who are able to see the action.
func (db *actions) notifyWatchers(ctx context.Context, act *Action) error {
watches, err := NewWatchesStore(db.DB).ListByRepo(ctx, act.RepoID)
if err != nil {
return errors.Wrap(err, "list watches")
}
// Clone returns a deep copy of the action with UserID assigned
clone := func(userID int64) *Action {
tmp := *act
tmp.UserID = userID
return &tmp
}
// Plus one for the actor
actions := make([]*Action, 0, len(watches)+1)
actions = append(actions, clone(act.ActUserID))
for _, watch := range watches {
if act.ActUserID == watch.UserID {
continue
}
actions = append(actions, clone(watch.UserID))
}
return db.Create(actions).Error
}
func (db *actions) NewRepo(ctx context.Context, doer, owner *User, repo *Repository) error {
opType := ActionCreateRepo
if repo.IsFork {
opType = ActionForkRepo
}
return db.notifyWatchers(ctx,
&Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: opType,
RepoID: repo.ID,
RepoUserName: owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
},
)
}
func (db *actions) RenameRepo(ctx context.Context, doer, owner *User, oldRepoName string, repo *Repository) error {
return db.notifyWatchers(ctx,
&Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: ActionRenameRepo,
RepoID: repo.ID,
RepoUserName: owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
Content: oldRepoName,
},
)
}
func (db *actions) mirrorSyncAction(ctx context.Context, opType ActionType, owner *User, repo *Repository, refName string, content []byte) error {
return db.notifyWatchers(ctx,
&Action{
ActUserID: owner.ID,
ActUserName: owner.Name,
OpType: opType,
Content: string(content),
RepoID: repo.ID,
RepoUserName: owner.Name,
RepoName: repo.Name,
RefName: refName,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
},
)
}
type MirrorSyncPushOptions struct {
Owner *User
Repo *Repository
RefName string
OldCommitID string
NewCommitID string
Commits *PushCommits
}
func (db *actions) MirrorSyncPush(ctx context.Context, opts MirrorSyncPushOptions) error {
if conf.UI.FeedMaxCommitNum > 0 && len(opts.Commits.Commits) > conf.UI.FeedMaxCommitNum {
opts.Commits.Commits = opts.Commits.Commits[:conf.UI.FeedMaxCommitNum]
}
apiCommits, err := opts.Commits.APIFormat(ctx,
NewUsersStore(db.DB),
repoutil.RepositoryPath(opts.Owner.Name, opts.Repo.Name),
repoutil.HTMLURL(opts.Owner.Name, opts.Repo.Name),
)
if err != nil {
return errors.Wrap(err, "convert commits to API format")
}
opts.Commits.CompareURL = repoutil.CompareCommitsPath(opts.Owner.Name, opts.Repo.Name, opts.OldCommitID, opts.NewCommitID)
apiPusher := opts.Owner.APIFormat()
err = PrepareWebhooks(
opts.Repo,
HOOK_EVENT_PUSH,
&api.PushPayload{
Ref: opts.RefName,
Before: opts.OldCommitID,
After: opts.NewCommitID,
CompareURL: conf.Server.ExternalURL + opts.Commits.CompareURL,
Commits: apiCommits,
Repo: opts.Repo.APIFormat(opts.Owner),
Pusher: apiPusher,
Sender: apiPusher,
},
)
if err != nil {
return errors.Wrap(err, "prepare webhooks")
}
data, err := jsoniter.Marshal(opts.Commits)
if err != nil {
return errors.Wrap(err, "marshal JSON")
}
return db.mirrorSyncAction(ctx, ActionMirrorSyncPush, opts.Owner, opts.Repo, opts.RefName, data)
}
func (db *actions) MirrorSyncCreate(ctx context.Context, owner *User, repo *Repository, refName string) error {
return db.mirrorSyncAction(ctx, ActionMirrorSyncCreate, owner, repo, refName, nil)
}
func (db *actions) MirrorSyncDelete(ctx context.Context, owner *User, repo *Repository, refName string) error {
return db.mirrorSyncAction(ctx, ActionMirrorSyncDelete, owner, repo, refName, nil)
}
func (db *actions) MergePullRequest(ctx context.Context, doer, owner *User, repo *Repository, pull *Issue) error {
return db.notifyWatchers(ctx,
&Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: ActionMergePullRequest,
Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
RepoID: repo.ID,
RepoUserName: owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
},
)
}
func (db *actions) TransferRepo(ctx context.Context, doer, oldOwner, newOwner *User, repo *Repository) error {
return db.notifyWatchers(ctx,
&Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: ActionTransferRepo,
RepoID: repo.ID,
RepoUserName: newOwner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
Content: oldOwner.Name + "/" + repo.Name,
},
)
}
var (
// Same as GitHub, see https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue
issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
issueReopenKeywords = []string{"reopen", "reopens", "reopened"}
issueCloseKeywordsPattern = lazyregexp.New(assembleKeywordsPattern(issueCloseKeywords))
issueReopenKeywordsPattern = lazyregexp.New(assembleKeywordsPattern(issueReopenKeywords))
issueReferencePattern = lazyregexp.New(`(?i)(?:)(^| )\S*#\d+`)
)
func assembleKeywordsPattern(words []string) string {
return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
}
// updateCommitReferencesToIssues checks if issues are manipulated by commit message.
func updateCommitReferencesToIssues(doer *User, repo *Repository, commits []*PushCommit) error {
trimRightNonDigits := func(c rune) bool {
return !unicode.IsDigit(c)
}
// Commits are appended in the reverse order.
for i := len(commits) - 1; i >= 0; i-- {
c := commits[i]
refMarked := make(map[int64]bool)
for _, ref := range issueReferencePattern.FindAllString(c.Message, -1) {
ref = strings.TrimSpace(ref)
ref = strings.TrimRightFunc(ref, trimRightNonDigits)
if ref == "" {
continue
}
// Add repo name if missing
if ref[0] == '#' {
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
} else if !strings.Contains(ref, "/") {
// FIXME: We don't support User#ID syntax yet
continue
}
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
if refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
msgLines := strings.Split(c.Message, "\n")
shortMsg := msgLines[0]
if len(msgLines) > 2 {
shortMsg += "..."
}
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, shortMsg)
if err = CreateRefComment(doer, repo, issue, message, c.Sha1); err != nil {
return err
}
}
refMarked = make(map[int64]bool)
// FIXME: Can merge this and the next for loop to a common function.
for _, ref := range issueCloseKeywordsPattern.FindAllString(c.Message, -1) {
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
ref = strings.TrimRightFunc(ref, trimRightNonDigits)
if ref == "" {
continue
}
// Add repo name if missing
if ref[0] == '#' {
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
} else if !strings.Contains(ref, "/") {
// FIXME: We don't support User#ID syntax yet
continue
}
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
if refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
if issue.RepoID != repo.ID || issue.IsClosed {
continue
}
if err = issue.ChangeStatus(doer, repo, true); err != nil {
return err
}
}
// It is conflict to have close and reopen at same time, so refsMarkd doesn't need to reinit here.
for _, ref := range issueReopenKeywordsPattern.FindAllString(c.Message, -1) {
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
ref = strings.TrimRightFunc(ref, trimRightNonDigits)
if ref == "" {
continue
}
// Add repo name if missing
if ref[0] == '#' {
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
} else if !strings.Contains(ref, "/") {
// We don't support User#ID syntax yet
// return ErrNotImplemented
continue
}
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
continue
}
return err
}
if refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
if issue.RepoID != repo.ID || !issue.IsClosed {
continue
}
if err = issue.ChangeStatus(doer, repo, false); err != nil {
return err
}
}
}
return nil
}
type CommitRepoOptions struct {
Owner *User
Repo *Repository
PusherName string
RefFullName string
OldCommitID string
NewCommitID string
Commits *PushCommits
}
func (db *actions) CommitRepo(ctx context.Context, opts CommitRepoOptions) error {
err := NewReposStore(db.DB).Touch(ctx, opts.Repo.ID)
if err != nil {
return errors.Wrap(err, "touch repository")
}
pusher, err := NewUsersStore(db.DB).GetByUsername(ctx, opts.PusherName)
if err != nil {
return errors.Wrapf(err, "get pusher [name: %s]", opts.PusherName)
}
isNewRef := opts.OldCommitID == git.EmptyID
isDelRef := opts.NewCommitID == git.EmptyID
// If not the first commit, set the compare URL.
if !isNewRef && !isDelRef {
opts.Commits.CompareURL = repoutil.CompareCommitsPath(opts.Owner.Name, opts.Repo.Name, opts.OldCommitID, opts.NewCommitID)
}
refName := git.RefShortName(opts.RefFullName)
action := &Action{
ActUserID: pusher.ID,
ActUserName: pusher.Name,
RepoID: opts.Repo.ID,
RepoUserName: opts.Owner.Name,
RepoName: opts.Repo.Name,
RefName: refName,
IsPrivate: opts.Repo.IsPrivate || opts.Repo.IsUnlisted,
}
apiRepo := opts.Repo.APIFormat(opts.Owner)
apiPusher := pusher.APIFormat()
if isDelRef {
err = PrepareWebhooks(
opts.Repo,
HOOK_EVENT_DELETE,
&api.DeletePayload{
Ref: refName,
RefType: "branch",
PusherType: api.PUSHER_TYPE_USER,
Repo: apiRepo,
Sender: apiPusher,
},
)
if err != nil {
return errors.Wrap(err, "prepare webhooks for delete branch")
}
action.OpType = ActionDeleteBranch
err = db.notifyWatchers(ctx, action)
if err != nil {
return errors.Wrap(err, "notify watchers")
}
// Delete branch doesn't have anything to push or compare
return nil
}
// Only update issues via commits when internal issue tracker is enabled
if opts.Repo.EnableIssues && !opts.Repo.EnableExternalTracker {
if err = updateCommitReferencesToIssues(pusher, opts.Repo, opts.Commits.Commits); err != nil {
log.Error("update commit references to issues: %v", err)
}
}
if conf.UI.FeedMaxCommitNum > 0 && len(opts.Commits.Commits) > conf.UI.FeedMaxCommitNum {
opts.Commits.Commits = opts.Commits.Commits[:conf.UI.FeedMaxCommitNum]
}
data, err := jsoniter.Marshal(opts.Commits)
if err != nil {
return errors.Wrap(err, "marshal JSON")
}
action.Content = string(data)
var compareURL string
if isNewRef {
err = PrepareWebhooks(
opts.Repo,
HOOK_EVENT_CREATE,
&api.CreatePayload{
Ref: refName,
RefType: "branch",
DefaultBranch: opts.Repo.DefaultBranch,
Repo: apiRepo,
Sender: apiPusher,
},
)
if err != nil {
return errors.Wrap(err, "prepare webhooks for new branch")
}
action.OpType = ActionCreateBranch
err = db.notifyWatchers(ctx, action)
if err != nil {
return errors.Wrap(err, "notify watchers")
}
} else {
compareURL = conf.Server.ExternalURL + opts.Commits.CompareURL
}
commits, err := opts.Commits.APIFormat(ctx,
NewUsersStore(db.DB),
repoutil.RepositoryPath(opts.Owner.Name, opts.Repo.Name),
repoutil.HTMLURL(opts.Owner.Name, opts.Repo.Name),
)
if err != nil {
return errors.Wrap(err, "convert commits to API format")
}
err = PrepareWebhooks(
opts.Repo,
HOOK_EVENT_PUSH,
&api.PushPayload{
Ref: opts.RefFullName,
Before: opts.OldCommitID,
After: opts.NewCommitID,
CompareURL: compareURL,
Commits: commits,
Repo: apiRepo,
Pusher: apiPusher,
Sender: apiPusher,
},
)
if err != nil {
return errors.Wrap(err, "prepare webhooks for new commit")
}
action.OpType = ActionCommitRepo
err = db.notifyWatchers(ctx, action)
if err != nil {
return errors.Wrap(err, "notify watchers")
}
return nil
}
type PushTagOptions struct {
Owner *User
Repo *Repository
PusherName string
RefFullName string
NewCommitID string
}
func (db *actions) PushTag(ctx context.Context, opts PushTagOptions) error {
err := NewReposStore(db.DB).Touch(ctx, opts.Repo.ID)
if err != nil {
return errors.Wrap(err, "touch repository")
}
pusher, err := NewUsersStore(db.DB).GetByUsername(ctx, opts.PusherName)
if err != nil {
return errors.Wrapf(err, "get pusher [name: %s]", opts.PusherName)
}
refName := git.RefShortName(opts.RefFullName)
action := &Action{
ActUserID: pusher.ID,
ActUserName: pusher.Name,
RepoID: opts.Repo.ID,
RepoUserName: opts.Owner.Name,
RepoName: opts.Repo.Name,
RefName: refName,
IsPrivate: opts.Repo.IsPrivate || opts.Repo.IsUnlisted,
}
apiRepo := opts.Repo.APIFormat(opts.Owner)
apiPusher := pusher.APIFormat()
if opts.NewCommitID == git.EmptyID {
err = PrepareWebhooks(
opts.Repo,
HOOK_EVENT_DELETE,
&api.DeletePayload{
Ref: refName,
RefType: "tag",
PusherType: api.PUSHER_TYPE_USER,
Repo: apiRepo,
Sender: apiPusher,
},
)
if err != nil {
return errors.Wrap(err, "prepare webhooks for delete tag")
}
action.OpType = ActionDeleteTag
err = db.notifyWatchers(ctx, action)
if err != nil {
return errors.Wrap(err, "notify watchers")
}
return nil
}
err = PrepareWebhooks(
opts.Repo,
HOOK_EVENT_CREATE,
&api.CreatePayload{
Ref: refName,
RefType: "tag",
Sha: opts.NewCommitID,
DefaultBranch: opts.Repo.DefaultBranch,
Repo: apiRepo,
Sender: apiPusher,
},
)
if err != nil {
return errors.Wrapf(err, "prepare webhooks for new tag")
}
action.OpType = ActionPushTag
err = db.notifyWatchers(ctx, action)
if err != nil {
return errors.Wrap(err, "notify watchers")
}
return nil
}
// ActionType is the type of an action.
type ActionType int
// ⚠️ WARNING: Only append to the end of list to maintain backward compatibility.
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
ActionCreateBranch // 16
ActionDeleteBranch // 17
ActionDeleteTag // 18
ActionForkRepo // 19
ActionMirrorSyncPush // 20
ActionMirrorSyncCreate // 21
ActionMirrorSyncDelete // 22
)
// Action is a user operation to a repository. It implements template.Actioner
// interface to be able to use it in template rendering.
type Action struct {
ID int64 `gorm:"primaryKey"`
UserID int64 `gorm:"index"` // Receiver user ID
OpType ActionType
ActUserID int64 // Doer user ID
ActUserName string // Doer user name
ActAvatar string `xorm:"-" gorm:"-" json:"-"`
RepoID int64 `xorm:"INDEX" gorm:"index"`
RepoUserName string
RepoName string
RefName string
IsPrivate bool `xorm:"NOT NULL DEFAULT false" gorm:"not null;default:FALSE"`
Content string `xorm:"TEXT"`
Created time.Time `xorm:"-" gorm:"-" json:"-"`
CreatedUnix int64
}
// BeforeCreate implements the GORM create hook.
func (a *Action) BeforeCreate(tx *gorm.DB) error {
if a.CreatedUnix <= 0 {
a.CreatedUnix = tx.NowFunc().Unix()
}
return nil
}
// AfterFind implements the GORM query hook.
func (a *Action) AfterFind(_ *gorm.DB) error {
a.Created = time.Unix(a.CreatedUnix, 0).Local()
return nil
}
func (a *Action) GetOpType() int {
return int(a.OpType)
}
func (a *Action) GetActUserName() string {
return a.ActUserName
}
func (a *Action) ShortActUserName() string {
return strutil.Ellipsis(a.ActUserName, 20)
}
func (a *Action) GetRepoUserName() string {
return a.RepoUserName
}
func (a *Action) ShortRepoUserName() string {
return strutil.Ellipsis(a.RepoUserName, 20)
}
func (a *Action) GetRepoName() string {
return a.RepoName
}
func (a *Action) ShortRepoName() string {
return strutil.Ellipsis(a.RepoName, 33)
}
func (a *Action) GetRepoPath() string {
return path.Join(a.RepoUserName, a.RepoName)
}
func (a *Action) ShortRepoPath() string {
return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
}
func (a *Action) GetRepoLink() string {
if conf.Server.Subpath != "" {
return path.Join(conf.Server.Subpath, a.GetRepoPath())
}
return "/" + a.GetRepoPath()
}
func (a *Action) GetBranch() string {
return a.RefName
}
func (a *Action) GetContent() string {
return a.Content
}
func (a *Action) GetCreate() time.Time {
return a.Created
}
func (a *Action) GetIssueInfos() []string {
return strings.SplitN(a.Content, "|", 2)
}
func (a *Action) GetIssueTitle() string {
index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
issue, err := GetIssueByIndex(a.RepoID, index)
if err != nil {
log.Error("Failed to get issue title [repo_id: %d, index: %d]: %v", a.RepoID, index, err)
return "error getting issue"
}
return issue.Title
}
func (a *Action) GetIssueContent() string {
index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
issue, err := GetIssueByIndex(a.RepoID, index)
if err != nil {
log.Error("Failed to get issue content [repo_id: %d, index: %d]: %v", a.RepoID, index, err)
return "error getting issue"
}
return issue.Content
}
// PushCommit contains information of a pushed commit.
type PushCommit struct {
Sha1 string
Message string
AuthorEmail string
AuthorName string
CommitterEmail string
CommitterName string
Timestamp time.Time
}
// PushCommits is a list of pushed commits.
type PushCommits struct {
Len int
Commits []*PushCommit
CompareURL string
avatars map[string]string
}
// NewPushCommits returns a new PushCommits.
func NewPushCommits() *PushCommits {
return &PushCommits{
avatars: make(map[string]string),
}
}
func (pcs *PushCommits) APIFormat(ctx context.Context, usersStore UsersStore, repoPath, repoURL string) ([]*api.PayloadCommit, error) {
// NOTE: We cache query results in case there are many commits in a single push.
usernameByEmail := make(map[string]string)
getUsernameByEmail := func(email string) (string, error) {
username, ok := usernameByEmail[email]
if ok {
return username, nil
}
user, err := usersStore.GetByEmail(ctx, email)
if err != nil {
if IsErrUserNotExist(err) {
usernameByEmail[email] = ""
return "", nil
}
return "", err
}
usernameByEmail[email] = user.Name
return user.Name, nil
}
commits := make([]*api.PayloadCommit, len(pcs.Commits))
for i, commit := range pcs.Commits {
authorUsername, err := getUsernameByEmail(commit.AuthorEmail)
if err != nil {
return nil, errors.Wrap(err, "get author username")
}
committerUsername, err := getUsernameByEmail(commit.CommitterEmail)
if err != nil {
return nil, errors.Wrap(err, "get committer username")
}
nameStatus := &git.NameStatus{}
if !testutil.InTest {
nameStatus, err = git.ShowNameStatus(repoPath, commit.Sha1)
if err != nil {
return nil, errors.Wrapf(err, "show name status [commit_sha1: %s]", commit.Sha1)
}
}
commits[i] = &api.PayloadCommit{
ID: commit.Sha1,
Message: commit.Message,
URL: fmt.Sprintf("%s/commit/%s", repoURL, 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: nameStatus.Added,
Removed: nameStatus.Removed,
Modified: nameStatus.Modified,
Timestamp: commit.Timestamp,
}
}
return commits, nil
}
// AvatarLink tries to match user in database with email in order to show custom
// avatars, and falls back to general avatar link.
//
// FIXME: This method does not belong to PushCommits, should be a pure template
// function.
func (pcs *PushCommits) AvatarLink(email string) string {
_, ok := pcs.avatars[email]
if !ok {
u, err := Users.GetByEmail(context.Background(), email)
if err != nil {
pcs.avatars[email] = tool.AvatarLink(email)
if !IsErrUserNotExist(err) {
log.Error("Failed to get user [email: %s]: %v", email, err)
}
} else {
pcs.avatars[email] = u.RelAvatarLink()
}
}
return pcs.avatars[email]
}

872
internal/db/actions_test.go Normal file
View File

@@ -0,0 +1,872 @@
// Copyright 2022 The Gogs 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 db
import (
"context"
"os"
"testing"
"time"
"github.com/gogs/git-module"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/dbtest"
)
func TestIssueReferencePattern(t *testing.T) {
tests := []struct {
name string
message string
want []string
}{
{
name: "no match",
message: "Hello world!",
want: nil,
},
{
name: "contains issue numbers",
message: "#123 is fixed, and #456 is WIP",
want: []string{"#123", " #456"},
},
{
name: "contains full issue references",
message: "#123 is fixed, and user/repo#456 is WIP",
want: []string{"#123", " user/repo#456"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := issueReferencePattern.FindAllString(test.message, -1)
assert.Equal(t, test.want, got)
})
}
}
func TestAction_BeforeCreate(t *testing.T) {
now := time.Now()
db := &gorm.DB{
Config: &gorm.Config{
SkipDefaultTransaction: true,
NowFunc: func() time.Time {
return now
},
},
}
t.Run("CreatedUnix has been set", func(t *testing.T) {
action := &Action{CreatedUnix: 1}
_ = action.BeforeCreate(db)
assert.Equal(t, int64(1), action.CreatedUnix)
})
t.Run("CreatedUnix has not been set", func(t *testing.T) {
action := &Action{}
_ = action.BeforeCreate(db)
assert.Equal(t, db.NowFunc().Unix(), action.CreatedUnix)
})
}
func TestActions(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(Action), new(User), new(Repository), new(EmailAddress), new(Watch)}
db := &actions{
DB: dbtest.NewDB(t, "actions", tables...),
}
for _, tc := range []struct {
name string
test func(*testing.T, *actions)
}{
{"CommitRepo", actionsCommitRepo},
{"ListByOrganization", actionsListByOrganization},
{"ListByUser", actionsListByUser},
{"MergePullRequest", actionsMergePullRequest},
{"MirrorSyncCreate", actionsMirrorSyncCreate},
{"MirrorSyncDelete", actionsMirrorSyncDelete},
{"MirrorSyncPush", actionsMirrorSyncPush},
{"NewRepo", actionsNewRepo},
{"PushTag", actionsPushTag},
{"RenameRepo", actionsRenameRepo},
{"TransferRepo", actionsTransferRepo},
} {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
err := clearTables(t, db.DB, tables...)
require.NoError(t, err)
})
tc.test(t, db)
})
if t.Failed() {
break
}
}
}
func actionsCommitRepo(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
now := time.Unix(1588568886, 0).UTC()
t.Run("new commit", func(t *testing.T) {
t.Cleanup(func() {
err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).WithContext(ctx).Delete(new(Action)).Error
require.NoError(t, err)
})
err = db.CommitRepo(ctx,
CommitRepoOptions{
PusherName: alice.Name,
Owner: alice,
Repo: repo,
RefFullName: "refs/heads/main",
OldCommitID: "ca82a6dff817ec66f44342007202690a93763949",
NewCommitID: "085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7",
Commits: CommitsToPushCommits(
[]*git.Commit{
{
ID: git.MustIDFromString("085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7"),
Author: &git.Signature{
Name: "alice",
Email: "alice@example.com",
When: now,
},
Committer: &git.Signature{
Name: "alice",
Email: "alice@example.com",
When: now,
},
Message: "A random commit",
},
},
),
},
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionCommitRepo,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "main",
IsPrivate: false,
Content: `{"Len":1,"Commits":[{"Sha1":"085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7","Message":"A random commit","AuthorEmail":"alice@example.com","AuthorName":"alice","CommitterEmail":"alice@example.com","CommitterName":"alice","Timestamp":"2020-05-04T05:08:06Z"}],"CompareURL":"alice/example/compare/ca82a6dff817ec66f44342007202690a93763949...085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7"}`,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
})
t.Run("new ref", func(t *testing.T) {
t.Cleanup(func() {
err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).WithContext(ctx).Delete(new(Action)).Error
require.NoError(t, err)
})
err = db.CommitRepo(ctx,
CommitRepoOptions{
PusherName: alice.Name,
Owner: alice,
Repo: repo,
RefFullName: "refs/heads/main",
OldCommitID: git.EmptyID,
NewCommitID: "085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7",
Commits: CommitsToPushCommits(
[]*git.Commit{
{
ID: git.MustIDFromString("085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7"),
Author: &git.Signature{
Name: "alice",
Email: "alice@example.com",
When: now,
},
Committer: &git.Signature{
Name: "alice",
Email: "alice@example.com",
When: now,
},
Message: "A random commit",
},
},
),
},
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 2)
got[0].ID = 0
got[1].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionCommitRepo,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "main",
IsPrivate: false,
Content: `{"Len":1,"Commits":[{"Sha1":"085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7","Message":"A random commit","AuthorEmail":"alice@example.com","AuthorName":"alice","CommitterEmail":"alice@example.com","CommitterName":"alice","Timestamp":"2020-05-04T05:08:06Z"}],"CompareURL":""}`,
CreatedUnix: db.NowFunc().Unix(),
},
{
UserID: alice.ID,
OpType: ActionCreateBranch,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "main",
IsPrivate: false,
Content: `{"Len":1,"Commits":[{"Sha1":"085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7","Message":"A random commit","AuthorEmail":"alice@example.com","AuthorName":"alice","CommitterEmail":"alice@example.com","CommitterName":"alice","Timestamp":"2020-05-04T05:08:06Z"}],"CompareURL":""}`,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
want[1].Created = time.Unix(want[1].CreatedUnix, 0)
assert.Equal(t, want, got)
})
t.Run("delete ref", func(t *testing.T) {
t.Cleanup(func() {
err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).WithContext(ctx).Delete(new(Action)).Error
require.NoError(t, err)
})
err = db.CommitRepo(ctx,
CommitRepoOptions{
PusherName: alice.Name,
Owner: alice,
Repo: repo,
RefFullName: "refs/heads/main",
OldCommitID: "ca82a6dff817ec66f44342007202690a93763949",
NewCommitID: git.EmptyID,
},
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionDeleteBranch,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "main",
IsPrivate: false,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
})
}
func actionsListByOrganization(t *testing.T, db *actions) {
if os.Getenv("GOGS_DATABASE_TYPE") != "postgres" {
t.Skip("Skipping testing with not using PostgreSQL")
return
}
ctx := context.Background()
conf.SetMockUI(t,
conf.UIOpts{
User: conf.UIUserOpts{
NewsFeedPagingNum: 20,
},
},
)
tests := []struct {
name string
orgID int64
actorID int64
afterID int64
want string
}{
{
name: "no afterID",
orgID: 1,
actorID: 1,
afterID: 0,
want: `SELECT * FROM "action" WHERE user_id = 1 AND (true OR id < 0) AND repo_id IN (SELECT repository.id FROM "repository" JOIN team_repo ON repository.id = team_repo.repo_id WHERE team_repo.team_id IN (SELECT team_id FROM "team_user" WHERE team_user.org_id = 1 AND uid = 1) OR (repository.is_private = false AND repository.is_unlisted = false)) ORDER BY id DESC LIMIT 20`,
},
{
name: "has afterID",
orgID: 1,
actorID: 1,
afterID: 5,
want: `SELECT * FROM "action" WHERE user_id = 1 AND (false OR id < 5) AND repo_id IN (SELECT repository.id FROM "repository" JOIN team_repo ON repository.id = team_repo.repo_id WHERE team_repo.team_id IN (SELECT team_id FROM "team_user" WHERE team_user.org_id = 1 AND uid = 1) OR (repository.is_private = false AND repository.is_unlisted = false)) ORDER BY id DESC LIMIT 20`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := db.DB.ToSQL(func(tx *gorm.DB) *gorm.DB {
return NewActionsStore(tx).(*actions).listByOrganization(ctx, test.orgID, test.actorID, test.afterID).Find(new(Action))
})
assert.Equal(t, test.want, got)
})
}
}
func actionsListByUser(t *testing.T, db *actions) {
if os.Getenv("GOGS_DATABASE_TYPE") != "postgres" {
t.Skip("Skipping testing with not using PostgreSQL")
return
}
ctx := context.Background()
conf.SetMockUI(t,
conf.UIOpts{
User: conf.UIUserOpts{
NewsFeedPagingNum: 20,
},
},
)
tests := []struct {
name string
userID int64
actorID int64
afterID int64
isProfile bool
want string
}{
{
name: "same user no afterID not in profile",
userID: 1,
actorID: 1,
afterID: 0,
isProfile: false,
want: `SELECT * FROM "action" WHERE user_id = 1 AND (true OR id < 0) AND (true OR (is_private = false AND act_user_id = 1)) ORDER BY id DESC LIMIT 20`,
},
{
name: "same user no afterID in profile",
userID: 1,
actorID: 1,
afterID: 0,
isProfile: true,
want: `SELECT * FROM "action" WHERE user_id = 1 AND (true OR id < 0) AND (true OR (is_private = false AND act_user_id = 1)) ORDER BY id DESC LIMIT 20`,
},
{
name: "same user has afterID not in profile",
userID: 1,
actorID: 1,
afterID: 5,
isProfile: false,
want: `SELECT * FROM "action" WHERE user_id = 1 AND (false OR id < 5) AND (true OR (is_private = false AND act_user_id = 1)) ORDER BY id DESC LIMIT 20`,
},
{
name: "different user no afterID in profile",
userID: 1,
actorID: 2,
afterID: 0,
isProfile: true,
want: `SELECT * FROM "action" WHERE user_id = 1 AND (true OR id < 0) AND (false OR (is_private = false AND act_user_id = 1)) ORDER BY id DESC LIMIT 20`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := db.DB.ToSQL(func(tx *gorm.DB) *gorm.DB {
return NewActionsStore(tx).(*actions).listByUser(ctx, test.userID, test.actorID, test.afterID, test.isProfile).Find(new(Action))
})
assert.Equal(t, test.want, got)
})
}
}
func actionsMergePullRequest(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
err = db.MergePullRequest(ctx,
alice,
alice,
repo,
&Issue{
Index: 1,
Title: "Fix issue 1",
},
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionMergePullRequest,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
IsPrivate: false,
Content: `1|Fix issue 1`,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
}
func actionsMirrorSyncCreate(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
err = db.MirrorSyncCreate(ctx,
alice,
repo,
"main",
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionMirrorSyncCreate,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "main",
IsPrivate: false,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
}
func actionsMirrorSyncDelete(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
err = db.MirrorSyncDelete(ctx,
alice,
repo,
"main",
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionMirrorSyncDelete,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "main",
IsPrivate: false,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
}
func actionsMirrorSyncPush(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
now := time.Unix(1588568886, 0).UTC()
err = db.MirrorSyncPush(ctx,
MirrorSyncPushOptions{
Owner: alice,
Repo: repo,
RefName: "main",
OldCommitID: "ca82a6dff817ec66f44342007202690a93763949",
NewCommitID: "085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7",
Commits: CommitsToPushCommits(
[]*git.Commit{
{
ID: git.MustIDFromString("085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7"),
Author: &git.Signature{
Name: "alice",
Email: "alice@example.com",
When: now,
},
Committer: &git.Signature{
Name: "alice",
Email: "alice@example.com",
When: now,
},
Message: "A random commit",
},
},
),
},
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionMirrorSyncPush,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "main",
IsPrivate: false,
Content: `{"Len":1,"Commits":[{"Sha1":"085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7","Message":"A random commit","AuthorEmail":"alice@example.com","AuthorName":"alice","CommitterEmail":"alice@example.com","CommitterName":"alice","Timestamp":"2020-05-04T05:08:06Z"}],"CompareURL":"alice/example/compare/ca82a6dff817ec66f44342007202690a93763949...085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7"}`,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
}
func actionsNewRepo(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
t.Run("new repo", func(t *testing.T) {
t.Cleanup(func() {
err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).WithContext(ctx).Delete(new(Action)).Error
require.NoError(t, err)
})
err = db.NewRepo(ctx, alice, alice, repo)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionCreateRepo,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
IsPrivate: false,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
})
t.Run("fork repo", func(t *testing.T) {
t.Cleanup(func() {
err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).WithContext(ctx).Delete(new(Action)).Error
require.NoError(t, err)
})
repo.IsFork = true
err = db.NewRepo(ctx, alice, alice, repo)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionForkRepo,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
IsPrivate: false,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
})
}
func actionsPushTag(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
t.Run("new tag", func(t *testing.T) {
t.Cleanup(func() {
err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).WithContext(ctx).Delete(new(Action)).Error
require.NoError(t, err)
})
err = db.PushTag(ctx,
PushTagOptions{
Owner: alice,
Repo: repo,
PusherName: alice.Name,
RefFullName: "refs/tags/v1.0.0",
NewCommitID: "085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7",
},
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionPushTag,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "v1.0.0",
IsPrivate: false,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
})
t.Run("delete tag", func(t *testing.T) {
t.Cleanup(func() {
err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).WithContext(ctx).Delete(new(Action)).Error
require.NoError(t, err)
})
err = db.PushTag(ctx,
PushTagOptions{
Owner: alice,
Repo: repo,
PusherName: alice.Name,
RefFullName: "refs/tags/v1.0.0",
NewCommitID: git.EmptyID,
},
)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionDeleteTag,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
RefName: "v1.0.0",
IsPrivate: false,
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
})
}
func actionsRenameRepo(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
err = db.RenameRepo(ctx, alice, alice, "oldExample", repo)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionRenameRepo,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: alice.Name,
RepoName: repo.Name,
IsPrivate: false,
Content: "oldExample",
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
}
func actionsTransferRepo(t *testing.T, db *actions) {
ctx := context.Background()
alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
require.NoError(t, err)
bob, err := NewUsersStore(db.DB).Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
require.NoError(t, err)
repo, err := NewReposStore(db.DB).Create(ctx,
alice.ID,
CreateRepoOptions{
Name: "example",
},
)
require.NoError(t, err)
err = db.TransferRepo(ctx, alice, alice, bob, repo)
require.NoError(t, err)
got, err := db.ListByUser(ctx, alice.ID, alice.ID, 0, false)
require.NoError(t, err)
require.Len(t, got, 1)
got[0].ID = 0
want := []*Action{
{
UserID: alice.ID,
OpType: ActionTransferRepo,
ActUserID: alice.ID,
ActUserName: alice.Name,
RepoID: repo.ID,
RepoUserName: bob.Name,
RepoName: repo.Name,
IsPrivate: false,
Content: "alice/example",
CreatedUnix: db.NowFunc().Unix(),
},
}
want[0].Created = time.Unix(want[0].CreatedUnix, 0)
assert.Equal(t, want, got)
}

View File

@@ -221,7 +221,7 @@ func importTable(ctx context.Context, db *gorm.DB, table interface{}, r io.Reade
// PostgreSQL needs manually reset table sequence for auto increment keys
if conf.UsePostgreSQL && !skipResetIDSeq[rawTableName] {
seqName := rawTableName + "_id_seq"
if _, err = x.Context(ctx).Exec(fmt.Sprintf(`SELECT setval('%s', COALESCE((SELECT MAX(id)+1 FROM "%s"), 1), false);`, seqName, rawTableName)); err != nil {
if err = db.WithContext(ctx).Exec(fmt.Sprintf(`SELECT setval('%s', COALESCE((SELECT MAX(id)+1 FROM "%s"), 1), false)`, seqName, rawTableName)).Error; err != nil {
return errors.Wrapf(err, "reset table %q.%q", rawTableName, seqName)
}
}

View File

@@ -29,11 +29,10 @@ func TestDumpAndImport(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
if len(Tables) != 4 {
t.Fatalf("New table has added (want 4 got %d), please add new tests for the table and update this check", len(Tables))
if len(Tables) != 5 {
t.Fatalf("New table has added (want 5 got %d), please add new tests for the table and update this check", len(Tables))
}
db := dbtest.NewDB(t, "dumpAndImport", Tables...)
@@ -90,6 +89,48 @@ func setupDBToDump(t *testing.T, db *gorm.DB) {
CreatedUnix: 1588568886,
},
&Action{
ID: 1,
UserID: 1,
OpType: ActionCreateBranch,
ActUserID: 1,
ActUserName: "alice",
RepoID: 1,
RepoUserName: "alice",
RepoName: "example",
RefName: "main",
IsPrivate: false,
Content: `{"Len":1,"Commits":[],"CompareURL":""}`,
CreatedUnix: 1588568886,
},
&Action{
ID: 2,
UserID: 1,
OpType: ActionCommitRepo,
ActUserID: 1,
ActUserName: "alice",
RepoID: 1,
RepoUserName: "alice",
RepoName: "example",
RefName: "main",
IsPrivate: false,
Content: `{"Len":1,"Commits":[],"CompareURL":""}`,
CreatedUnix: 1588568886,
},
&Action{
ID: 3,
UserID: 1,
OpType: ActionDeleteBranch,
ActUserID: 1,
ActUserName: "alice",
RepoID: 1,
RepoUserName: "alice",
RepoName: "example",
RefName: "main",
IsPrivate: false,
CreatedUnix: 1588568886,
},
&LFSObject{
RepoID: 1,
OID: "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",

View File

@@ -172,11 +172,11 @@ func (cmt *Comment) mailParticipants(e Engine, opType ActionType, issue *Issue)
}
switch opType {
case ACTION_COMMENT_ISSUE:
case ActionCommentIssue:
issue.Content = cmt.Content
case ACTION_CLOSE_ISSUE:
case ActionCloseIssue:
issue.Content = fmt.Sprintf("Closed #%d", issue.Index)
case ACTION_REOPEN_ISSUE:
case ActionReopenIssue:
issue.Content = fmt.Sprintf("Reopened #%d", issue.Index)
}
if err = mailIssueCommentToParticipants(issue, cmt.Poster, mentions); err != nil {
@@ -216,7 +216,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
// Check comment type.
switch opts.Type {
case COMMENT_TYPE_COMMENT:
act.OpType = ACTION_COMMENT_ISSUE
act.OpType = ActionCommentIssue
if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
return nil, err
@@ -245,9 +245,9 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
}
case COMMENT_TYPE_REOPEN:
act.OpType = ACTION_REOPEN_ISSUE
act.OpType = ActionReopenIssue
if opts.Issue.IsPull {
act.OpType = ACTION_REOPEN_PULL_REQUEST
act.OpType = ActionReopenPullRequest
}
if opts.Issue.IsPull {
@@ -260,9 +260,9 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
}
case COMMENT_TYPE_CLOSE:
act.OpType = ACTION_CLOSE_ISSUE
act.OpType = ActionCloseIssue
if opts.Issue.IsPull {
act.OpType = ACTION_CLOSE_PULL_REQUEST
act.OpType = ActionClosePullRequest
}
if opts.Issue.IsPull {
@@ -353,7 +353,7 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri
Action: api.HOOK_ISSUE_COMMENT_CREATED,
Issue: issue.APIFormat(),
Comment: comment.APIFormat(),
Repository: repo.APIFormat(nil),
Repository: repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
@@ -494,7 +494,7 @@ func UpdateComment(doer *User, c *Comment, oldContent string) (err error) {
From: oldContent,
},
},
Repository: c.Issue.Repo.APIFormat(nil),
Repository: c.Issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks [comment_id: %d]: %v", c.ID, err)
@@ -544,7 +544,7 @@ func DeleteCommentByID(doer *User, id int64) error {
Action: api.HOOK_ISSUE_COMMENT_DELETED,
Issue: comment.Issue.APIFormat(),
Comment: comment.APIFormat(),
Repository: comment.Issue.Repo.APIFormat(nil),
Repository: comment.Issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)

View File

@@ -41,7 +41,7 @@ func newLogWriter() (logger.Writer, error) {
//
// NOTE: Lines are sorted in alphabetical order, each letter in its own line.
var Tables = []interface{}{
new(Access), new(AccessToken),
new(Access), new(AccessToken), new(Action),
new(LFSObject), new(LoginSource),
}
@@ -119,12 +119,14 @@ func Init(w logger.Writer) (*gorm.DB, error) {
// Initialize stores, sorted in alphabetical order.
AccessTokens = &accessTokens{DB: db}
Actions = NewActionsStore(db)
LoginSources = &loginSources{DB: db, files: sourceFiles}
LFS = &lfs{DB: db}
Perms = &perms{DB: db}
Repos = &repos{DB: db}
Repos = NewReposStore(db)
TwoFactors = &twoFactors{DB: db}
Users = &users{DB: db}
Users = NewUsersStore(db)
Watches = NewWatchesStore(db)
return db, nil
}

View File

@@ -237,7 +237,7 @@ func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
Action: api.HOOK_ISSUE_LABEL_UPDATED,
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
} else {
@@ -245,7 +245,7 @@ func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
Action: api.HOOK_ISSUE_LABEL_UPDATED,
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
}
@@ -350,7 +350,7 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
Action: api.HOOK_ISSUE_LABEL_CLEARED,
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
} else {
@@ -358,7 +358,7 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
Action: api.HOOK_ISSUE_LABEL_CLEARED,
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
}
@@ -477,7 +477,7 @@ func (issue *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (e
apiPullRequest := &api.PullRequestPayload{
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: repo.APIFormat(nil),
Repository: repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}
if isClosed {
@@ -490,7 +490,7 @@ func (issue *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (e
apiIssues := &api.IssuesPayload{
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: repo.APIFormat(nil),
Repository: repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}
if isClosed {
@@ -525,7 +525,7 @@ func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
From: oldTitle,
},
},
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
} else {
@@ -538,7 +538,7 @@ func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
From: oldTitle,
},
},
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
}
@@ -567,7 +567,7 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
From: oldContent,
},
},
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
} else {
@@ -580,7 +580,7 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
From: oldContent,
},
},
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
}
@@ -610,7 +610,7 @@ func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
apiPullRequest := &api.PullRequestPayload{
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}
if isRemoveAssignee {
@@ -623,7 +623,7 @@ func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
apiIssues := &api.IssuesPayload{
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}
if isRemoveAssignee {
@@ -763,7 +763,7 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
if err = NotifyWatchers(&Action{
ActUserID: issue.Poster.ID,
ActUserName: issue.Poster.Name,
OpType: ACTION_CREATE_ISSUE,
OpType: ActionCreateIssue,
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
@@ -780,7 +780,7 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
Action: api.HOOK_ISSUE_OPENED,
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: repo.APIFormat(nil),
Repository: repo.APIFormatLegacy(nil),
Sender: issue.Poster.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks: %v", err)

View File

@@ -33,8 +33,8 @@ var LFS LFSStore
// LFSObject is the relation between an LFS object and a repository.
type LFSObject struct {
RepoID int64 `gorm:"primary_key;auto_increment:false"`
OID lfsutil.OID `gorm:"primary_key;column:oid"`
RepoID int64 `gorm:"primaryKey;auto_increment:false"`
OID lfsutil.OID `gorm:"primaryKey;column:oid"`
Size int64 `gorm:"not null"`
Storage lfsutil.Storage `gorm:"not null"`
CreatedAt time.Time `gorm:"not null"`

View File

@@ -21,7 +21,6 @@ func TestLFS(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(LFSObject)}

View File

@@ -33,7 +33,7 @@ type loginSourceFilesStore interface {
// Len returns number of login sources.
Len() int
// List returns a list of login sources filtered by options.
List(opts ListLoginSourceOpts) []*LoginSource
List(opts ListLoginSourceOptions) []*LoginSource
// Update updates in-memory copy of the authentication source.
Update(source *LoginSource)
}
@@ -85,7 +85,7 @@ func (s *loginSourceFiles) Len() int {
return len(s.sources)
}
func (s *loginSourceFiles) List(opts ListLoginSourceOpts) []*LoginSource {
func (s *loginSourceFiles) List(opts ListLoginSourceOptions) []*LoginSource {
s.RLock()
defer s.RUnlock()

View File

@@ -53,12 +53,12 @@ func TestLoginSourceFiles_List(t *testing.T) {
}
t.Run("list all sources", func(t *testing.T) {
sources := store.List(ListLoginSourceOpts{})
sources := store.List(ListLoginSourceOptions{})
assert.Equal(t, 2, len(sources), "number of sources")
})
t.Run("list only activated sources", func(t *testing.T) {
sources := store.List(ListLoginSourceOpts{OnlyActivated: true})
sources := store.List(ListLoginSourceOptions{OnlyActivated: true})
assert.Equal(t, 1, len(sources), "number of sources")
assert.Equal(t, int64(101), sources[0].ID)
})

View File

@@ -28,7 +28,7 @@ import (
type LoginSourcesStore interface {
// Create creates a new login source and persist to database. It returns
// ErrLoginSourceAlreadyExist when a login source with same name already exists.
Create(ctx context.Context, opts CreateLoginSourceOpts) (*LoginSource, error)
Create(ctx context.Context, opts CreateLoginSourceOptions) (*LoginSource, error)
// Count returns the total number of login sources.
Count(ctx context.Context) int64
// DeleteByID deletes a login source by given ID. It returns ErrLoginSourceInUse
@@ -38,7 +38,7 @@ type LoginSourcesStore interface {
// ErrLoginSourceNotExist when not found.
GetByID(ctx context.Context, id int64) (*LoginSource, error)
// List returns a list of login sources filtered by options.
List(ctx context.Context, opts ListLoginSourceOpts) ([]*LoginSource, error)
List(ctx context.Context, opts ListLoginSourceOptions) ([]*LoginSource, error)
// ResetNonDefault clears default flag for all the other login sources.
ResetNonDefault(ctx context.Context, source *LoginSource) error
// Save persists all values of given login source to database or local file. The
@@ -50,7 +50,7 @@ var LoginSources LoginSourcesStore
// LoginSource represents an external way for authorizing users.
type LoginSource struct {
ID int64
ID int64 `gorm:"primaryKey"`
Type auth.Type
Name string `xorm:"UNIQUE" gorm:"UNIQUE"`
IsActived bool `xorm:"NOT NULL DEFAULT false" gorm:"NOT NULL"`
@@ -189,7 +189,7 @@ type loginSources struct {
files loginSourceFilesStore
}
type CreateLoginSourceOpts struct {
type CreateLoginSourceOptions struct {
Type auth.Type
Name string
Activated bool
@@ -210,7 +210,7 @@ func (err ErrLoginSourceAlreadyExist) Error() string {
return fmt.Sprintf("login source already exists: %v", err.args)
}
func (db *loginSources) Create(ctx context.Context, opts CreateLoginSourceOpts) (*LoginSource, error) {
func (db *loginSources) Create(ctx context.Context, opts CreateLoginSourceOptions) (*LoginSource, error) {
err := db.WithContext(ctx).Where("name = ?", opts.Name).First(new(LoginSource)).Error
if err == nil {
return nil, ErrLoginSourceAlreadyExist{args: errutil.Args{"name": opts.Name}}
@@ -274,12 +274,12 @@ func (db *loginSources) GetByID(ctx context.Context, id int64) (*LoginSource, er
return source, nil
}
type ListLoginSourceOpts struct {
type ListLoginSourceOptions struct {
// Whether to only include activated login sources.
OnlyActivated bool
}
func (db *loginSources) List(ctx context.Context, opts ListLoginSourceOpts) ([]*LoginSource, error) {
func (db *loginSources) List(ctx context.Context, opts ListLoginSourceOptions) ([]*LoginSource, error) {
var sources []*LoginSource
query := db.WithContext(ctx).Order("id ASC")
if opts.OnlyActivated {
@@ -303,7 +303,7 @@ func (db *loginSources) ResetNonDefault(ctx context.Context, dflt *LoginSource)
return err
}
for _, source := range db.files.List(ListLoginSourceOpts{}) {
for _, source := range db.files.List(ListLoginSourceOptions{}) {
if source.File != nil && source.ID != dflt.ID {
source.File.SetGeneral("is_default", "false")
if err = source.File.Save(); err != nil {

View File

@@ -81,7 +81,6 @@ func Test_loginSources(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(LoginSource), new(User)}
@@ -119,7 +118,7 @@ func loginSourcesCreate(t *testing.T, db *loginSources) {
// Create first login source with name "GitHub"
source, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.GitHub,
Name: "GitHub",
Activated: true,
@@ -138,7 +137,7 @@ func loginSourcesCreate(t *testing.T, db *loginSources) {
assert.Equal(t, db.NowFunc().Format(time.RFC3339), source.Updated.UTC().Format(time.RFC3339))
// Try create second login source with same name should fail
_, err = db.Create(ctx, CreateLoginSourceOpts{Name: source.Name})
_, err = db.Create(ctx, CreateLoginSourceOptions{Name: source.Name})
wantErr := ErrLoginSourceAlreadyExist{args: errutil.Args{"name": source.Name}}
assert.Equal(t, wantErr, err)
}
@@ -148,7 +147,7 @@ func loginSourcesCount(t *testing.T, db *loginSources) {
// Create two login sources, one in database and one as source file.
_, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.GitHub,
Name: "GitHub",
Activated: true,
@@ -172,7 +171,7 @@ func loginSourcesDeleteByID(t *testing.T, db *loginSources) {
t.Run("delete but in used", func(t *testing.T) {
source, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.GitHub,
Name: "GitHub",
Activated: true,
@@ -186,7 +185,7 @@ func loginSourcesDeleteByID(t *testing.T, db *loginSources) {
// Create a user that uses this login source
_, err = (&users{DB: db.DB}).Create(ctx, "alice", "",
CreateUserOpts{
CreateUserOptions{
LoginSource: source.ID,
},
)
@@ -206,7 +205,7 @@ func loginSourcesDeleteByID(t *testing.T, db *loginSources) {
// Create a login source with name "GitHub2"
source, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.GitHub,
Name: "GitHub2",
Activated: true,
@@ -254,7 +253,7 @@ func loginSourcesGetByID(t *testing.T, db *loginSources) {
// Create a login source with name "GitHub"
source, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.GitHub,
Name: "GitHub",
Activated: true,
@@ -278,7 +277,7 @@ func loginSourcesList(t *testing.T, db *loginSources) {
ctx := context.Background()
mock := NewMockLoginSourceFilesStore()
mock.ListFunc.SetDefaultHook(func(opts ListLoginSourceOpts) []*LoginSource {
mock.ListFunc.SetDefaultHook(func(opts ListLoginSourceOptions) []*LoginSource {
if opts.OnlyActivated {
return []*LoginSource{
{ID: 1},
@@ -293,7 +292,7 @@ func loginSourcesList(t *testing.T, db *loginSources) {
// Create two login sources in database, one activated and the other one not
_, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.PAM,
Name: "PAM",
Config: &pam.Config{
@@ -303,7 +302,7 @@ func loginSourcesList(t *testing.T, db *loginSources) {
)
require.NoError(t, err)
_, err = db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.GitHub,
Name: "GitHub",
Activated: true,
@@ -315,12 +314,12 @@ func loginSourcesList(t *testing.T, db *loginSources) {
require.NoError(t, err)
// List all login sources
sources, err := db.List(ctx, ListLoginSourceOpts{})
sources, err := db.List(ctx, ListLoginSourceOptions{})
require.NoError(t, err)
assert.Equal(t, 4, len(sources), "number of sources")
// Only list activated login sources
sources, err = db.List(ctx, ListLoginSourceOpts{OnlyActivated: true})
sources, err = db.List(ctx, ListLoginSourceOptions{OnlyActivated: true})
require.NoError(t, err)
assert.Equal(t, 2, len(sources), "number of sources")
}
@@ -329,7 +328,7 @@ func loginSourcesResetNonDefault(t *testing.T, db *loginSources) {
ctx := context.Background()
mock := NewMockLoginSourceFilesStore()
mock.ListFunc.SetDefaultHook(func(opts ListLoginSourceOpts) []*LoginSource {
mock.ListFunc.SetDefaultHook(func(opts ListLoginSourceOptions) []*LoginSource {
mockFile := NewMockLoginSourceFileStore()
mockFile.SetGeneralFunc.SetDefaultHook(func(name, value string) {
assert.Equal(t, "is_default", name)
@@ -345,7 +344,7 @@ func loginSourcesResetNonDefault(t *testing.T, db *loginSources) {
// Create two login sources both have default on
source1, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.PAM,
Name: "PAM",
Default: true,
@@ -356,7 +355,7 @@ func loginSourcesResetNonDefault(t *testing.T, db *loginSources) {
)
require.NoError(t, err)
source2, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.GitHub,
Name: "GitHub",
Activated: true,
@@ -388,7 +387,7 @@ func loginSourcesSave(t *testing.T, db *loginSources) {
t.Run("save to database", func(t *testing.T) {
// Create a login source with name "GitHub"
source, err := db.Create(ctx,
CreateLoginSourceOpts{
CreateLoginSourceOptions{
Type: auth.GitHub,
Name: "GitHub",
Activated: true,

View File

@@ -54,6 +54,8 @@ var migrations = []Migration{
// v19 -> v20:v0.13.0
NewMigration("migrate access tokens to store SHA56", migrateAccessTokenToSHA256),
// v20 -> v21:v0.13.0
NewMigration("add index to action.user_id", addIndexToActionUserID),
}
// Migrate migrates the database schema and/or data to the current version.

View File

@@ -0,0 +1,19 @@
// Copyright 2022 The Gogs 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 (
"gorm.io/gorm"
)
func addIndexToActionUserID(db *gorm.DB) error {
type action struct {
UserID string `gorm:"index"`
}
if db.Migrator().HasIndex(&action{}, "UserID") {
return nil
}
return db.Migrator().CreateIndex(&action{}, "UserID")
}

View File

@@ -0,0 +1,82 @@
// Copyright 2022 The Gogs 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 (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gogs.io/gogs/internal/dbtest"
)
type actionPreV21 struct {
ID int64 `gorm:"primaryKey"`
UserID int64
OpType int
ActUserID int64
ActUserName string
RepoID int64 `gorm:"index"`
RepoUserName string
RepoName string
RefName string
IsPrivate bool `gorm:"not null;default:FALSE"`
Content string
CreatedUnix int64
}
func (*actionPreV21) TableName() string {
return "action"
}
type actionV21 struct {
ID int64 `gorm:"primaryKey"`
UserID int64 `gorm:"index"`
OpType int
ActUserID int64
ActUserName string
RepoID int64 `gorm:"index"`
RepoUserName string
RepoName string
RefName string
IsPrivate bool `gorm:"not null;default:FALSE"`
Content string
CreatedUnix int64
}
func (*actionV21) TableName() string {
return "action"
}
func TestAddIndexToActionUserID(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
db := dbtest.NewDB(t, "addIndexToActionUserID", new(actionPreV21))
err := db.Create(
&actionPreV21{
ID: 1,
UserID: 1,
OpType: 1,
ActUserID: 1,
ActUserName: "alice",
RepoID: 1,
RepoUserName: "alice",
RepoName: "example",
RefName: "main",
IsPrivate: false,
CreatedUnix: db.NowFunc().Unix(),
},
).Error
require.NoError(t, err)
assert.False(t, db.Migrator().HasIndex(&actionV21{}, "UserID"))
err = addIndexToActionUserID(db)
require.NoError(t, err)
assert.True(t, db.Migrator().HasIndex(&actionV21{}, "UserID"))
}

View File

@@ -363,7 +363,7 @@ func ChangeMilestoneAssign(doer *User, issue *Issue, oldMilestoneID int64) (err
Action: hookAction,
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
} else {
@@ -371,7 +371,7 @@ func ChangeMilestoneAssign(doer *User, issue *Issue, oldMilestoneID int64) (err
Action: hookAction,
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: issue.Repo.APIFormat(nil),
Repository: issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
})
}

View File

@@ -5,6 +5,7 @@
package db
import (
"context"
"fmt"
"net/url"
"strings"
@@ -314,6 +315,8 @@ func MirrorUpdate() {
// SyncMirrors checks and syncs mirrors.
// TODO: sync more mirrors at same time.
func SyncMirrors() {
ctx := context.Background()
// Start listening on new sync requests.
for repoID := range MirrorQueue.Queue() {
log.Trace("SyncMirrors [repo_id: %s]", repoID)
@@ -358,8 +361,8 @@ func SyncMirrors() {
// Delete reference
if result.newCommitID == gitShortEmptyID {
if err = MirrorSyncDeleteAction(m.Repo, result.refName); err != nil {
log.Error("MirrorSyncDeleteAction [repo_id: %d]: %v", m.RepoID, err)
if err = Actions.MirrorSyncDelete(ctx, m.Repo.MustOwner(), m.Repo, result.refName); err != nil {
log.Error("Failed to create action for mirror sync delete [repo_id: %d]: %v", m.RepoID, err)
}
continue
}
@@ -367,8 +370,8 @@ func SyncMirrors() {
// New reference
isNewRef := false
if result.oldCommitID == gitShortEmptyID {
if err = MirrorSyncCreateAction(m.Repo, result.refName); err != nil {
log.Error("MirrorSyncCreateAction [repo_id: %d]: %v", m.RepoID, err)
if err = Actions.MirrorSyncCreate(ctx, m.Repo.MustOwner(), m.Repo, result.refName); err != nil {
log.Error("Failed to create action for mirror sync create [repo_id: %d]: %v", m.RepoID, err)
continue
}
isNewRef = true
@@ -416,13 +419,18 @@ func SyncMirrors() {
newCommitID = refNewCommit.ID.String()
}
if err = MirrorSyncPushAction(m.Repo, MirrorSyncPushActionOptions{
RefName: result.refName,
OldCommitID: oldCommitID,
NewCommitID: newCommitID,
Commits: CommitsToPushCommits(commits),
}); err != nil {
log.Error("MirrorSyncPushAction [repo_id: %d]: %v", m.RepoID, err)
err = Actions.MirrorSyncPush(ctx,
MirrorSyncPushOptions{
Owner: m.Repo.MustOwner(),
Repo: m.Repo,
RefName: result.refName,
OldCommitID: oldCommitID,
NewCommitID: newCommitID,
Commits: CommitsToPushCommits(commits),
},
)
if err != nil {
log.Error("Failed to create action for mirror sync push [repo_id: %d]: %v", m.RepoID, err)
continue
}
}

View File

@@ -51,7 +51,7 @@ func NewMockLoginSourcesStore() *MockLoginSourcesStore {
},
},
CreateFunc: &LoginSourcesStoreCreateFunc{
defaultHook: func(context.Context, CreateLoginSourceOpts) (r0 *LoginSource, r1 error) {
defaultHook: func(context.Context, CreateLoginSourceOptions) (r0 *LoginSource, r1 error) {
return
},
},
@@ -66,7 +66,7 @@ func NewMockLoginSourcesStore() *MockLoginSourcesStore {
},
},
ListFunc: &LoginSourcesStoreListFunc{
defaultHook: func(context.Context, ListLoginSourceOpts) (r0 []*LoginSource, r1 error) {
defaultHook: func(context.Context, ListLoginSourceOptions) (r0 []*LoginSource, r1 error) {
return
},
},
@@ -94,7 +94,7 @@ func NewStrictMockLoginSourcesStore() *MockLoginSourcesStore {
},
},
CreateFunc: &LoginSourcesStoreCreateFunc{
defaultHook: func(context.Context, CreateLoginSourceOpts) (*LoginSource, error) {
defaultHook: func(context.Context, CreateLoginSourceOptions) (*LoginSource, error) {
panic("unexpected invocation of MockLoginSourcesStore.Create")
},
},
@@ -109,7 +109,7 @@ func NewStrictMockLoginSourcesStore() *MockLoginSourcesStore {
},
},
ListFunc: &LoginSourcesStoreListFunc{
defaultHook: func(context.Context, ListLoginSourceOpts) ([]*LoginSource, error) {
defaultHook: func(context.Context, ListLoginSourceOptions) ([]*LoginSource, error) {
panic("unexpected invocation of MockLoginSourcesStore.List")
},
},
@@ -260,15 +260,15 @@ func (c LoginSourcesStoreCountFuncCall) Results() []interface{} {
// LoginSourcesStoreCreateFunc describes the behavior when the Create method
// of the parent MockLoginSourcesStore instance is invoked.
type LoginSourcesStoreCreateFunc struct {
defaultHook func(context.Context, CreateLoginSourceOpts) (*LoginSource, error)
hooks []func(context.Context, CreateLoginSourceOpts) (*LoginSource, error)
defaultHook func(context.Context, CreateLoginSourceOptions) (*LoginSource, error)
hooks []func(context.Context, CreateLoginSourceOptions) (*LoginSource, error)
history []LoginSourcesStoreCreateFuncCall
mutex sync.Mutex
}
// Create delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockLoginSourcesStore) Create(v0 context.Context, v1 CreateLoginSourceOpts) (*LoginSource, error) {
func (m *MockLoginSourcesStore) Create(v0 context.Context, v1 CreateLoginSourceOptions) (*LoginSource, error) {
r0, r1 := m.CreateFunc.nextHook()(v0, v1)
m.CreateFunc.appendCall(LoginSourcesStoreCreateFuncCall{v0, v1, r0, r1})
return r0, r1
@@ -277,7 +277,7 @@ func (m *MockLoginSourcesStore) Create(v0 context.Context, v1 CreateLoginSourceO
// SetDefaultHook sets function that is called when the Create method of the
// parent MockLoginSourcesStore instance is invoked and the hook queue is
// empty.
func (f *LoginSourcesStoreCreateFunc) SetDefaultHook(hook func(context.Context, CreateLoginSourceOpts) (*LoginSource, error)) {
func (f *LoginSourcesStoreCreateFunc) SetDefaultHook(hook func(context.Context, CreateLoginSourceOptions) (*LoginSource, error)) {
f.defaultHook = hook
}
@@ -285,7 +285,7 @@ func (f *LoginSourcesStoreCreateFunc) SetDefaultHook(hook func(context.Context,
// Create method of the parent MockLoginSourcesStore instance invokes the
// hook at the front of the queue and discards it. After the queue is empty,
// the default hook function is invoked for any future action.
func (f *LoginSourcesStoreCreateFunc) PushHook(hook func(context.Context, CreateLoginSourceOpts) (*LoginSource, error)) {
func (f *LoginSourcesStoreCreateFunc) PushHook(hook func(context.Context, CreateLoginSourceOptions) (*LoginSource, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
@@ -294,19 +294,19 @@ func (f *LoginSourcesStoreCreateFunc) PushHook(hook func(context.Context, Create
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *LoginSourcesStoreCreateFunc) SetDefaultReturn(r0 *LoginSource, r1 error) {
f.SetDefaultHook(func(context.Context, CreateLoginSourceOpts) (*LoginSource, error) {
f.SetDefaultHook(func(context.Context, CreateLoginSourceOptions) (*LoginSource, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *LoginSourcesStoreCreateFunc) PushReturn(r0 *LoginSource, r1 error) {
f.PushHook(func(context.Context, CreateLoginSourceOpts) (*LoginSource, error) {
f.PushHook(func(context.Context, CreateLoginSourceOptions) (*LoginSource, error) {
return r0, r1
})
}
func (f *LoginSourcesStoreCreateFunc) nextHook() func(context.Context, CreateLoginSourceOpts) (*LoginSource, error) {
func (f *LoginSourcesStoreCreateFunc) nextHook() func(context.Context, CreateLoginSourceOptions) (*LoginSource, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
@@ -344,7 +344,7 @@ type LoginSourcesStoreCreateFuncCall struct {
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 CreateLoginSourceOpts
Arg1 CreateLoginSourceOptions
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 *LoginSource
@@ -582,15 +582,15 @@ func (c LoginSourcesStoreGetByIDFuncCall) Results() []interface{} {
// LoginSourcesStoreListFunc describes the behavior when the List method of
// the parent MockLoginSourcesStore instance is invoked.
type LoginSourcesStoreListFunc struct {
defaultHook func(context.Context, ListLoginSourceOpts) ([]*LoginSource, error)
hooks []func(context.Context, ListLoginSourceOpts) ([]*LoginSource, error)
defaultHook func(context.Context, ListLoginSourceOptions) ([]*LoginSource, error)
hooks []func(context.Context, ListLoginSourceOptions) ([]*LoginSource, error)
history []LoginSourcesStoreListFuncCall
mutex sync.Mutex
}
// List delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockLoginSourcesStore) List(v0 context.Context, v1 ListLoginSourceOpts) ([]*LoginSource, error) {
func (m *MockLoginSourcesStore) List(v0 context.Context, v1 ListLoginSourceOptions) ([]*LoginSource, error) {
r0, r1 := m.ListFunc.nextHook()(v0, v1)
m.ListFunc.appendCall(LoginSourcesStoreListFuncCall{v0, v1, r0, r1})
return r0, r1
@@ -599,7 +599,7 @@ func (m *MockLoginSourcesStore) List(v0 context.Context, v1 ListLoginSourceOpts)
// SetDefaultHook sets function that is called when the List method of the
// parent MockLoginSourcesStore instance is invoked and the hook queue is
// empty.
func (f *LoginSourcesStoreListFunc) SetDefaultHook(hook func(context.Context, ListLoginSourceOpts) ([]*LoginSource, error)) {
func (f *LoginSourcesStoreListFunc) SetDefaultHook(hook func(context.Context, ListLoginSourceOptions) ([]*LoginSource, error)) {
f.defaultHook = hook
}
@@ -607,7 +607,7 @@ func (f *LoginSourcesStoreListFunc) SetDefaultHook(hook func(context.Context, Li
// List method of the parent MockLoginSourcesStore instance invokes the hook
// at the front of the queue and discards it. After the queue is empty, the
// default hook function is invoked for any future action.
func (f *LoginSourcesStoreListFunc) PushHook(hook func(context.Context, ListLoginSourceOpts) ([]*LoginSource, error)) {
func (f *LoginSourcesStoreListFunc) PushHook(hook func(context.Context, ListLoginSourceOptions) ([]*LoginSource, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
@@ -616,19 +616,19 @@ func (f *LoginSourcesStoreListFunc) PushHook(hook func(context.Context, ListLogi
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *LoginSourcesStoreListFunc) SetDefaultReturn(r0 []*LoginSource, r1 error) {
f.SetDefaultHook(func(context.Context, ListLoginSourceOpts) ([]*LoginSource, error) {
f.SetDefaultHook(func(context.Context, ListLoginSourceOptions) ([]*LoginSource, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *LoginSourcesStoreListFunc) PushReturn(r0 []*LoginSource, r1 error) {
f.PushHook(func(context.Context, ListLoginSourceOpts) ([]*LoginSource, error) {
f.PushHook(func(context.Context, ListLoginSourceOptions) ([]*LoginSource, error) {
return r0, r1
})
}
func (f *LoginSourcesStoreListFunc) nextHook() func(context.Context, ListLoginSourceOpts) ([]*LoginSource, error) {
func (f *LoginSourcesStoreListFunc) nextHook() func(context.Context, ListLoginSourceOptions) ([]*LoginSource, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
@@ -666,7 +666,7 @@ type LoginSourcesStoreListFuncCall struct {
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 ListLoginSourceOpts
Arg1 ListLoginSourceOptions
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 []*LoginSource
@@ -1328,7 +1328,7 @@ func NewMockLoginSourceFilesStore() *MockLoginSourceFilesStore {
},
},
ListFunc: &LoginSourceFilesStoreListFunc{
defaultHook: func(ListLoginSourceOpts) (r0 []*LoginSource) {
defaultHook: func(ListLoginSourceOptions) (r0 []*LoginSource) {
return
},
},
@@ -1356,7 +1356,7 @@ func NewStrictMockLoginSourceFilesStore() *MockLoginSourceFilesStore {
},
},
ListFunc: &LoginSourceFilesStoreListFunc{
defaultHook: func(ListLoginSourceOpts) []*LoginSource {
defaultHook: func(ListLoginSourceOptions) []*LoginSource {
panic("unexpected invocation of MockLoginSourceFilesStore.List")
},
},
@@ -1374,7 +1374,7 @@ func NewStrictMockLoginSourceFilesStore() *MockLoginSourceFilesStore {
type surrogateMockLoginSourceFilesStore interface {
GetByID(int64) (*LoginSource, error)
Len() int
List(ListLoginSourceOpts) []*LoginSource
List(ListLoginSourceOptions) []*LoginSource
Update(*LoginSource)
}
@@ -1605,15 +1605,15 @@ func (c LoginSourceFilesStoreLenFuncCall) Results() []interface{} {
// LoginSourceFilesStoreListFunc describes the behavior when the List method
// of the parent MockLoginSourceFilesStore instance is invoked.
type LoginSourceFilesStoreListFunc struct {
defaultHook func(ListLoginSourceOpts) []*LoginSource
hooks []func(ListLoginSourceOpts) []*LoginSource
defaultHook func(ListLoginSourceOptions) []*LoginSource
hooks []func(ListLoginSourceOptions) []*LoginSource
history []LoginSourceFilesStoreListFuncCall
mutex sync.Mutex
}
// List delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockLoginSourceFilesStore) List(v0 ListLoginSourceOpts) []*LoginSource {
func (m *MockLoginSourceFilesStore) List(v0 ListLoginSourceOptions) []*LoginSource {
r0 := m.ListFunc.nextHook()(v0)
m.ListFunc.appendCall(LoginSourceFilesStoreListFuncCall{v0, r0})
return r0
@@ -1622,7 +1622,7 @@ func (m *MockLoginSourceFilesStore) List(v0 ListLoginSourceOpts) []*LoginSource
// SetDefaultHook sets function that is called when the List method of the
// parent MockLoginSourceFilesStore instance is invoked and the hook queue
// is empty.
func (f *LoginSourceFilesStoreListFunc) SetDefaultHook(hook func(ListLoginSourceOpts) []*LoginSource) {
func (f *LoginSourceFilesStoreListFunc) SetDefaultHook(hook func(ListLoginSourceOptions) []*LoginSource) {
f.defaultHook = hook
}
@@ -1630,7 +1630,7 @@ func (f *LoginSourceFilesStoreListFunc) SetDefaultHook(hook func(ListLoginSource
// List method of the parent MockLoginSourceFilesStore instance invokes the
// hook at the front of the queue and discards it. After the queue is empty,
// the default hook function is invoked for any future action.
func (f *LoginSourceFilesStoreListFunc) PushHook(hook func(ListLoginSourceOpts) []*LoginSource) {
func (f *LoginSourceFilesStoreListFunc) PushHook(hook func(ListLoginSourceOptions) []*LoginSource) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
@@ -1639,19 +1639,19 @@ func (f *LoginSourceFilesStoreListFunc) PushHook(hook func(ListLoginSourceOpts)
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *LoginSourceFilesStoreListFunc) SetDefaultReturn(r0 []*LoginSource) {
f.SetDefaultHook(func(ListLoginSourceOpts) []*LoginSource {
f.SetDefaultHook(func(ListLoginSourceOptions) []*LoginSource {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *LoginSourceFilesStoreListFunc) PushReturn(r0 []*LoginSource) {
f.PushHook(func(ListLoginSourceOpts) []*LoginSource {
f.PushHook(func(ListLoginSourceOptions) []*LoginSource {
return r0
})
}
func (f *LoginSourceFilesStoreListFunc) nextHook() func(ListLoginSourceOpts) []*LoginSource {
func (f *LoginSourceFilesStoreListFunc) nextHook() func(ListLoginSourceOptions) []*LoginSource {
f.mutex.Lock()
defer f.mutex.Unlock()
@@ -1686,7 +1686,7 @@ func (f *LoginSourceFilesStoreListFunc) History() []LoginSourceFilesStoreListFun
type LoginSourceFilesStoreListFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 ListLoginSourceOpts
Arg0 ListLoginSourceOptions
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 []*LoginSource

View File

@@ -52,7 +52,7 @@ func init() {
legacyTables = append(legacyTables,
new(User), new(PublicKey), new(TwoFactor), new(TwoFactorRecoveryCode),
new(Repository), new(DeployKey), new(Collaboration), new(Upload),
new(Watch), new(Star), new(Follow), new(Action),
new(Watch), new(Star), new(Follow),
new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
new(Label), new(IssueLabel), new(Milestone),
new(Mirror), new(Release), new(Webhook), new(HookTask),

View File

@@ -32,7 +32,7 @@ var Perms PermsStore
// In case of an organization repository, the members of the owners team are in
// this table.
type Access struct {
ID int64
ID int64 `gorm:"primaryKey"`
UserID int64 `xorm:"UNIQUE(s)" gorm:"uniqueIndex:access_user_repo_unique;not null"`
RepoID int64 `xorm:"UNIQUE(s)" gorm:"uniqueIndex:access_user_repo_unique;not null"`
Mode AccessMode `gorm:"not null"`

View File

@@ -18,7 +18,6 @@ func TestPerms(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(Access)}

View File

@@ -5,6 +5,7 @@
package db
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -138,7 +139,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
Name: "deleted",
}
} else {
apiHeadRepo = pr.HeadRepo.APIFormat(nil)
apiHeadRepo = pr.HeadRepo.APIFormatLegacy(nil)
}
apiIssue := pr.Issue.APIFormat()
@@ -156,7 +157,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
HeadBranch: pr.HeadBranch,
HeadRepo: apiHeadRepo,
BaseBranch: pr.BaseBranch,
BaseRepo: pr.BaseRepo.APIFormat(nil),
BaseRepo: pr.BaseRepo.APIFormatLegacy(nil),
HTMLURL: pr.Issue.HTMLURL(),
HasMerged: pr.HasMerged,
}
@@ -195,6 +196,8 @@ const (
// Merge merges pull request to base repository.
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle MergeStyle, commitDescription string) (err error) {
ctx := context.TODO()
defer func() {
go HookQueue.Add(pr.BaseRepo.ID)
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false)
@@ -334,8 +337,8 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
return fmt.Errorf("Commit: %v", err)
}
if err = MergePullRequestAction(doer, pr.Issue.Repo, pr.Issue); err != nil {
log.Error("MergePullRequestAction [%d]: %v", pr.ID, err)
if err = Actions.MergePullRequest(ctx, doer, pr.Issue.Repo.Owner, pr.Issue.Repo, pr.Issue); err != nil {
log.Error("Failed to create action for merge pull request, pull_request_id: %d, error: %v", pr.ID, err)
}
// Reload pull request information.
@@ -347,7 +350,7 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
Action: api.HOOK_ISSUE_CLOSED,
Index: pr.Index,
PullRequest: pr.APIFormat(),
Repository: pr.Issue.Repo.APIFormat(nil),
Repository: pr.Issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
@@ -372,7 +375,7 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
commits = append([]*git.Commit{mergeCommit}, commits...)
}
pcs, err := CommitsToPushCommits(commits).ToApiPayloadCommits(pr.BaseRepo.RepoPath(), pr.BaseRepo.HTMLURL())
pcs, err := CommitsToPushCommits(commits).APIFormat(ctx, Users, pr.BaseRepo.RepoPath(), pr.BaseRepo.HTMLURL())
if err != nil {
log.Error("Failed to convert to API payload commits: %v", err)
return nil
@@ -384,7 +387,7 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
After: mergeCommit.ID.String(),
CompareURL: conf.Server.ExternalURL + pr.BaseRepo.ComposeCompareURL(pr.MergeBase, pr.MergedCommitID),
Commits: pcs,
Repo: pr.BaseRepo.APIFormat(nil),
Repo: pr.BaseRepo.APIFormatLegacy(nil),
Pusher: pr.HeadRepo.MustOwner().APIFormat(),
Sender: doer.APIFormat(),
}
@@ -487,7 +490,7 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
if err = NotifyWatchers(&Action{
ActUserID: pull.Poster.ID,
ActUserName: pull.Poster.Name,
OpType: ACTION_CREATE_PULL_REQUEST,
OpType: ActionCreatePullRequest,
Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
@@ -506,7 +509,7 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
Action: api.HOOK_ISSUE_OPENED,
Index: pull.Index,
PullRequest: pr.APIFormat(),
Repository: repo.APIFormat(nil),
Repository: repo.APIFormatLegacy(nil),
Sender: pull.Poster.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
@@ -798,7 +801,7 @@ func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool
Action: api.HOOK_ISSUE_SYNCHRONIZED,
Index: pr.Issue.Index,
PullRequest: pr.Issue.PullRequest.APIFormat(),
Repository: pr.Issue.Repo.APIFormat(nil),
Repository: pr.Issue.Repo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err)

View File

@@ -153,7 +153,7 @@ func (r *Release) preparePublishWebhooks() {
if err := PrepareWebhooks(r.Repo, HOOK_EVENT_RELEASE, &api.ReleasePayload{
Action: api.HOOK_RELEASE_PUBLISHED,
Release: r.APIFormat(),
Repository: r.Repo.APIFormat(nil),
Repository: r.Repo.APIFormatLegacy(nil),
Sender: r.Publisher.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks: %v", err)

View File

@@ -21,6 +21,7 @@ import (
"time"
"github.com/nfnt/resize"
"github.com/pkg/errors"
"github.com/unknwon/cae/zip"
"github.com/unknwon/com"
"gopkg.in/ini.v1"
@@ -33,11 +34,12 @@ import (
embedConf "gogs.io/gogs/conf"
"gogs.io/gogs/internal/avatar"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db/errors"
dberrors "gogs.io/gogs/internal/db/errors"
"gogs.io/gogs/internal/errutil"
"gogs.io/gogs/internal/markup"
"gogs.io/gogs/internal/osutil"
"gogs.io/gogs/internal/process"
"gogs.io/gogs/internal/repoutil"
"gogs.io/gogs/internal/semverutil"
"gogs.io/gogs/internal/sync"
)
@@ -150,15 +152,15 @@ func NewRepoContext() {
// Repository contains information of a repository.
type Repository struct {
ID int64
OwnerID int64 `xorm:"UNIQUE(s)" gorm:"UNIQUE_INDEX:s"`
ID int64 `gorm:"primaryKey"`
OwnerID int64 `xorm:"UNIQUE(s)" gorm:"uniqueIndex:repo_owner_name_unique"`
Owner *User `xorm:"-" gorm:"-" json:"-"`
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL" gorm:"UNIQUE_INDEX:s"`
Name string `xorm:"INDEX NOT NULL" gorm:"NOT NULL"`
Description string `xorm:"VARCHAR(512)" gorm:"TYPE:VARCHAR(512)"`
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL" gorm:"uniqueIndex:repo_owner_name_unique;index;not null"`
Name string `xorm:"INDEX NOT NULL" gorm:"index;not null"`
Description string `xorm:"VARCHAR(512)" gorm:"type:VARCHAR(512)"`
Website string
DefaultBranch string
Size int64 `xorm:"NOT NULL DEFAULT 0" gorm:"NOT NULL;DEFAULT:0"`
Size int64 `xorm:"NOT NULL DEFAULT 0" gorm:"not null;default:0"`
UseCustomAvatar bool
// Counters
@@ -171,37 +173,37 @@ type Repository struct {
NumPulls int
NumClosedPulls int
NumOpenPulls int `xorm:"-" gorm:"-" json:"-"`
NumMilestones int `xorm:"NOT NULL DEFAULT 0" gorm:"NOT NULL;DEFAULT:0"`
NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0" gorm:"NOT NULL;DEFAULT:0"`
NumMilestones int `xorm:"NOT NULL DEFAULT 0" gorm:"not null;default:0"`
NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0" gorm:"not null;default:0"`
NumOpenMilestones int `xorm:"-" gorm:"-" json:"-"`
NumTags int `xorm:"-" gorm:"-" json:"-"`
IsPrivate bool
// TODO: When migrate to GORM, make sure to do a loose migration with `HasColumn` and `AddColumn`,
// see docs in https://gorm.io/docs/migration.html.
IsUnlisted bool `xorm:"NOT NULL DEFAULT false"`
IsUnlisted bool `xorm:"NOT NULL DEFAULT false" gorm:"not null;default:FALSE"`
IsBare bool
IsMirror bool
*Mirror `xorm:"-" gorm:"-" json:"-"`
// Advanced settings
EnableWiki bool `xorm:"NOT NULL DEFAULT true" gorm:"NOT NULL;DEFAULT:TRUE"`
EnableWiki bool `xorm:"NOT NULL DEFAULT true" gorm:"not null;default:TRUE"`
AllowPublicWiki bool
EnableExternalWiki bool
ExternalWikiURL string
EnableIssues bool `xorm:"NOT NULL DEFAULT true" gorm:"NOT NULL;DEFAULT:TRUE"`
EnableIssues bool `xorm:"NOT NULL DEFAULT true" gorm:"not null;default:TRUE"`
AllowPublicIssues bool
EnableExternalTracker bool
ExternalTrackerURL string
ExternalTrackerFormat string
ExternalTrackerStyle string
ExternalMetas map[string]string `xorm:"-" gorm:"-" json:"-"`
EnablePulls bool `xorm:"NOT NULL DEFAULT true" gorm:"NOT NULL;DEFAULT:TRUE"`
PullsIgnoreWhitespace bool `xorm:"NOT NULL DEFAULT false" gorm:"NOT NULL;DEFAULT:FALSE"`
PullsAllowRebase bool `xorm:"NOT NULL DEFAULT false" gorm:"NOT NULL;DEFAULT:FALSE"`
EnablePulls bool `xorm:"NOT NULL DEFAULT true" gorm:"not null;default:TRUE"`
PullsIgnoreWhitespace bool `xorm:"NOT NULL DEFAULT false" gorm:"not null;default:FALSE"`
PullsAllowRebase bool `xorm:"NOT NULL DEFAULT false" gorm:"not null;default:FALSE"`
IsFork bool `xorm:"NOT NULL DEFAULT false" gorm:"NOT NULL;DEFAULT:FALSE"`
IsFork bool `xorm:"NOT NULL DEFAULT false" gorm:"not null;default:FALSE"`
ForkID int64
BaseRepo *Repository `xorm:"-" gorm:"-" json:"-"`
@@ -290,6 +292,7 @@ func (repo *Repository) FullName() string {
return repo.MustOwner().Name + "/" + repo.Name
}
// Deprecated: Use repoutil.HTMLURL instead.
func (repo *Repository) HTMLURL() string {
return conf.Server.ExternalURL + repo.FullName()
}
@@ -356,7 +359,9 @@ func (repo *Repository) DeleteAvatar() error {
// This method assumes following fields have been assigned with valid values:
// Required - BaseRepo (if fork)
// Arguments that are allowed to be nil: permission
func (repo *Repository) APIFormat(permission *api.Permission, user ...*User) *api.Repository {
//
// Deprecated: Use APIFormat instead.
func (repo *Repository) APIFormatLegacy(permission *api.Permission, user ...*User) *api.Repository {
cloneLink := repo.CloneLink()
apiRepo := &api.Repository{
ID: repo.ID,
@@ -390,7 +395,7 @@ func (repo *Repository) APIFormat(permission *api.Permission, user ...*User) *ap
p.Admin = user[0].IsAdminOfRepo(repo)
p.Push = user[0].IsWriterOfRepo(repo)
}
apiRepo.Parent = repo.BaseRepo.APIFormat(p)
apiRepo.Parent = repo.BaseRepo.APIFormatLegacy(p)
}
return apiRepo
}
@@ -537,6 +542,7 @@ func (repo *Repository) repoPath(e Engine) string {
return RepoPath(repo.mustOwner(e).Name, repo.Name)
}
// Deprecated: Use repoutil.RepositoryPath instead.
func (repo *Repository) RepoPath() string {
return repo.repoPath(x)
}
@@ -553,6 +559,7 @@ func (repo *Repository) Link() string {
return conf.Server.Subpath + "/" + repo.FullName()
}
// Deprecated: Use repoutil.ComparePath instead.
func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string {
return fmt.Sprintf("%s/%s/compare/%s...%s", repo.MustOwner().Name, repo.Name, oldCommitID, newCommitID)
}
@@ -694,37 +701,28 @@ func IsRepositoryExist(u *User, repoName string) (bool, error) {
return isRepositoryExist(x, u, repoName)
}
// CloneLink represents different types of clone URLs of repository.
type CloneLink struct {
SSH string
HTTPS string
Git string
}
// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name.
func ComposeHTTPSCloneURL(owner, repo string) string {
return fmt.Sprintf("%s%s/%s.git", conf.Server.ExternalURL, owner, repo)
}
func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
// Deprecated: Use repoutil.NewCloneLink instead.
func (repo *Repository) cloneLink(isWiki bool) *repoutil.CloneLink {
repoName := repo.Name
if isWiki {
repoName += ".wiki"
}
repo.Owner = repo.MustOwner()
cl := new(CloneLink)
cl := new(repoutil.CloneLink)
if conf.SSH.Port != 22 {
cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", conf.App.RunUser, conf.SSH.Domain, conf.SSH.Port, repo.Owner.Name, repoName)
} else {
cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", conf.App.RunUser, conf.SSH.Domain, repo.Owner.Name, repoName)
}
cl.HTTPS = ComposeHTTPSCloneURL(repo.Owner.Name, repoName)
cl.HTTPS = repoutil.HTTPSCloneURL(repo.Owner.Name, repoName)
return cl
}
// CloneLink returns clone URLs of repository.
func (repo *Repository) CloneLink() (cl *CloneLink) {
//
// Deprecated: Use repoutil.NewCloneLink instead.
func (repo *Repository) CloneLink() (cl *repoutil.CloneLink) {
return repo.cloneLink(false)
}
@@ -758,7 +756,7 @@ func wikiRemoteURL(remote string) string {
// MigrateRepository migrates a existing repository from other project hosting.
func MigrateRepository(doer, owner *User, opts MigrateRepoOptions) (*Repository, error) {
repo, err := CreateRepository(doer, owner, CreateRepoOptions{
repo, err := CreateRepository(doer, owner, CreateRepoOptionsLegacy{
Name: opts.Name,
Description: opts.Description,
IsPrivate: opts.IsPrivate,
@@ -930,7 +928,7 @@ func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
return nil
}
type CreateRepoOptions struct {
type CreateRepoOptionsLegacy struct {
Name string
Description string
Gitignores string
@@ -953,7 +951,7 @@ func getRepoInitFile(tp, name string) ([]byte, error) {
return embedConf.Files.ReadFile(relPath)
}
func prepareRepoCommit(repo *Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
func prepareRepoCommit(repo *Repository, tmpDir, repoPath string, opts CreateRepoOptionsLegacy) error {
// Clone to temporary path and do the init commit.
_, stderr, err := process.Exec(
fmt.Sprintf("initRepository(git clone): %s", repoPath), "git", "clone", repoPath, tmpDir)
@@ -1016,7 +1014,7 @@ func prepareRepoCommit(repo *Repository, tmpDir, repoPath string, opts CreateRep
}
// initRepository performs initial commit with chosen setup files on behave of doer.
func initRepository(e Engine, repoPath string, doer *User, repo *Repository, opts CreateRepoOptions) (err error) {
func initRepository(e Engine, repoPath string, doer *User, repo *Repository, opts CreateRepoOptionsLegacy) (err error) {
// Somehow the directory could exist.
if com.IsExist(repoPath) {
return fmt.Errorf("initRepository: path already exists: %s", repoPath)
@@ -1116,7 +1114,29 @@ func createRepository(e *xorm.Session, doer, owner *User, repo *Repository) (err
if err = watchRepo(e, owner.ID, repo.ID, true); err != nil {
return fmt.Errorf("watchRepo: %v", err)
} else if err = newRepoAction(e, doer, owner, repo); err != nil {
}
// FIXME: This is identical to Actions.NewRepo but we are not yet able to wrap
// transaction with different ORM objects, should delete this once migrated to
// GORM for this part of logic.
newRepoAction := func(e Engine, doer *User, repo *Repository) (err error) {
opType := ActionCreateRepo
if repo.IsFork {
opType = ActionForkRepo
}
return notifyWatchers(e, &Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: opType,
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
CreatedUnix: time.Now().Unix(),
})
}
if err = newRepoAction(e, doer, repo); err != nil {
return fmt.Errorf("newRepoAction: %v", err)
}
@@ -1137,7 +1157,7 @@ func (err ErrReachLimitOfRepo) Error() string {
}
// CreateRepository creates a repository for given user or organization.
func CreateRepository(doer, owner *User, opts CreateRepoOptions) (_ *Repository, err error) {
func CreateRepository(doer, owner *User, opts CreateRepoOptionsLegacy) (_ *Repository, err error) {
if !owner.CanCreateRepo() {
return nil, ErrReachLimitOfRepo{Limit: owner.RepoCreationNum()}
}
@@ -1265,6 +1285,8 @@ func FilterRepositoryWithIssues(repoIDs []int64) ([]int64, error) {
}
// RepoPath returns repository path by given user and repository name.
//
// Deprecated: Use repoutil.RepositoryPath instead.
func RepoPath(userName, repoName string) string {
return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git")
}
@@ -1361,9 +1383,34 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
return fmt.Errorf("decrease old owner repository count: %v", err)
}
// Remove watch for organization.
if owner.IsOrganization() {
if err = watchRepo(sess, owner.ID, repo.ID, false); err != nil {
return errors.Wrap(err, "unwatch repository for the organization owner")
}
}
if err = watchRepo(sess, newOwner.ID, repo.ID, true); err != nil {
return fmt.Errorf("watchRepo: %v", err)
} else if err = transferRepoAction(sess, doer, owner, repo); err != nil {
}
// FIXME: This is identical to Actions.TransferRepo but we are not yet able to
// wrap transaction with different ORM objects, should delete this once migrated
// to GORM for this part of logic.
transferRepoAction := func(e Engine, doer, oldOwner *User, repo *Repository) error {
return notifyWatchers(e, &Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: ActionTransferRepo,
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate || repo.IsUnlisted,
Content: path.Join(oldOwner.Name, repo.Name),
CreatedUnix: time.Now().Unix(),
})
}
if err = transferRepoAction(sess, doer, owner, repo); err != nil {
return fmt.Errorf("transferRepoAction: %v", err)
}
@@ -1651,7 +1698,7 @@ func DeleteRepository(ownerID, repoID int64) error {
func GetRepositoryByRef(ref string) (*Repository, error) {
n := strings.IndexByte(ref, byte('/'))
if n < 2 {
return nil, errors.InvalidRepoReference{Ref: ref}
return nil, dberrors.InvalidRepoReference{Ref: ref}
}
userName, repoName := ref[:n], ref[n+1:]
@@ -2235,9 +2282,9 @@ func (repos MirrorRepositoryList) LoadAttributes() error {
// Watch is connection request for receiving repository notification.
type Watch struct {
ID int64
UserID int64 `xorm:"UNIQUE(watch)"`
RepoID int64 `xorm:"UNIQUE(watch)"`
ID int64 `gorm:"primaryKey"`
UserID int64 `xorm:"UNIQUE(watch)" gorm:"uniqueIndex:watch_user_repo_unique;not null"`
RepoID int64 `xorm:"UNIQUE(watch)" gorm:"uniqueIndex:watch_user_repo_unique;not null"`
}
func isWatching(e Engine, userID, repoID int64) bool {
@@ -2276,12 +2323,15 @@ func WatchRepo(userID, repoID int64, watch bool) (err error) {
return watchRepo(x, userID, repoID, watch)
}
// Deprecated: Use Repos.ListByRepo instead.
func getWatchers(e Engine, repoID int64) ([]*Watch, error) {
watches := make([]*Watch, 0, 10)
return watches, e.Find(&watches, &Watch{RepoID: repoID})
}
// GetWatchers returns all watchers of given repository.
//
// Deprecated: Use Repos.ListByRepo instead.
func GetWatchers(repoID int64) ([]*Watch, error) {
return getWatchers(x, repoID)
}
@@ -2298,7 +2348,12 @@ func (repo *Repository) GetWatchers(page int) ([]*User, error) {
return users, sess.Find(&users)
}
// Deprecated: Use Actions.notifyWatchers instead.
func notifyWatchers(e Engine, act *Action) error {
if act.CreatedUnix <= 0 {
act.CreatedUnix = time.Now().Unix()
}
// Add feeds for user self and all watchers.
watchers, err := getWatchers(e, act.RepoID)
if err != nil {
@@ -2329,6 +2384,8 @@ func notifyWatchers(e Engine, act *Action) error {
}
// NotifyWatchers creates batch of actions for every watcher.
//
// Deprecated: Use Actions.notifyWatchers instead.
func NotifyWatchers(act *Action) error {
return notifyWatchers(x, act)
}
@@ -2469,8 +2526,8 @@ func ForkRepository(doer, owner *User, baseRepo *Repository, name, desc string)
log.Error("UpdateSize [repo_id: %d]: %v", repo.ID, err)
}
if err = PrepareWebhooks(baseRepo, HOOK_EVENT_FORK, &api.ForkPayload{
Forkee: repo.APIFormat(nil),
Repo: baseRepo.APIFormat(nil),
Forkee: repo.APIFormatLegacy(nil),
Repo: baseRepo.APIFormatLegacy(nil),
Sender: doer.APIFormat(),
}); err != nil {
log.Error("PrepareWebhooks [repo_id: %d]: %v", baseRepo.ID, err)

View File

@@ -10,18 +10,28 @@ import (
"strings"
"time"
api "github.com/gogs/go-gogs-client"
"gorm.io/gorm"
"gogs.io/gogs/internal/errutil"
"gogs.io/gogs/internal/repoutil"
)
// ReposStore is the persistent interface for repositories.
//
// NOTE: All methods are sorted in alphabetical order.
type ReposStore interface {
// Create creates a new repository record in the database. It returns
// ErrNameNotAllowed when the repository name is not allowed, or
// ErrRepoAlreadyExist when a repository with same name already exists for the
// owner.
Create(ctx context.Context, ownerID int64, opts CreateRepoOptions) (*Repository, error)
// GetByName returns the repository with given owner and name. It returns
// ErrRepoNotExist when not found.
GetByName(ctx context.Context, ownerID int64, name string) (*Repository, error)
// Touch updates the updated time to the current time and removes the bare state
// of the given repository.
Touch(ctx context.Context, id int64) error
}
var Repos ReposStore
@@ -47,12 +57,58 @@ func (r *Repository) AfterFind(_ *gorm.DB) error {
return nil
}
type RepositoryAPIFormatOptions struct {
Permission *api.Permission
Parent *api.Repository
}
// APIFormat returns the API format of a repository.
func (r *Repository) APIFormat(owner *User, opts ...RepositoryAPIFormatOptions) *api.Repository {
var opt RepositoryAPIFormatOptions
if len(opts) > 0 {
opt = opts[0]
}
cloneLink := repoutil.NewCloneLink(owner.Name, r.Name, false)
return &api.Repository{
ID: r.ID,
Owner: owner.APIFormat(),
Name: r.Name,
FullName: owner.Name + "/" + r.Name,
Description: r.Description,
Private: r.IsPrivate,
Fork: r.IsFork,
Parent: opt.Parent,
Empty: r.IsBare,
Mirror: r.IsMirror,
Size: r.Size,
HTMLURL: repoutil.HTMLURL(owner.Name, r.Name),
SSHURL: cloneLink.SSH,
CloneURL: cloneLink.HTTPS,
Website: r.Website,
Stars: r.NumStars,
Forks: r.NumForks,
Watchers: r.NumWatches,
OpenIssues: r.NumOpenIssues,
DefaultBranch: r.DefaultBranch,
Created: r.Created,
Updated: r.Updated,
Permissions: opt.Permission,
}
}
var _ ReposStore = (*repos)(nil)
type repos struct {
*gorm.DB
}
// NewReposStore returns a persistent interface for repositories with given
// database connection.
func NewReposStore(db *gorm.DB) ReposStore {
return &repos{DB: db}
}
type ErrRepoAlreadyExist struct {
args errutil.Args
}
@@ -66,7 +122,7 @@ func (err ErrRepoAlreadyExist) Error() string {
return fmt.Sprintf("repository already exists: %v", err.args)
}
type createRepoOpts struct {
type CreateRepoOptions struct {
Name string
Description string
DefaultBranch string
@@ -79,10 +135,7 @@ type createRepoOpts struct {
ForkID int64
}
// create creates a new repository record in the database. Fields of "repo" will be updated
// in place upon insertion. It returns ErrNameNotAllowed when the repository name is not allowed,
// or ErrRepoAlreadyExist when a repository with same name already exists for the owner.
func (db *repos) create(ctx context.Context, ownerID int64, opts createRepoOpts) (*Repository, error) {
func (db *repos) Create(ctx context.Context, ownerID int64, opts CreateRepoOptions) (*Repository, error) {
err := isRepoNameAllowed(opts.Name)
if err != nil {
return nil, err
@@ -90,7 +143,12 @@ func (db *repos) create(ctx context.Context, ownerID int64, opts createRepoOpts)
_, err = db.GetByName(ctx, ownerID, opts.Name)
if err == nil {
return nil, ErrRepoAlreadyExist{args: errutil.Args{"ownerID": ownerID, "name": opts.Name}}
return nil, ErrRepoAlreadyExist{
args: errutil.Args{
"ownerID": ownerID,
"name": opts.Name,
},
}
} else if !IsErrRepoNotExist(err) {
return nil, err
}
@@ -115,7 +173,7 @@ func (db *repos) create(ctx context.Context, ownerID int64, opts createRepoOpts)
var _ errutil.NotFound = (*ErrRepoNotExist)(nil)
type ErrRepoNotExist struct {
args map[string]interface{}
args errutil.Args
}
func IsErrRepoNotExist(err error) bool {
@@ -139,9 +197,25 @@ func (db *repos) GetByName(ctx context.Context, ownerID int64, name string) (*Re
Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrRepoNotExist{args: map[string]interface{}{"ownerID": ownerID, "name": name}}
return nil, ErrRepoNotExist{
args: errutil.Args{
"ownerID": ownerID,
"name": name,
},
}
}
return nil, err
}
return repo, nil
}
func (db *repos) Touch(ctx context.Context, id int64) error {
return db.WithContext(ctx).
Model(new(Repository)).
Where("id = ?", id).
Updates(map[string]interface{}{
"is_bare": false,
"updated_unix": db.NowFunc().Unix(),
}).
Error
}

View File

@@ -20,7 +20,6 @@ func TestRepos(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(Repository)}
@@ -32,8 +31,9 @@ func TestRepos(t *testing.T) {
name string
test func(*testing.T, *repos)
}{
{"create", reposCreate},
{"Create", reposCreate},
{"GetByName", reposGetByName},
{"Touch", reposTouch},
} {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
@@ -52,9 +52,9 @@ func reposCreate(t *testing.T, db *repos) {
ctx := context.Background()
t.Run("name not allowed", func(t *testing.T) {
_, err := db.create(ctx,
_, err := db.Create(ctx,
1,
createRepoOpts{
CreateRepoOptions{
Name: "my.git",
},
)
@@ -63,15 +63,15 @@ func reposCreate(t *testing.T, db *repos) {
})
t.Run("already exists", func(t *testing.T) {
_, err := db.create(ctx, 2,
createRepoOpts{
_, err := db.Create(ctx, 2,
CreateRepoOptions{
Name: "repo1",
},
)
require.NoError(t, err)
_, err = db.create(ctx, 2,
createRepoOpts{
_, err = db.Create(ctx, 2,
CreateRepoOptions{
Name: "repo1",
},
)
@@ -79,8 +79,8 @@ func reposCreate(t *testing.T, db *repos) {
assert.Equal(t, wantErr, err)
})
repo, err := db.create(ctx, 3,
createRepoOpts{
repo, err := db.Create(ctx, 3,
CreateRepoOptions{
Name: "repo2",
},
)
@@ -94,8 +94,8 @@ func reposCreate(t *testing.T, db *repos) {
func reposGetByName(t *testing.T, db *repos) {
ctx := context.Background()
repo, err := db.create(ctx, 1,
createRepoOpts{
repo, err := db.Create(ctx, 1,
CreateRepoOptions{
Name: "repo1",
},
)
@@ -108,3 +108,31 @@ func reposGetByName(t *testing.T, db *repos) {
wantErr := ErrRepoNotExist{args: errutil.Args{"ownerID": int64(1), "name": "bad_name"}}
assert.Equal(t, wantErr, err)
}
func reposTouch(t *testing.T, db *repos) {
ctx := context.Background()
repo, err := db.Create(ctx, 1,
CreateRepoOptions{
Name: "repo1",
},
)
require.NoError(t, err)
err = db.WithContext(ctx).Model(new(Repository)).Where("id = ?", repo.ID).Update("is_bare", true).Error
require.NoError(t, err)
// Make sure it is bare
got, err := db.GetByName(ctx, repo.OwnerID, repo.Name)
require.NoError(t, err)
assert.True(t, got.IsBare)
// Touch it
err = db.Touch(ctx, repo.ID)
require.NoError(t, err)
// It should not be bare anymore
got, err = db.GetByName(ctx, repo.OwnerID, repo.Name)
require.NoError(t, err)
assert.False(t, got.IsBare)
}

View File

@@ -0,0 +1,3 @@
{"ID":1,"UserID":1,"OpType":16,"ActUserID":1,"ActUserName":"alice","RepoID":1,"RepoUserName":"alice","RepoName":"example","RefName":"main","IsPrivate":false,"Content":"{\"Len\":1,\"Commits\":[],\"CompareURL\":\"\"}","CreatedUnix":1588568886}
{"ID":2,"UserID":1,"OpType":5,"ActUserID":1,"ActUserName":"alice","RepoID":1,"RepoUserName":"alice","RepoName":"example","RefName":"main","IsPrivate":false,"Content":"{\"Len\":1,\"Commits\":[],\"CompareURL\":\"\"}","CreatedUnix":1588568886}
{"ID":3,"UserID":1,"OpType":17,"ActUserID":1,"ActUserName":"alice","RepoID":1,"RepoUserName":"alice","RepoName":"example","RefName":"main","IsPrivate":false,"Content":"","CreatedUnix":1588568886}

View File

@@ -18,8 +18,8 @@ import (
// TwoFactor is a 2FA token of a user.
type TwoFactor struct {
ID int64
UserID int64 `xorm:"UNIQUE" gorm:"UNIQUE"`
ID int64 `gorm:"primaryKey"`
UserID int64 `xorm:"UNIQUE" gorm:"unique"`
Secret string
Created time.Time `xorm:"-" gorm:"-" json:"-"`
CreatedUnix int64

View File

@@ -11,16 +11,40 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gogs.io/gogs/internal/dbtest"
"gogs.io/gogs/internal/errutil"
)
func TestTwoFactor_BeforeCreate(t *testing.T) {
now := time.Now()
db := &gorm.DB{
Config: &gorm.Config{
SkipDefaultTransaction: true,
NowFunc: func() time.Time {
return now
},
},
}
t.Run("CreatedUnix has been set", func(t *testing.T) {
tf := &TwoFactor{CreatedUnix: 1}
_ = tf.BeforeCreate(db)
assert.Equal(t, int64(1), tf.CreatedUnix)
})
t.Run("CreatedUnix has not been set", func(t *testing.T) {
tf := &TwoFactor{}
_ = tf.BeforeCreate(db)
assert.Equal(t, db.NowFunc().Unix(), tf.CreatedUnix)
})
}
func TestTwoFactors(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(TwoFactor), new(TwoFactorRecoveryCode)}

View File

@@ -5,11 +5,13 @@
package db
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/gogs/git-module"
"github.com/pkg/errors"
)
// CommitToPushCommit transforms a git.Commit to PushCommit type.
@@ -50,6 +52,8 @@ type PushUpdateOptions struct {
// PushUpdate must be called for any push actions in order to
// generates necessary push action history feeds.
func PushUpdate(opts PushUpdateOptions) (err error) {
ctx := context.TODO()
isNewRef := strings.HasPrefix(opts.OldCommitID, git.EmptyID)
isDelRef := strings.HasPrefix(opts.NewCommitID, git.EmptyID)
if isNewRef && isDelRef {
@@ -85,16 +89,17 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
// Push tags
if strings.HasPrefix(opts.FullRefspec, git.RefsTags) {
if err := CommitRepoAction(CommitRepoActionOptions{
PusherName: opts.PusherName,
RepoOwnerID: owner.ID,
RepoName: repo.Name,
RefFullName: opts.FullRefspec,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: &PushCommits{},
}); err != nil {
return fmt.Errorf("CommitRepoAction.(tag): %v", err)
err := Actions.PushTag(ctx,
PushTagOptions{
Owner: owner,
Repo: repo,
PusherName: opts.PusherName,
RefFullName: opts.FullRefspec,
NewCommitID: opts.NewCommitID,
},
)
if err != nil {
return errors.Wrap(err, "create action for push tag")
}
return nil
}
@@ -122,16 +127,19 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
}
}
if err := CommitRepoAction(CommitRepoActionOptions{
PusherName: opts.PusherName,
RepoOwnerID: owner.ID,
RepoName: repo.Name,
RefFullName: opts.FullRefspec,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: CommitsToPushCommits(commits),
}); err != nil {
return fmt.Errorf("CommitRepoAction.(branch): %v", err)
err = Actions.CommitRepo(ctx,
CommitRepoOptions{
Owner: owner,
Repo: repo,
PusherName: opts.PusherName,
RefFullName: opts.FullRefspec,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: CommitsToPushCommits(commits),
},
)
if err != nil {
return errors.Wrap(err, "create action for commit push")
}
return nil
}

View File

@@ -49,14 +49,14 @@ const (
// User represents the object of individual and member of organization.
type User struct {
ID int64
LowerName string `xorm:"UNIQUE NOT NULL" gorm:"UNIQUE"`
Name string `xorm:"UNIQUE NOT NULL" gorm:"NOT NULL"`
ID int64 `gorm:"primaryKey"`
LowerName string `xorm:"UNIQUE NOT NULL" gorm:"unique;not null"`
Name string `xorm:"UNIQUE NOT NULL" gorm:"not null"`
FullName string
// Email is the primary email address (to be used for communication)
Email string `xorm:"NOT NULL" gorm:"NOT NULL"`
Passwd string `xorm:"NOT NULL" gorm:"NOT NULL"`
LoginSource int64 `xorm:"NOT NULL DEFAULT 0" gorm:"NOT NULL;DEFAULT:0"`
Email string `xorm:"NOT NULL" gorm:"not null"`
Passwd string `xorm:"NOT NULL" gorm:"not null"`
LoginSource int64 `xorm:"NOT NULL DEFAULT 0" gorm:"not null;default:0"`
LoginName string
Type UserType
OwnedOrgs []*User `xorm:"-" gorm:"-" json:"-"`
@@ -64,8 +64,8 @@ type User struct {
Repos []*Repository `xorm:"-" gorm:"-" json:"-"`
Location string
Website string
Rands string `xorm:"VARCHAR(10)" gorm:"TYPE:VARCHAR(10)"`
Salt string `xorm:"VARCHAR(10)" gorm:"TYPE:VARCHAR(10)"`
Rands string `xorm:"VARCHAR(10)" gorm:"type:VARCHAR(10)"`
Salt string `xorm:"VARCHAR(10)" gorm:"type:VARCHAR(10)"`
Created time.Time `xorm:"-" gorm:"-" json:"-"`
CreatedUnix int64
@@ -75,7 +75,7 @@ type User struct {
// Remember visibility choice for convenience, true for private
LastRepoVisibility bool
// Maximum repository creation limit, -1 means use global default
MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1" gorm:"NOT NULL;DEFAULT:-1"`
MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1" gorm:"not null;default:-1"`
// Permissions
IsActive bool // Activate primary email
@@ -85,13 +85,13 @@ type User struct {
ProhibitLogin bool
// Avatar
Avatar string `xorm:"VARCHAR(2048) NOT NULL" gorm:"TYPE:VARCHAR(2048);NOT NULL"`
AvatarEmail string `xorm:"NOT NULL" gorm:"NOT NULL"`
Avatar string `xorm:"VARCHAR(2048) NOT NULL" gorm:"type:VARCHAR(2048);not null"`
AvatarEmail string `xorm:"NOT NULL" gorm:"not null"`
UseCustomAvatar bool
// Counters
NumFollowers int
NumFollowing int `xorm:"NOT NULL DEFAULT 0" gorm:"NOT NULL;DEFAULT:0"`
NumFollowing int `xorm:"NOT NULL DEFAULT 0" gorm:"not null;default:0"`
NumStars int
NumRepos int
@@ -466,7 +466,7 @@ func (u *User) DisplayName() string {
}
func (u *User) ShortName(length int) string {
return tool.EllipsisString(u.Name, length)
return strutil.Ellipsis(u.Name, length)
}
// IsMailable checks if a user is eligible
@@ -908,6 +908,8 @@ func DeleteInactivateUsers() (err error) {
}
// UserPath returns the path absolute path of user repositories.
//
// Deprecated: Use repoutil.UserPath instead.
func UserPath(username string) string {
return filepath.Join(conf.Repository.Root, strings.ToLower(username))
}

View File

@@ -16,9 +16,9 @@ import (
// primary email address, but is not obligatory.
type EmailAddress struct {
ID int64
UID int64 `xorm:"INDEX NOT NULL" gorm:"INDEX"`
Email string `xorm:"UNIQUE NOT NULL" gorm:"UNIQUE"`
IsActivated bool `gorm:"NOT NULL;DEFAULT:FALSE"`
UID int64 `xorm:"INDEX NOT NULL" gorm:"index;not null"`
Email string `xorm:"UNIQUE NOT NULL" gorm:"unique;not null"`
IsActivated bool `gorm:"not null;default:FALSE"`
IsPrimary bool `xorm:"-" gorm:"-" json:"-"`
}

View File

@@ -38,7 +38,7 @@ type UsersStore interface {
// Create creates a new user and persists to database. It returns
// ErrUserAlreadyExist when a user with same name already exists, or
// ErrEmailAlreadyUsed if the email has been used by another user.
Create(ctx context.Context, username, email string, opts CreateUserOpts) (*User, error)
Create(ctx context.Context, username, email string, opts CreateUserOptions) (*User, error)
// GetByEmail returns the user (not organization) with given email. It ignores
// records with unverified emails and returns ErrUserNotExist when not found.
GetByEmail(ctx context.Context, email string) (*User, error)
@@ -74,6 +74,12 @@ type users struct {
*gorm.DB
}
// NewUsersStore returns a persistent interface for users with given database
// connection.
func NewUsersStore(db *gorm.DB) UsersStore {
return &users{DB: db}
}
type ErrLoginSourceMismatch struct {
args errutil.Args
}
@@ -154,7 +160,7 @@ func (db *users) Authenticate(ctx context.Context, login, password string, login
}
return db.Create(ctx, extAccount.Name, extAccount.Email,
CreateUserOpts{
CreateUserOptions{
FullName: extAccount.FullName,
LoginSource: authSourceID,
LoginName: extAccount.Login,
@@ -166,7 +172,7 @@ func (db *users) Authenticate(ctx context.Context, login, password string, login
)
}
type CreateUserOpts struct {
type CreateUserOptions struct {
FullName string
Password string
LoginSource int64
@@ -211,7 +217,7 @@ func (err ErrEmailAlreadyUsed) Error() string {
return fmt.Sprintf("email has been used: %v", err.args)
}
func (db *users) Create(ctx context.Context, username, email string, opts CreateUserOpts) (*User, error) {
func (db *users) Create(ctx context.Context, username, email string, opts CreateUserOptions) (*User, error) {
err := isUsernameAllowed(username)
if err != nil {
return nil, err

View File

@@ -22,7 +22,6 @@ func TestUsers(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(User), new(EmailAddress)}
@@ -58,7 +57,7 @@ func usersAuthenticate(t *testing.T, db *users) {
password := "pa$$word"
alice, err := db.Create(ctx, "alice", "alice@example.com",
CreateUserOpts{
CreateUserOptions{
Password: password,
},
)
@@ -109,7 +108,7 @@ func usersAuthenticate(t *testing.T, db *users) {
setMockLoginSourcesStore(t, mockLoginSources)
bob, err := db.Create(ctx, "bob", "bob@example.com",
CreateUserOpts{
CreateUserOptions{
Password: password,
LoginSource: 1,
},
@@ -154,26 +153,26 @@ func usersCreate(t *testing.T, db *users) {
ctx := context.Background()
alice, err := db.Create(ctx, "alice", "alice@example.com",
CreateUserOpts{
CreateUserOptions{
Activated: true,
},
)
require.NoError(t, err)
t.Run("name not allowed", func(t *testing.T) {
_, err := db.Create(ctx, "-", "", CreateUserOpts{})
_, err := db.Create(ctx, "-", "", CreateUserOptions{})
wantErr := ErrNameNotAllowed{args: errutil.Args{"reason": "reserved", "name": "-"}}
assert.Equal(t, wantErr, err)
})
t.Run("name already exists", func(t *testing.T) {
_, err := db.Create(ctx, alice.Name, "", CreateUserOpts{})
_, err := db.Create(ctx, alice.Name, "", CreateUserOptions{})
wantErr := ErrUserAlreadyExist{args: errutil.Args{"name": alice.Name}}
assert.Equal(t, wantErr, err)
})
t.Run("email already exists", func(t *testing.T) {
_, err := db.Create(ctx, "bob", alice.Email, CreateUserOpts{})
_, err := db.Create(ctx, "bob", alice.Email, CreateUserOptions{})
wantErr := ErrEmailAlreadyUsed{args: errutil.Args{"email": alice.Email}}
assert.Equal(t, wantErr, err)
})
@@ -195,7 +194,7 @@ func usersGetByEmail(t *testing.T, db *users) {
t.Run("ignore organization", func(t *testing.T) {
// TODO: Use Orgs.Create to replace SQL hack when the method is available.
org, err := db.Create(ctx, "gogs", "gogs@exmaple.com", CreateUserOpts{})
org, err := db.Create(ctx, "gogs", "gogs@exmaple.com", CreateUserOptions{})
require.NoError(t, err)
err = db.Model(&User{}).Where("id", org.ID).UpdateColumn("type", UserOrganization).Error
@@ -207,7 +206,7 @@ func usersGetByEmail(t *testing.T, db *users) {
})
t.Run("by primary email", func(t *testing.T) {
alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOpts{})
alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
require.NoError(t, err)
_, err = db.GetByEmail(ctx, alice.Email)
@@ -225,7 +224,7 @@ func usersGetByEmail(t *testing.T, db *users) {
})
t.Run("by secondary email", func(t *testing.T) {
bob, err := db.Create(ctx, "bob", "bob@example.com", CreateUserOpts{})
bob, err := db.Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
require.NoError(t, err)
// TODO: Use UserEmails.Create to replace SQL hack when the method is available.
@@ -250,7 +249,7 @@ func usersGetByEmail(t *testing.T, db *users) {
func usersGetByID(t *testing.T, db *users) {
ctx := context.Background()
alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOpts{})
alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
require.NoError(t, err)
user, err := db.GetByID(ctx, alice.ID)
@@ -265,7 +264,7 @@ func usersGetByID(t *testing.T, db *users) {
func usersGetByUsername(t *testing.T, db *users) {
ctx := context.Background()
alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOpts{})
alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
require.NoError(t, err)
user, err := db.GetByUsername(ctx, alice.Name)

38
internal/db/watches.go Normal file
View File

@@ -0,0 +1,38 @@
// Copyright 2020 The Gogs 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 db
import (
"context"
"gorm.io/gorm"
)
// WatchesStore is the persistent interface for watches.
//
// NOTE: All methods are sorted in alphabetical order.
type WatchesStore interface {
// ListByRepo returns all watches of the given repository.
ListByRepo(ctx context.Context, repoID int64) ([]*Watch, error)
}
var Watches WatchesStore
var _ WatchesStore = (*watches)(nil)
type watches struct {
*gorm.DB
}
// NewWatchesStore returns a persistent interface for watches with given
// database connection.
func NewWatchesStore(db *gorm.DB) WatchesStore {
return &watches{DB: db}
}
func (db *watches) ListByRepo(ctx context.Context, repoID int64) ([]*Watch, error) {
var watches []*Watch
return watches, db.WithContext(ctx).Where("repo_id = ?", repoID).Find(&watches).Error
}

View File

@@ -0,0 +1,47 @@
// Copyright 2022 The Gogs 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 db
import (
"testing"
"github.com/stretchr/testify/require"
"gogs.io/gogs/internal/dbtest"
)
func TestWatches(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
tables := []interface{}{new(Watch)}
db := &watches{
DB: dbtest.NewDB(t, "watches", tables...),
}
for _, tc := range []struct {
name string
test func(*testing.T, *watches)
}{
{"ListByRepo", watchesListByRepo},
} {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
err := clearTables(t, db.DB, tables...)
require.NoError(t, err)
})
tc.test(t, db)
})
if t.Failed() {
break
}
}
}
func watchesListByRepo(_ *testing.T, _ *watches) {
// TODO: Add tests once WatchRepo is migrated to GORM.
}

View File

@@ -26,6 +26,7 @@ import (
"gogs.io/gogs/internal/httplib"
"gogs.io/gogs/internal/netutil"
"gogs.io/gogs/internal/sync"
"gogs.io/gogs/internal/testutil"
)
var HookQueue = sync.NewUniqueQueue(1000)
@@ -676,6 +677,11 @@ func prepareWebhooks(e Engine, repo *Repository, event HookEventType, p api.Payl
// PrepareWebhooks adds all active webhooks to task queue.
func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error {
// NOTE: To prevent too many cascading changes in a single refactoring PR, we
// choose to ignore this function in tests.
if x == nil && testutil.InTest {
return nil
}
return prepareWebhooks(x, repo, event, p)
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/gogs/git-module"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/repoutil"
"gogs.io/gogs/internal/sync"
)
@@ -37,7 +38,9 @@ func ToWikiPageName(urlString string) string {
}
// WikiCloneLink returns clone URLs of repository wiki.
func (repo *Repository) WikiCloneLink() (cl *CloneLink) {
//
// Deprecated: Use repoutil.NewCloneLink instead.
func (repo *Repository) WikiCloneLink() (cl *repoutil.CloneLink) {
return repo.cloneLink(true)
}

View File

@@ -55,8 +55,8 @@ func NewDB(t *testing.T, suite string, tables ...interface{}) *gorm.DB {
dbOpts.Name = dbName
cleanup = func(db *gorm.DB) {
db.Exec(fmt.Sprintf("DROP DATABASE `%s`", dbName))
cleanup = func(_ *gorm.DB) {
_, _ = sqlDB.Exec(fmt.Sprintf("DROP DATABASE `%s`", dbName))
_ = sqlDB.Close()
}
case "postgres":
@@ -86,8 +86,8 @@ func NewDB(t *testing.T, suite string, tables ...interface{}) *gorm.DB {
dbOpts.Name = dbName
cleanup = func(db *gorm.DB) {
db.Exec(fmt.Sprintf(`DROP DATABASE %q`, dbName))
cleanup = func(_ *gorm.DB) {
_, _ = sqlDB.Exec(fmt.Sprintf(`DROP DATABASE %q`, dbName))
_ = sqlDB.Close()
}
case "sqlite":

View File

@@ -7,10 +7,10 @@
package lazyregexp
import (
"os"
"regexp"
"strings"
"sync"
"gogs.io/gogs/internal/testutil"
)
// Regexp is a wrapper around regexp.Regexp, where the underlying regexp will be
@@ -99,14 +99,12 @@ func (r *Regexp) ReplaceAll(src, repl []byte) []byte {
return r.Regexp().ReplaceAll(src, repl)
}
var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
// New creates a new lazy regexp, delaying the compiling work until it is first
// needed. If the code is being run as part of tests, the regexp compiling will
// happen immediately.
func New(str string) *Regexp {
lr := &Regexp{str: str}
if inTest {
if testutil.InTest {
// In tests, always compile the regexps early.
lr.Regexp()
}

View File

@@ -0,0 +1,62 @@
// Copyright 2022 The Gogs 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 repoutil
import (
"fmt"
"path/filepath"
"strings"
"gogs.io/gogs/internal/conf"
)
// CloneLink represents different types of clone URLs of repository.
type CloneLink struct {
SSH string
HTTPS string
}
// NewCloneLink returns clone URLs using given owner and repository name.
func NewCloneLink(owner, repo string, isWiki bool) *CloneLink {
if isWiki {
repo += ".wiki"
}
cl := new(CloneLink)
if conf.SSH.Port != 22 {
cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", conf.App.RunUser, conf.SSH.Domain, conf.SSH.Port, owner, repo)
} else {
cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", conf.App.RunUser, conf.SSH.Domain, owner, repo)
}
cl.HTTPS = HTTPSCloneURL(owner, repo)
return cl
}
// HTTPSCloneURL returns HTTPS clone URL using given owner and repository name.
func HTTPSCloneURL(owner, repo string) string {
return fmt.Sprintf("%s%s/%s.git", conf.Server.ExternalURL, owner, repo)
}
// HTMLURL returns HTML URL using given owner and repository name.
func HTMLURL(owner, repo string) string {
return conf.Server.ExternalURL + owner + "/" + repo
}
// CompareCommitsPath returns the comparison path using given owner, repository,
// and commit IDs.
func CompareCommitsPath(owner, repo, oldCommitID, newCommitID string) string {
return fmt.Sprintf("%s/%s/compare/%s...%s", owner, repo, oldCommitID, newCommitID)
}
// UserPath returns the absolute path for storing user repositories.
func UserPath(user string) string {
return filepath.Join(conf.Repository.Root, strings.ToLower(user))
}
// RepositoryPath returns the absolute path using given user and repository
// name.
func RepositoryPath(owner, repo string) string {
return filepath.Join(UserPath(owner), strings.ToLower(repo)+".git")
}

View File

@@ -0,0 +1,127 @@
// Copyright 2022 The Gogs 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 repoutil
import (
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"gogs.io/gogs/internal/conf"
)
func TestNewCloneLink(t *testing.T) {
conf.SetMockApp(t,
conf.AppOpts{
RunUser: "git",
},
)
conf.SetMockServer(t,
conf.ServerOpts{
ExternalURL: "https://example.com/",
},
)
t.Run("regular SSH port", func(t *testing.T) {
conf.SetMockSSH(t,
conf.SSHOpts{
Domain: "example.com",
Port: 22,
},
)
got := NewCloneLink("alice", "example", false)
want := &CloneLink{
SSH: "git@example.com:alice/example.git",
HTTPS: "https://example.com/alice/example.git",
}
assert.Equal(t, want, got)
})
t.Run("irregular SSH port", func(t *testing.T) {
conf.SetMockSSH(t,
conf.SSHOpts{
Domain: "example.com",
Port: 2222,
},
)
got := NewCloneLink("alice", "example", false)
want := &CloneLink{
SSH: "ssh://git@example.com:2222/alice/example.git",
HTTPS: "https://example.com/alice/example.git",
}
assert.Equal(t, want, got)
})
t.Run("wiki", func(t *testing.T) {
conf.SetMockSSH(t,
conf.SSHOpts{
Domain: "example.com",
Port: 22,
},
)
got := NewCloneLink("alice", "example", true)
want := &CloneLink{
SSH: "git@example.com:alice/example.wiki.git",
HTTPS: "https://example.com/alice/example.wiki.git",
}
assert.Equal(t, want, got)
})
}
func TestHTMLURL(t *testing.T) {
conf.SetMockServer(t,
conf.ServerOpts{
ExternalURL: "https://example.com/",
},
)
got := HTMLURL("alice", "example")
want := "https://example.com/alice/example"
assert.Equal(t, want, got)
}
func TestCompareCommitsPath(t *testing.T) {
got := CompareCommitsPath("alice", "example", "old", "new")
want := "alice/example/compare/old...new"
assert.Equal(t, want, got)
}
func TestUserPath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping testing on Windows")
return
}
conf.SetMockRepository(t,
conf.RepositoryOpts{
Root: "/home/git/gogs-repositories",
},
)
got := UserPath("alice")
want := "/home/git/gogs-repositories/alice"
assert.Equal(t, want, got)
}
func TestRepositoryPath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping testing on Windows")
return
}
conf.SetMockRepository(t,
conf.RepositoryOpts{
Root: "/home/git/gogs-repositories",
},
)
got := RepositoryPath("alice", "example")
want := "/home/git/gogs-repositories/alice/example.git"
assert.Equal(t, want, got)
}

View File

@@ -35,7 +35,7 @@ func Authentications(c *context.Context) {
c.PageIs("AdminAuthentications")
var err error
c.Data["Sources"], err = db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOpts{})
c.Data["Sources"], err = db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOptions{})
if err != nil {
c.Error(err, "list login sources")
return
@@ -160,7 +160,7 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
}
source, err := db.LoginSources.Create(c.Req.Context(),
db.CreateLoginSourceOpts{
db.CreateLoginSourceOptions{
Type: auth.Type(f.Type),
Name: f.Name,
Activated: f.IsActive,

View File

@@ -46,7 +46,7 @@ func NewUser(c *context.Context) {
c.Data["login_type"] = "0-0"
sources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOpts{})
sources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOptions{})
if err != nil {
c.Error(err, "list login sources")
return
@@ -62,7 +62,7 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
c.Data["PageIsAdmin"] = true
c.Data["PageIsAdminUsers"] = true
sources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOpts{})
sources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOptions{})
if err != nil {
c.Error(err, "list login sources")
return
@@ -136,7 +136,7 @@ func prepareUserInfo(c *context.Context) *db.User {
c.Data["LoginSource"] = &db.LoginSource{}
}
sources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOpts{})
sources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOptions{})
if err != nil {
c.Error(err, "list login sources")
return nil

View File

@@ -66,7 +66,7 @@ func Search(c *context.APIContext) {
results := make([]*api.Repository, len(repos))
for i := range repos {
results[i] = repos[i].APIFormat(nil)
results[i] = repos[i].APIFormatLegacy(nil)
}
c.SetLinkHeader(int(count), opts.PageSize)
@@ -110,7 +110,7 @@ func listUserRepositories(c *context.APIContext, username string) {
if c.User.ID != user.ID {
repos := make([]*api.Repository, len(ownRepos))
for i := range ownRepos {
repos[i] = ownRepos[i].APIFormat(&api.Permission{Admin: true, Push: true, Pull: true})
repos[i] = ownRepos[i].APIFormatLegacy(&api.Permission{Admin: true, Push: true, Pull: true})
}
c.JSONSuccess(&repos)
return
@@ -125,12 +125,12 @@ func listUserRepositories(c *context.APIContext, username string) {
numOwnRepos := len(ownRepos)
repos := make([]*api.Repository, numOwnRepos+len(accessibleRepos))
for i := range ownRepos {
repos[i] = ownRepos[i].APIFormat(&api.Permission{Admin: true, Push: true, Pull: true})
repos[i] = ownRepos[i].APIFormatLegacy(&api.Permission{Admin: true, Push: true, Pull: true})
}
i := numOwnRepos
for repo, access := range accessibleRepos {
repos[i] = repo.APIFormat(&api.Permission{
repos[i] = repo.APIFormatLegacy(&api.Permission{
Admin: access >= db.AccessModeAdmin,
Push: access >= db.AccessModeWrite,
Pull: true,
@@ -154,7 +154,7 @@ func ListOrgRepositories(c *context.APIContext) {
}
func CreateUserRepo(c *context.APIContext, owner *db.User, opt api.CreateRepoOption) {
repo, err := db.CreateRepository(c.User, owner, db.CreateRepoOptions{
repo, err := db.CreateRepository(c.User, owner, db.CreateRepoOptionsLegacy{
Name: opt.Name,
Description: opt.Description,
Gitignores: opt.Gitignores,
@@ -178,7 +178,7 @@ func CreateUserRepo(c *context.APIContext, owner *db.User, opt api.CreateRepoOpt
return
}
c.JSON(201, repo.APIFormat(&api.Permission{Admin: true, Push: true, Pull: true}))
c.JSON(201, repo.APIFormatLegacy(&api.Permission{Admin: true, Push: true, Pull: true}))
}
func Create(c *context.APIContext, opt api.CreateRepoOption) {
@@ -282,7 +282,7 @@ func Migrate(c *context.APIContext, f form.MigrateRepo) {
}
log.Trace("Repository migrated: %s/%s", ctxUser.Name, f.RepoName)
c.JSON(201, repo.APIFormat(&api.Permission{Admin: true, Push: true, Pull: true}))
c.JSON(201, repo.APIFormatLegacy(&api.Permission{Admin: true, Push: true, Pull: true}))
}
// FIXME: inject in the handler chain
@@ -312,7 +312,7 @@ func Get(c *context.APIContext) {
return
}
c.JSONSuccess(repo.APIFormat(&api.Permission{
c.JSONSuccess(repo.APIFormatLegacy(&api.Permission{
Admin: c.Repo.IsAdmin(),
Push: c.Repo.IsWriter(),
Pull: true,
@@ -352,7 +352,7 @@ func ListForks(c *context.APIContext) {
c.Error(err, "get owner")
return
}
apiForks[i] = forks[i].APIFormat(&api.Permission{
apiForks[i] = forks[i].APIFormatLegacy(&api.Permission{
Admin: c.User.IsAdminOfRepo(forks[i]),
Push: c.User.IsWriterOfRepo(forks[i]),
Pull: true,

View File

@@ -1492,20 +1492,36 @@ func (c PermsStoreSetRepoPermsFuncCall) Results() []interface{} {
// MockReposStore is a mock implementation of the ReposStore interface (from
// the package gogs.io/gogs/internal/db) used for unit testing.
type MockReposStore struct {
// CreateFunc is an instance of a mock function object controlling the
// behavior of the method Create.
CreateFunc *ReposStoreCreateFunc
// GetByNameFunc is an instance of a mock function object controlling
// the behavior of the method GetByName.
GetByNameFunc *ReposStoreGetByNameFunc
// TouchFunc is an instance of a mock function object controlling the
// behavior of the method Touch.
TouchFunc *ReposStoreTouchFunc
}
// NewMockReposStore creates a new mock of the ReposStore interface. All
// methods return zero values for all results, unless overwritten.
func NewMockReposStore() *MockReposStore {
return &MockReposStore{
CreateFunc: &ReposStoreCreateFunc{
defaultHook: func(context.Context, int64, db.CreateRepoOptions) (r0 *db.Repository, r1 error) {
return
},
},
GetByNameFunc: &ReposStoreGetByNameFunc{
defaultHook: func(context.Context, int64, string) (r0 *db.Repository, r1 error) {
return
},
},
TouchFunc: &ReposStoreTouchFunc{
defaultHook: func(context.Context, int64) (r0 error) {
return
},
},
}
}
@@ -1513,11 +1529,21 @@ func NewMockReposStore() *MockReposStore {
// All methods panic on invocation, unless overwritten.
func NewStrictMockReposStore() *MockReposStore {
return &MockReposStore{
CreateFunc: &ReposStoreCreateFunc{
defaultHook: func(context.Context, int64, db.CreateRepoOptions) (*db.Repository, error) {
panic("unexpected invocation of MockReposStore.Create")
},
},
GetByNameFunc: &ReposStoreGetByNameFunc{
defaultHook: func(context.Context, int64, string) (*db.Repository, error) {
panic("unexpected invocation of MockReposStore.GetByName")
},
},
TouchFunc: &ReposStoreTouchFunc{
defaultHook: func(context.Context, int64) error {
panic("unexpected invocation of MockReposStore.Touch")
},
},
}
}
@@ -1525,12 +1551,128 @@ func NewStrictMockReposStore() *MockReposStore {
// All methods delegate to the given implementation, unless overwritten.
func NewMockReposStoreFrom(i db.ReposStore) *MockReposStore {
return &MockReposStore{
CreateFunc: &ReposStoreCreateFunc{
defaultHook: i.Create,
},
GetByNameFunc: &ReposStoreGetByNameFunc{
defaultHook: i.GetByName,
},
TouchFunc: &ReposStoreTouchFunc{
defaultHook: i.Touch,
},
}
}
// ReposStoreCreateFunc describes the behavior when the Create method of the
// parent MockReposStore instance is invoked.
type ReposStoreCreateFunc struct {
defaultHook func(context.Context, int64, db.CreateRepoOptions) (*db.Repository, error)
hooks []func(context.Context, int64, db.CreateRepoOptions) (*db.Repository, error)
history []ReposStoreCreateFuncCall
mutex sync.Mutex
}
// Create delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockReposStore) Create(v0 context.Context, v1 int64, v2 db.CreateRepoOptions) (*db.Repository, error) {
r0, r1 := m.CreateFunc.nextHook()(v0, v1, v2)
m.CreateFunc.appendCall(ReposStoreCreateFuncCall{v0, v1, v2, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the Create method of the
// parent MockReposStore instance is invoked and the hook queue is empty.
func (f *ReposStoreCreateFunc) SetDefaultHook(hook func(context.Context, int64, db.CreateRepoOptions) (*db.Repository, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// Create method of the parent MockReposStore instance invokes the hook at
// the front of the queue and discards it. After the queue is empty, the
// default hook function is invoked for any future action.
func (f *ReposStoreCreateFunc) PushHook(hook func(context.Context, int64, db.CreateRepoOptions) (*db.Repository, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *ReposStoreCreateFunc) SetDefaultReturn(r0 *db.Repository, r1 error) {
f.SetDefaultHook(func(context.Context, int64, db.CreateRepoOptions) (*db.Repository, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *ReposStoreCreateFunc) PushReturn(r0 *db.Repository, r1 error) {
f.PushHook(func(context.Context, int64, db.CreateRepoOptions) (*db.Repository, error) {
return r0, r1
})
}
func (f *ReposStoreCreateFunc) nextHook() func(context.Context, int64, db.CreateRepoOptions) (*db.Repository, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *ReposStoreCreateFunc) appendCall(r0 ReposStoreCreateFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of ReposStoreCreateFuncCall objects describing
// the invocations of this function.
func (f *ReposStoreCreateFunc) History() []ReposStoreCreateFuncCall {
f.mutex.Lock()
history := make([]ReposStoreCreateFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// ReposStoreCreateFuncCall is an object that describes an invocation of
// method Create on an instance of MockReposStore.
type ReposStoreCreateFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int64
// Arg2 is the value of the 3rd argument passed to this method
// invocation.
Arg2 db.CreateRepoOptions
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 *db.Repository
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c ReposStoreCreateFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c ReposStoreCreateFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// ReposStoreGetByNameFunc describes the behavior when the GetByName method
// of the parent MockReposStore instance is invoked.
type ReposStoreGetByNameFunc struct {
@@ -1642,6 +1784,110 @@ func (c ReposStoreGetByNameFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// ReposStoreTouchFunc describes the behavior when the Touch method of the
// parent MockReposStore instance is invoked.
type ReposStoreTouchFunc struct {
defaultHook func(context.Context, int64) error
hooks []func(context.Context, int64) error
history []ReposStoreTouchFuncCall
mutex sync.Mutex
}
// Touch delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockReposStore) Touch(v0 context.Context, v1 int64) error {
r0 := m.TouchFunc.nextHook()(v0, v1)
m.TouchFunc.appendCall(ReposStoreTouchFuncCall{v0, v1, r0})
return r0
}
// SetDefaultHook sets function that is called when the Touch method of the
// parent MockReposStore instance is invoked and the hook queue is empty.
func (f *ReposStoreTouchFunc) SetDefaultHook(hook func(context.Context, int64) error) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// Touch method of the parent MockReposStore instance invokes the hook at
// the front of the queue and discards it. After the queue is empty, the
// default hook function is invoked for any future action.
func (f *ReposStoreTouchFunc) PushHook(hook func(context.Context, int64) error) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *ReposStoreTouchFunc) SetDefaultReturn(r0 error) {
f.SetDefaultHook(func(context.Context, int64) error {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *ReposStoreTouchFunc) PushReturn(r0 error) {
f.PushHook(func(context.Context, int64) error {
return r0
})
}
func (f *ReposStoreTouchFunc) nextHook() func(context.Context, int64) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *ReposStoreTouchFunc) appendCall(r0 ReposStoreTouchFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of ReposStoreTouchFuncCall objects describing
// the invocations of this function.
func (f *ReposStoreTouchFunc) History() []ReposStoreTouchFuncCall {
f.mutex.Lock()
history := make([]ReposStoreTouchFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// ReposStoreTouchFuncCall is an object that describes an invocation of
// method Touch on an instance of MockReposStore.
type ReposStoreTouchFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int64
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c ReposStoreTouchFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c ReposStoreTouchFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// MockTwoFactorsStore is a mock implementation of the TwoFactorsStore
// interface (from the package gogs.io/gogs/internal/db) used for unit
// testing.
@@ -2074,7 +2320,7 @@ func NewMockUsersStore() *MockUsersStore {
},
},
CreateFunc: &UsersStoreCreateFunc{
defaultHook: func(context.Context, string, string, db.CreateUserOpts) (r0 *db.User, r1 error) {
defaultHook: func(context.Context, string, string, db.CreateUserOptions) (r0 *db.User, r1 error) {
return
},
},
@@ -2106,7 +2352,7 @@ func NewStrictMockUsersStore() *MockUsersStore {
},
},
CreateFunc: &UsersStoreCreateFunc{
defaultHook: func(context.Context, string, string, db.CreateUserOpts) (*db.User, error) {
defaultHook: func(context.Context, string, string, db.CreateUserOptions) (*db.User, error) {
panic("unexpected invocation of MockUsersStore.Create")
},
},
@@ -2267,15 +2513,15 @@ func (c UsersStoreAuthenticateFuncCall) Results() []interface{} {
// UsersStoreCreateFunc describes the behavior when the Create method of the
// parent MockUsersStore instance is invoked.
type UsersStoreCreateFunc struct {
defaultHook func(context.Context, string, string, db.CreateUserOpts) (*db.User, error)
hooks []func(context.Context, string, string, db.CreateUserOpts) (*db.User, error)
defaultHook func(context.Context, string, string, db.CreateUserOptions) (*db.User, error)
hooks []func(context.Context, string, string, db.CreateUserOptions) (*db.User, error)
history []UsersStoreCreateFuncCall
mutex sync.Mutex
}
// Create delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockUsersStore) Create(v0 context.Context, v1 string, v2 string, v3 db.CreateUserOpts) (*db.User, error) {
func (m *MockUsersStore) Create(v0 context.Context, v1 string, v2 string, v3 db.CreateUserOptions) (*db.User, error) {
r0, r1 := m.CreateFunc.nextHook()(v0, v1, v2, v3)
m.CreateFunc.appendCall(UsersStoreCreateFuncCall{v0, v1, v2, v3, r0, r1})
return r0, r1
@@ -2283,7 +2529,7 @@ func (m *MockUsersStore) Create(v0 context.Context, v1 string, v2 string, v3 db.
// SetDefaultHook sets function that is called when the Create method of the
// parent MockUsersStore instance is invoked and the hook queue is empty.
func (f *UsersStoreCreateFunc) SetDefaultHook(hook func(context.Context, string, string, db.CreateUserOpts) (*db.User, error)) {
func (f *UsersStoreCreateFunc) SetDefaultHook(hook func(context.Context, string, string, db.CreateUserOptions) (*db.User, error)) {
f.defaultHook = hook
}
@@ -2291,7 +2537,7 @@ func (f *UsersStoreCreateFunc) SetDefaultHook(hook func(context.Context, string,
// Create method of the parent MockUsersStore instance invokes the hook at
// the front of the queue and discards it. After the queue is empty, the
// default hook function is invoked for any future action.
func (f *UsersStoreCreateFunc) PushHook(hook func(context.Context, string, string, db.CreateUserOpts) (*db.User, error)) {
func (f *UsersStoreCreateFunc) PushHook(hook func(context.Context, string, string, db.CreateUserOptions) (*db.User, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
@@ -2300,19 +2546,19 @@ func (f *UsersStoreCreateFunc) PushHook(hook func(context.Context, string, strin
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *UsersStoreCreateFunc) SetDefaultReturn(r0 *db.User, r1 error) {
f.SetDefaultHook(func(context.Context, string, string, db.CreateUserOpts) (*db.User, error) {
f.SetDefaultHook(func(context.Context, string, string, db.CreateUserOptions) (*db.User, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *UsersStoreCreateFunc) PushReturn(r0 *db.User, r1 error) {
f.PushHook(func(context.Context, string, string, db.CreateUserOpts) (*db.User, error) {
f.PushHook(func(context.Context, string, string, db.CreateUserOptions) (*db.User, error) {
return r0, r1
})
}
func (f *UsersStoreCreateFunc) nextHook() func(context.Context, string, string, db.CreateUserOpts) (*db.User, error) {
func (f *UsersStoreCreateFunc) nextHook() func(context.Context, string, string, db.CreateUserOptions) (*db.User, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
@@ -2356,7 +2602,7 @@ type UsersStoreCreateFuncCall struct {
Arg2 string
// Arg3 is the value of the 4th argument passed to this method
// invocation.
Arg3 db.CreateUserOpts
Arg3 db.CreateUserOptions
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 *db.User

View File

@@ -146,7 +146,7 @@ func DeleteBranchPost(c *context.Context) {
Ref: branchName,
RefType: "branch",
PusherType: api.PUSHER_TYPE_USER,
Repo: c.Repo.Repository.APIFormat(nil),
Repo: c.Repo.Repository.APIFormatLegacy(nil),
Sender: c.User.APIFormat(),
}); err != nil {
log.Error("Failed to prepare webhooks for %q: %v", db.HOOK_EVENT_DELETE, err)

View File

@@ -119,7 +119,7 @@ func CreatePost(c *context.Context, f form.CreateRepo) {
return
}
repo, err := db.CreateRepository(c.User, ctxUser, db.CreateRepoOptions{
repo, err := db.CreateRepository(c.User, ctxUser, db.CreateRepoOptionsLegacy{
Name: f.RepoName,
Description: f.Description,
Gitignores: f.Gitignores,

View File

@@ -100,8 +100,8 @@ func SettingsPost(c *context.Context, f form.RepoSetting) {
log.Trace("Repository basic settings updated: %s/%s", c.Repo.Owner.Name, repo.Name)
if isNameChanged {
if err := db.RenameRepoAction(c.User, oldRepoName, repo); err != nil {
log.Error("RenameRepoAction: %v", err)
if err := db.Actions.RenameRepo(c.Req.Context(), c.User, repo.MustOwner(), oldRepoName, repo); err != nil {
log.Error("create rename repository action: %v", err)
}
}

View File

@@ -536,7 +536,7 @@ func TestWebhook(c *context.Context) {
Modified: nameStatus.Modified,
},
},
Repo: c.Repo.Repository.APIFormat(nil),
Repo: c.Repo.Repository.APIFormatLegacy(nil),
Pusher: apiUser,
Sender: apiUser,
}

View File

@@ -102,7 +102,7 @@ func Login(c *context.Context) {
}
// Display normal login page
loginSources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOpts{OnlyActivated: true})
loginSources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOptions{OnlyActivated: true})
if err != nil {
c.Error(err, "list activated login sources")
return
@@ -149,7 +149,7 @@ func afterLogin(c *context.Context, u *db.User, remember bool) {
func LoginPost(c *context.Context, f form.SignIn) {
c.Title("sign_in")
loginSources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOpts{OnlyActivated: true})
loginSources, err := db.LoginSources.List(c.Req.Context(), db.ListLoginSourceOptions{OnlyActivated: true})
if err != nil {
c.Error(err, "list activated login sources")
return

View File

@@ -53,9 +53,17 @@ func getDashboardContextUser(c *context.Context) *db.User {
// The user could be organization so it is not always the logged in user,
// which is why we have to explicitly pass the context user ID.
func retrieveFeeds(c *context.Context, ctxUser *db.User, userID int64, isProfile bool) {
actions, err := db.GetFeeds(ctxUser, userID, c.QueryInt64("after_id"), isProfile)
afterID := c.QueryInt64("after_id")
var err error
var actions []*db.Action
if ctxUser.IsOrganization() {
actions, err = db.Actions.ListByOrganization(c.Req.Context(), ctxUser.ID, userID, afterID)
} else {
actions, err = db.Actions.ListByUser(c.Req.Context(), ctxUser.ID, userID, afterID, isProfile)
}
if err != nil {
c.Error(err, "get feeds")
c.Error(err, "list actions")
return
}

View File

@@ -54,7 +54,7 @@ func Profile(c *context.Context, puser *context.ParamsUser) {
c.Data["TabName"] = tab
switch tab {
case "activity":
retrieveFeeds(c, puser.User, -1, true)
retrieveFeeds(c, puser.User, c.UserID(), true)
if c.Written() {
return
}

View File

@@ -44,3 +44,13 @@ func RandomChars(n int) (string, error) {
return string(buffer), nil
}
// Ellipsis returns a truncated string and appends "..." to the end of the
// string if the string length is larger than the threshold. Otherwise, the
// original string is returned.
func Ellipsis(str string, threshold int) string {
if len(str) <= threshold || threshold < 0 {
return str
}
return str[:threshold] + "..."
}

View File

@@ -55,3 +55,43 @@ func TestRandomChars(t *testing.T) {
cache[chars] = true
}
}
func TestEllipsis(t *testing.T) {
tests := []struct {
name string
str string
threshold int
want string
}{
{
name: "empty string and zero threshold",
str: "",
threshold: 0,
want: "",
},
{
name: "smaller length than threshold",
str: "ab",
threshold: 3,
want: "ab",
},
{
name: "same length as threshold",
str: "abc",
threshold: 3,
want: "abc",
},
{
name: "greater length than threshold",
str: "ab",
threshold: 1,
want: "a...",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := Ellipsis(test.str, test.threshold)
assert.Equal(t, test.want, got)
})
}
}

View File

@@ -27,6 +27,7 @@ import (
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/gitutil"
"gogs.io/gogs/internal/markup"
"gogs.io/gogs/internal/strutil"
"gogs.io/gogs/internal/tool"
)
@@ -106,7 +107,7 @@ func FuncMap() []template.FuncMap {
return str[start:end]
},
"Join": strings.Join,
"EllipsisString": tool.EllipsisString,
"EllipsisString": strutil.Ellipsis,
"DiffFileTypeToStr": DiffFileTypeToStr,
"DiffLineTypeToStr": DiffLineTypeToStr,
"Sha1": Sha1,

View File

@@ -0,0 +1,13 @@
// Copyright 2022 The Gogs 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 testutil
import (
"os"
"strings"
)
// InTest is ture if the current binary looks like a test artifact.
var InTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")

View File

@@ -0,0 +1,15 @@
// Copyright 2022 The Gogs 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 testutil
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestInTest(t *testing.T) {
assert.True(t, InTest)
}

View File

@@ -358,15 +358,6 @@ func Subtract(left, right interface{}) interface{} {
}
}
// EllipsisString returns a truncated short string,
// it appends '...' in the end of the length of string is too large.
func EllipsisString(str string, length int) string {
if len(str) < length {
return str
}
return str[:length-3] + "..."
}
// TruncateString returns a truncated string with given limit,
// it returns input string if length is not reached limit.
func TruncateString(str string, limit int) string {