Batch hook pre- and post-receive calls (#8602)

* make notifyWatchers work on multiple actions

* more efficient multiple notifyWatchers

* Make CommitRepoAction take advantage of multiple actions

* Batch post and pre-receive results

* Set batch to 30

* Auto adjust timeout & add logging

* adjust processing message

* Add some messages to pre-receive

* Make any non-200 status code from pre-receive an error

* Add missing hookPrintResults

* Remove shortcut for single action

* mistaken merge fix

* oops

* Move master branch to the front

* If repo was empty and the master branch is pushed ensure that that is set as the default branch

* fixup

* fixup

* Missed HookOptions in setdefaultbranch

* Batch PushUpdateAddTag and PushUpdateDelTag

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
release/v1.15
zeripath 2019-12-26 11:29:45 +00:00 committed by Lauris BH
parent 114d474f02
commit 7bfb83e064
10 changed files with 1063 additions and 440 deletions

View File

@ -21,6 +21,10 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
) )
const (
hookBatchSize = 30
)
var ( var (
// CmdHook represents the available hooks sub-command. // CmdHook represents the available hooks sub-command.
CmdHook = cli.Command{ CmdHook = cli.Command{
@ -75,12 +79,25 @@ Gitea or set your environment appropriately.`, "")
prID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchPRID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchPRID), 10, 64)
isDeployKey, _ := strconv.ParseBool(os.Getenv(models.EnvIsDeployKey)) isDeployKey, _ := strconv.ParseBool(os.Getenv(models.EnvIsDeployKey))
buf := bytes.NewBuffer(nil) hookOptions := private.HookOptions{
scanner := bufio.NewScanner(os.Stdin) UserID: userID,
for scanner.Scan() { GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
buf.Write(scanner.Bytes()) GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
buf.WriteByte('\n') GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
ProtectedBranchID: prID,
IsDeployKey: isDeployKey,
}
scanner := bufio.NewScanner(os.Stdin)
oldCommitIDs := make([]string, hookBatchSize)
newCommitIDs := make([]string, hookBatchSize)
refFullNames := make([]string, hookBatchSize)
count := 0
total := 0
lastline := 0
for scanner.Scan() {
// TODO: support news feeds for wiki // TODO: support news feeds for wiki
if isWiki { if isWiki {
continue continue
@ -94,29 +111,72 @@ Gitea or set your environment appropriately.`, "")
oldCommitID := string(fields[0]) oldCommitID := string(fields[0])
newCommitID := string(fields[1]) newCommitID := string(fields[1])
refFullName := string(fields[2]) refFullName := string(fields[2])
total++
lastline++
// If the ref is a branch, check if it's protected // If the ref is a branch, check if it's protected
if strings.HasPrefix(refFullName, git.BranchPrefix) { if strings.HasPrefix(refFullName, git.BranchPrefix) {
statusCode, msg := private.HookPreReceive(username, reponame, private.HookOptions{ oldCommitIDs[count] = oldCommitID
OldCommitID: oldCommitID, newCommitIDs[count] = newCommitID
NewCommitID: newCommitID, refFullNames[count] = refFullName
RefFullName: refFullName, count++
UserID: userID, fmt.Fprintf(os.Stdout, "*")
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories), os.Stdout.Sync()
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
GitQuarantinePath: os.Getenv(private.GitQuarantinePath), if count >= hookBatchSize {
ProtectedBranchID: prID, fmt.Fprintf(os.Stdout, " Checking %d branches\n", count)
IsDeployKey: isDeployKey, os.Stdout.Sync()
})
switch statusCode { hookOptions.OldCommitIDs = oldCommitIDs
case http.StatusInternalServerError: hookOptions.NewCommitIDs = newCommitIDs
fail("Internal Server Error", msg) hookOptions.RefFullNames = refFullNames
case http.StatusForbidden: statusCode, msg := private.HookPreReceive(username, reponame, hookOptions)
fail(msg, "") switch statusCode {
case http.StatusOK:
// no-op
case http.StatusInternalServerError:
fail("Internal Server Error", msg)
default:
fail(msg, "")
}
count = 0
lastline = 0
} }
} else {
fmt.Fprintf(os.Stdout, ".")
os.Stdout.Sync()
}
if lastline >= hookBatchSize {
fmt.Fprintf(os.Stdout, "\n")
os.Stdout.Sync()
lastline = 0
} }
} }
if count > 0 {
hookOptions.OldCommitIDs = oldCommitIDs[:count]
hookOptions.NewCommitIDs = newCommitIDs[:count]
hookOptions.RefFullNames = refFullNames[:count]
fmt.Fprintf(os.Stdout, " Checking %d branches\n", count)
os.Stdout.Sync()
statusCode, msg := private.HookPreReceive(username, reponame, hookOptions)
switch statusCode {
case http.StatusInternalServerError:
fail("Internal Server Error", msg)
case http.StatusForbidden:
fail(msg, "")
}
} else if lastline > 0 {
fmt.Fprintf(os.Stdout, "\n")
os.Stdout.Sync()
lastline = 0
}
fmt.Fprintf(os.Stdout, "Checked %d references in total\n", total)
os.Stdout.Sync()
return nil return nil
} }
@ -156,12 +216,24 @@ Gitea or set your environment appropriately.`, "")
pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64) pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
pusherName := os.Getenv(models.EnvPusherName) pusherName := os.Getenv(models.EnvPusherName)
buf := bytes.NewBuffer(nil) hookOptions := private.HookOptions{
UserName: pusherName,
UserID: pusherID,
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
}
oldCommitIDs := make([]string, hookBatchSize)
newCommitIDs := make([]string, hookBatchSize)
refFullNames := make([]string, hookBatchSize)
count := 0
total := 0
wasEmpty := false
masterPushed := false
results := make([]private.HookPostReceiveBranchResult, 0)
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() { for scanner.Scan() {
buf.Write(scanner.Bytes())
buf.WriteByte('\n')
// TODO: support news feeds for wiki // TODO: support news feeds for wiki
if isWiki { if isWiki {
continue continue
@ -172,36 +244,95 @@ Gitea or set your environment appropriately.`, "")
continue continue
} }
oldCommitID := string(fields[0]) fmt.Fprintf(os.Stdout, ".")
newCommitID := string(fields[1]) oldCommitIDs[count] = string(fields[0])
refFullName := string(fields[2]) newCommitIDs[count] = string(fields[1])
refFullNames[count] = string(fields[2])
res, err := private.HookPostReceive(repoUser, repoName, private.HookOptions{ if refFullNames[count] == git.BranchPrefix+"master" && newCommitIDs[count] != git.EmptySHA && count == total {
OldCommitID: oldCommitID, masterPushed = true
NewCommitID: newCommitID,
RefFullName: refFullName,
UserID: pusherID,
UserName: pusherName,
})
if res == nil {
fail("Internal Server Error", err)
} }
count++
total++
os.Stdout.Sync()
if res["message"] == false { if count >= hookBatchSize {
fmt.Fprintf(os.Stdout, " Processing %d references\n", count)
os.Stdout.Sync()
hookOptions.OldCommitIDs = oldCommitIDs
hookOptions.NewCommitIDs = newCommitIDs
hookOptions.RefFullNames = refFullNames
resp, err := private.HookPostReceive(repoUser, repoName, hookOptions)
if resp == nil {
hookPrintResults(results)
fail("Internal Server Error", err)
}
wasEmpty = wasEmpty || resp.RepoWasEmpty
results = append(results, resp.Results...)
count = 0
}
}
if count == 0 {
if wasEmpty && masterPushed {
// We need to tell the repo to reset the default branch to master
err := private.SetDefaultBranch(repoUser, repoName, "master")
if err != nil {
fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err)
}
}
fmt.Fprintf(os.Stdout, "Processed %d references in total\n", total)
os.Stdout.Sync()
hookPrintResults(results)
return nil
}
hookOptions.OldCommitIDs = oldCommitIDs[:count]
hookOptions.NewCommitIDs = newCommitIDs[:count]
hookOptions.RefFullNames = refFullNames[:count]
fmt.Fprintf(os.Stdout, " Processing %d references\n", count)
os.Stdout.Sync()
resp, err := private.HookPostReceive(repoUser, repoName, hookOptions)
if resp == nil {
hookPrintResults(results)
fail("Internal Server Error", err)
}
wasEmpty = wasEmpty || resp.RepoWasEmpty
results = append(results, resp.Results...)
fmt.Fprintf(os.Stdout, "Processed %d references in total\n", total)
os.Stdout.Sync()
if wasEmpty && masterPushed {
// We need to tell the repo to reset the default branch to master
err := private.SetDefaultBranch(repoUser, repoName, "master")
if err != nil {
fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err)
}
}
hookPrintResults(results)
return nil
}
func hookPrintResults(results []private.HookPostReceiveBranchResult) {
for _, res := range results {
if !res.Message {
continue continue
} }
fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "")
if res["create"] == true { if res.Create {
fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res["branch"]) fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res.Branch)
fmt.Fprintf(os.Stderr, " %s\n", res["url"]) fmt.Fprintf(os.Stderr, " %s\n", res.URL)
} else { } else {
fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
fmt.Fprintf(os.Stderr, " %s\n", res["url"]) fmt.Fprintf(os.Stderr, " %s\n", res.URL)
} }
fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "")
os.Stderr.Sync()
} }
return nil
} }

View File

@ -164,68 +164,111 @@ func (repo *Repository) GetWatchers(page int) ([]*User, error) {
return users, sess.Find(&users) return users, sess.Find(&users)
} }
func notifyWatchers(e Engine, act *Action) error { func notifyWatchers(e Engine, actions ...*Action) error {
// Add feeds for user self and all watchers. var watchers []*Watch
watches, err := getWatchers(e, act.RepoID) var repo *Repository
if err != nil { var err error
return fmt.Errorf("get watchers: %v", err) var permCode []bool
} var permIssue []bool
var permPR []bool
// Add feed for actioner. for _, act := range actions {
act.UserID = act.ActUserID repoChanged := repo == nil || repo.ID != act.RepoID
if _, err = e.InsertOne(act); err != nil {
return fmt.Errorf("insert new actioner: %v", err)
}
act.loadRepo() if repoChanged {
// check repo owner exist. // Add feeds for user self and all watchers.
if err := act.Repo.getOwner(e); err != nil { watchers, err = getWatchers(e, act.RepoID)
return fmt.Errorf("can't get repo owner: %v", err) if err != nil {
} return fmt.Errorf("get watchers: %v", err)
}
}
// Add feed for organization // Add feed for actioner.
if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID { act.UserID = act.ActUserID
act.ID = 0
act.UserID = act.Repo.Owner.ID
if _, err = e.InsertOne(act); err != nil { if _, err = e.InsertOne(act); err != nil {
return fmt.Errorf("insert new actioner: %v", err) return fmt.Errorf("insert new actioner: %v", err)
} }
}
for i := range watches { if repoChanged {
if act.ActUserID == watches[i].UserID { act.loadRepo()
continue repo = act.Repo
// check repo owner exist.
if err := act.Repo.getOwner(e); err != nil {
return fmt.Errorf("can't get repo owner: %v", err)
}
} else if act.Repo == nil {
act.Repo = repo
} }
act.ID = 0 // Add feed for organization
act.UserID = watches[i].UserID if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID {
act.Repo.Units = nil act.ID = 0
act.UserID = act.Repo.Owner.ID
switch act.OpType { if _, err = e.InsertOne(act); err != nil {
case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionDeleteBranch: return fmt.Errorf("insert new actioner: %v", err)
if !act.Repo.checkUnitUser(e, act.UserID, false, UnitTypeCode) {
continue
}
case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue:
if !act.Repo.checkUnitUser(e, act.UserID, false, UnitTypeIssues) {
continue
}
case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest:
if !act.Repo.checkUnitUser(e, act.UserID, false, UnitTypePullRequests) {
continue
} }
} }
if _, err = e.InsertOne(act); err != nil { if repoChanged {
return fmt.Errorf("insert new action: %v", err) permCode = make([]bool, len(watchers))
permIssue = make([]bool, len(watchers))
permPR = make([]bool, len(watchers))
for i, watcher := range watchers {
user, err := getUserByID(e, watcher.UserID)
if err != nil {
permCode[i] = false
permIssue[i] = false
permPR[i] = false
continue
}
perm, err := getUserRepoPermission(e, repo, user)
if err != nil {
permCode[i] = false
permIssue[i] = false
permPR[i] = false
continue
}
permCode[i] = perm.CanRead(UnitTypeCode)
permIssue[i] = perm.CanRead(UnitTypeIssues)
permPR[i] = perm.CanRead(UnitTypePullRequests)
}
}
for i, watcher := range watchers {
if act.ActUserID == watcher.UserID {
continue
}
act.ID = 0
act.UserID = watcher.UserID
act.Repo.Units = nil
switch act.OpType {
case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionDeleteBranch:
if !permCode[i] {
continue
}
case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue:
if !permIssue[i] {
continue
}
case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest:
if !permPR[i] {
continue
}
}
if _, err = e.InsertOne(act); err != nil {
return fmt.Errorf("insert new action: %v", err)
}
} }
} }
return nil return nil
} }
// NotifyWatchers creates batch of actions for every watcher. // NotifyWatchers creates batch of actions for every watcher.
func NotifyWatchers(act *Action) error { func NotifyWatchers(actions ...*Action) error {
return notifyWatchers(x, act) return notifyWatchers(x, actions...)
} }
// NotifyWatchersActions creates batch of actions for every watcher. // NotifyWatchersActions creates batch of actions for every watcher.

View File

@ -53,6 +53,66 @@ func ListToPushCommits(l *list.List) *PushCommits {
return &PushCommits{l.Len(), commits, "", make(map[string]string), make(map[string]*User)} return &PushCommits{l.Len(), commits, "", make(map[string]string), make(map[string]*User)}
} }
// PushUpdateAddDeleteTags updates a number of added and delete tags
func PushUpdateAddDeleteTags(repo *Repository, gitRepo *git.Repository, addTags, delTags []string) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return fmt.Errorf("Unable to begin sess in PushUpdateDeleteTags: %v", err)
}
if err := pushUpdateDeleteTags(sess, repo, delTags); err != nil {
return err
}
if err := pushUpdateAddTags(sess, repo, gitRepo, addTags); err != nil {
return err
}
return sess.Commit()
}
// PushUpdateDeleteTags updates a number of delete tags
func PushUpdateDeleteTags(repo *Repository, tags []string) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return fmt.Errorf("Unable to begin sess in PushUpdateDeleteTags: %v", err)
}
if err := pushUpdateDeleteTags(sess, repo, tags); err != nil {
return err
}
return sess.Commit()
}
func pushUpdateDeleteTags(e Engine, repo *Repository, tags []string) error {
if len(tags) == 0 {
return nil
}
lowerTags := make([]string, 0, len(tags))
for _, tag := range tags {
lowerTags = append(lowerTags, strings.ToLower(tag))
}
if _, err := e.
Where("repo_id = ? AND is_tag = ?", repo.ID, true).
In("lower_tag_name", lowerTags).
Delete(new(Release)); err != nil {
return fmt.Errorf("Delete: %v", err)
}
if _, err := e.
Where("repo_id = ? AND is_tag = ?", repo.ID, false).
In("lower_tag_name", lowerTags).
SetExpr("is_draft", true).
SetExpr("num_commits", 0).
SetExpr("sha1", "").
Update(new(Release)); err != nil {
return fmt.Errorf("Update: %v", err)
}
return nil
}
// PushUpdateDeleteTag must be called for any push actions to delete tag // PushUpdateDeleteTag must be called for any push actions to delete tag
func PushUpdateDeleteTag(repo *Repository, tagName string) error { func PushUpdateDeleteTag(repo *Repository, tagName string) error {
rel, err := GetRelease(repo.ID, tagName) rel, err := GetRelease(repo.ID, tagName)
@ -78,6 +138,125 @@ func PushUpdateDeleteTag(repo *Repository, tagName string) error {
return nil return nil
} }
// PushUpdateAddTags updates a number of add tags
func PushUpdateAddTags(repo *Repository, gitRepo *git.Repository, tags []string) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return fmt.Errorf("Unable to begin sess in PushUpdateAddTags: %v", err)
}
if err := pushUpdateAddTags(sess, repo, gitRepo, tags); err != nil {
return err
}
return sess.Commit()
}
func pushUpdateAddTags(e Engine, repo *Repository, gitRepo *git.Repository, tags []string) error {
if len(tags) == 0 {
return nil
}
lowerTags := make([]string, 0, len(tags))
for _, tag := range tags {
lowerTags = append(lowerTags, strings.ToLower(tag))
}
releases := make([]Release, 0, len(tags))
if err := e.Where("repo_id = ?", repo.ID).
In("lower_tag_name", lowerTags).Find(&releases); err != nil {
return fmt.Errorf("GetRelease: %v", err)
}
relMap := make(map[string]*Release)
for _, rel := range releases {
relMap[rel.LowerTagName] = &rel
}
newReleases := make([]*Release, 0, len(lowerTags)-len(relMap))
emailToUser := make(map[string]*User)
for i, lowerTag := range lowerTags {
tag, err := gitRepo.GetTag(tags[i])
if err != nil {
return fmt.Errorf("GetTag: %v", err)
}
commit, err := tag.Commit()
if err != nil {
return fmt.Errorf("Commit: %v", err)
}
sig := tag.Tagger
if sig == nil {
sig = commit.Author
}
if sig == nil {
sig = commit.Committer
}
var author *User
var createdAt = time.Unix(1, 0)
if sig != nil {
var ok bool
author, ok = emailToUser[sig.Email]
if !ok {
author, err = GetUserByEmail(sig.Email)
if err != nil && !IsErrUserNotExist(err) {
return fmt.Errorf("GetUserByEmail: %v", err)
}
}
createdAt = sig.When
}
commitsCount, err := commit.CommitsCount()
if err != nil {
return fmt.Errorf("CommitsCount: %v", err)
}
rel, has := relMap[lowerTag]
if !has {
rel = &Release{
RepoID: repo.ID,
Title: "",
TagName: tags[i],
LowerTagName: lowerTag,
Target: "",
Sha1: commit.ID.String(),
NumCommits: commitsCount,
Note: "",
IsDraft: false,
IsPrerelease: false,
IsTag: true,
CreatedUnix: timeutil.TimeStamp(createdAt.Unix()),
}
if author != nil {
rel.PublisherID = author.ID
}
newReleases = append(newReleases, rel)
} else {
rel.Sha1 = commit.ID.String()
rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
rel.NumCommits = commitsCount
rel.IsDraft = false
if rel.IsTag && author != nil {
rel.PublisherID = author.ID
}
if _, err = e.ID(rel.ID).AllCols().Update(rel); err != nil {
return fmt.Errorf("Update: %v", err)
}
}
}
if len(newReleases) > 0 {
if _, err := e.Insert(newReleases); err != nil {
return fmt.Errorf("Insert: %v", err)
}
}
return nil
}
// PushUpdateAddTag must be called for any push actions to add tag // PushUpdateAddTag must be called for any push actions to add tag
func PushUpdateAddTag(repo *Repository, gitRepo *git.Repository, tagName string) error { func PushUpdateAddTag(repo *Repository, gitRepo *git.Repository, tagName string) error {
rel, err := GetRelease(repo.ID, tagName) rel, err := GetRelease(repo.ID, tagName)

View File

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"time"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
@ -22,9 +23,9 @@ const (
// HookOptions represents the options for the Hook calls // HookOptions represents the options for the Hook calls
type HookOptions struct { type HookOptions struct {
OldCommitID string OldCommitIDs []string
NewCommitID string NewCommitIDs []string
RefFullName string RefFullNames []string
UserID int64 UserID int64
UserName string UserName string
GitObjectDirectory string GitObjectDirectory string
@ -34,23 +35,33 @@ type HookOptions struct {
IsDeployKey bool IsDeployKey bool
} }
// HookPostReceiveResult represents an individual result from PostReceive
type HookPostReceiveResult struct {
Results []HookPostReceiveBranchResult
RepoWasEmpty bool
Err string
}
// HookPostReceiveBranchResult represents an individual branch result from PostReceive
type HookPostReceiveBranchResult struct {
Message bool
Create bool
Branch string
URL string
}
// HookPreReceive check whether the provided commits are allowed // HookPreReceive check whether the provided commits are allowed
func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string) { func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&gitObjectDirectory=%s&gitAlternativeObjectDirectories=%s&gitQuarantinePath=%s&prID=%d&isDeployKey=%t", reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s",
url.PathEscape(ownerName), url.PathEscape(ownerName),
url.PathEscape(repoName), url.PathEscape(repoName),
url.QueryEscape(opts.OldCommitID),
url.QueryEscape(opts.NewCommitID),
url.QueryEscape(opts.RefFullName),
opts.UserID,
url.QueryEscape(opts.GitObjectDirectory),
url.QueryEscape(opts.GitAlternativeObjectDirectories),
url.QueryEscape(opts.GitQuarantinePath),
opts.ProtectedBranchID,
opts.IsDeployKey,
) )
req := newInternalRequest(reqURL, "POST")
resp, err := newInternalRequest(reqURL, "GET").Response() req = req.Header("Content-Type", "application/json")
jsonBytes, _ := json.Marshal(opts)
req.Body(jsonBytes)
req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second)
resp, err := req.Response()
if err != nil { if err != nil {
return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error())
} }
@ -64,17 +75,18 @@ func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string)
} }
// HookPostReceive updates services and users // HookPostReceive updates services and users
func HookPostReceive(ownerName, repoName string, opts HookOptions) (map[string]interface{}, string) { func HookPostReceive(ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, string) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&username=%s", reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s",
url.PathEscape(ownerName), url.PathEscape(ownerName),
url.PathEscape(repoName), url.PathEscape(repoName),
url.QueryEscape(opts.OldCommitID), )
url.QueryEscape(opts.NewCommitID),
url.QueryEscape(opts.RefFullName),
opts.UserID,
url.QueryEscape(opts.UserName))
resp, err := newInternalRequest(reqURL, "GET").Response() req := newInternalRequest(reqURL, "POST")
req = req.Header("Content-Type", "application/json")
req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second)
jsonBytes, _ := json.Marshal(opts)
req.Body(jsonBytes)
resp, err := req.Response()
if err != nil { if err != nil {
return nil, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) return nil, fmt.Sprintf("Unable to contact gitea: %v", err.Error())
} }
@ -83,8 +95,30 @@ func HookPostReceive(ownerName, repoName string, opts HookOptions) (map[string]i
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, decodeJSONError(resp).Err return nil, decodeJSONError(resp).Err
} }
res := map[string]interface{}{} res := &HookPostReceiveResult{}
_ = json.NewDecoder(resp.Body).Decode(&res) _ = json.NewDecoder(resp.Body).Decode(res)
return res, "" return res, ""
} }
// SetDefaultBranch will set the default branch to the provided branch for the provided repository
func SetDefaultBranch(ownerName, repoName, branch string) error {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s",
url.PathEscape(ownerName),
url.PathEscape(repoName),
url.PathEscape(branch),
)
req := newInternalRequest(reqURL, "POST")
req = req.Header("Content-Type", "application/json")
req.SetTimeout(60*time.Second, 60*time.Second)
resp, err := req.Response()
if err != nil {
return fmt.Errorf("Unable to contact gitea: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Error returned from gitea: %v", decodeJSONError(resp).Err)
}
return nil
}

View File

@ -159,112 +159,132 @@ type CommitRepoActionOptions struct {
// CommitRepoAction adds new commit action to the repository, and prepare // CommitRepoAction adds new commit action to the repository, and prepare
// corresponding webhooks. // corresponding webhooks.
func CommitRepoAction(opts CommitRepoActionOptions) error { func CommitRepoAction(optsList ...*CommitRepoActionOptions) error {
pusher, err := models.GetUserByName(opts.PusherName) var pusher *models.User
if err != nil { var repo *models.Repository
return fmt.Errorf("GetUserByName [%s]: %v", opts.PusherName, err) actions := make([]*models.Action, len(optsList))
}
repo, err := models.GetRepositoryByName(opts.RepoOwnerID, opts.RepoName) for i, opts := range optsList {
if err != nil { if pusher == nil || pusher.Name != opts.PusherName {
return fmt.Errorf("GetRepositoryByName [owner_id: %d, name: %s]: %v", opts.RepoOwnerID, opts.RepoName, err) var err error
} pusher, err = models.GetUserByName(opts.PusherName)
refName := git.RefEndName(opts.RefFullName)
// Change default branch and empty status only if pushed ref is non-empty branch.
if repo.IsEmpty && opts.NewCommitID != git.EmptySHA && strings.HasPrefix(opts.RefFullName, git.BranchPrefix) {
repo.DefaultBranch = refName
repo.IsEmpty = false
if refName != "master" {
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil { if err != nil {
return err return fmt.Errorf("GetUserByName [%s]: %v", opts.PusherName, err)
} }
if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { }
if !git.IsErrUnsupportedVersion(err) {
gitRepo.Close() if repo == nil || repo.OwnerID != opts.RepoOwnerID || repo.Name != opts.RepoName {
return err var err error
if repo != nil {
// Change repository empty status and update last updated time.
if err := models.UpdateRepository(repo, false); err != nil {
return fmt.Errorf("UpdateRepository: %v", err)
} }
} }
gitRepo.Close() repo, err = models.GetRepositoryByName(opts.RepoOwnerID, opts.RepoName)
if err != nil {
return fmt.Errorf("GetRepositoryByName [owner_id: %d, name: %s]: %v", opts.RepoOwnerID, opts.RepoName, err)
}
} }
} refName := git.RefEndName(opts.RefFullName)
// Change repository empty status and update last updated time. // Change default branch and empty status only if pushed ref is non-empty branch.
if err = models.UpdateRepository(repo, false); err != nil { if repo.IsEmpty && opts.NewCommitID != git.EmptySHA && strings.HasPrefix(opts.RefFullName, git.BranchPrefix) {
return fmt.Errorf("UpdateRepository: %v", err) repo.DefaultBranch = refName
} repo.IsEmpty = false
if refName != "master" {
isNewBranch := false gitRepo, err := git.OpenRepository(repo.RepoPath())
opType := models.ActionCommitRepo if err != nil {
// Check it's tag push or branch. return err
if strings.HasPrefix(opts.RefFullName, git.TagPrefix) { }
opType = models.ActionPushTag if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
if opts.NewCommitID == git.EmptySHA { if !git.IsErrUnsupportedVersion(err) {
opType = models.ActionDeleteTag gitRepo.Close()
return err
}
}
gitRepo.Close()
}
} }
opts.Commits = &models.PushCommits{}
} else if opts.NewCommitID == git.EmptySHA { isNewBranch := false
opType = models.ActionDeleteBranch opType := models.ActionCommitRepo
opts.Commits = &models.PushCommits{}
} else { // Check it's tag push or branch.
// if not the first commit, set the compare URL. if strings.HasPrefix(opts.RefFullName, git.TagPrefix) {
if opts.OldCommitID == git.EmptySHA { opType = models.ActionPushTag
isNewBranch = true if opts.NewCommitID == git.EmptySHA {
opType = models.ActionDeleteTag
}
opts.Commits = &models.PushCommits{}
} else if opts.NewCommitID == git.EmptySHA {
opType = models.ActionDeleteBranch
opts.Commits = &models.PushCommits{}
} else { } else {
opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID) // if not the first commit, set the compare URL.
if opts.OldCommitID == git.EmptySHA {
isNewBranch = true
} else {
opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
}
if err := UpdateIssuesCommit(pusher, repo, opts.Commits.Commits, refName); err != nil {
log.Error("updateIssuesCommit: %v", err)
}
} }
if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits, refName); err != nil { if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
log.Error("updateIssuesCommit: %v", err) opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
}
data, err := json.Marshal(opts.Commits)
if err != nil {
return fmt.Errorf("Marshal: %v", err)
}
actions[i] = &models.Action{
ActUserID: pusher.ID,
ActUser: pusher,
OpType: opType,
Content: string(data),
RepoID: repo.ID,
Repo: repo,
RefName: refName,
IsPrivate: repo.IsPrivate,
}
var isHookEventPush = true
switch opType {
case models.ActionCommitRepo: // Push
if isNewBranch {
notification.NotifyCreateRef(pusher, repo, "branch", opts.RefFullName)
}
case models.ActionDeleteBranch: // Delete Branch
notification.NotifyDeleteRef(pusher, repo, "branch", opts.RefFullName)
case models.ActionPushTag: // Create
notification.NotifyCreateRef(pusher, repo, "tag", opts.RefFullName)
case models.ActionDeleteTag: // Delete Tag
notification.NotifyDeleteRef(pusher, repo, "tag", opts.RefFullName)
default:
isHookEventPush = false
}
if isHookEventPush {
notification.NotifyPushCommits(pusher, repo, opts.RefFullName, opts.OldCommitID, opts.NewCommitID, opts.Commits)
} }
} }
if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum { if repo != nil {
opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum] // Change repository empty status and update last updated time.
if err := models.UpdateRepository(repo, false); err != nil {
return fmt.Errorf("UpdateRepository: %v", err)
}
} }
data, err := json.Marshal(opts.Commits) if err := models.NotifyWatchers(actions...); err != nil {
if err != nil {
return fmt.Errorf("Marshal: %v", err)
}
if err = models.NotifyWatchers(&models.Action{
ActUserID: pusher.ID,
ActUser: pusher,
OpType: opType,
Content: string(data),
RepoID: repo.ID,
Repo: repo,
RefName: refName,
IsPrivate: repo.IsPrivate,
}); err != nil {
return fmt.Errorf("NotifyWatchers: %v", err) return fmt.Errorf("NotifyWatchers: %v", err)
} }
var isHookEventPush = true
switch opType {
case models.ActionCommitRepo: // Push
if isNewBranch {
notification.NotifyCreateRef(pusher, repo, "branch", opts.RefFullName)
}
case models.ActionDeleteBranch: // Delete Branch
notification.NotifyDeleteRef(pusher, repo, "branch", opts.RefFullName)
case models.ActionPushTag: // Create
notification.NotifyCreateRef(pusher, repo, "tag", opts.RefFullName)
case models.ActionDeleteTag: // Delete Tag
notification.NotifyDeleteRef(pusher, repo, "tag", opts.RefFullName)
default:
isHookEventPush = false
}
if isHookEventPush {
notification.NotifyPushCommits(pusher, repo, opts.RefFullName, opts.OldCommitID, opts.NewCommitID, opts.Commits)
}
return nil return nil
} }

View File

@ -13,7 +13,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func testCorrectRepoAction(t *testing.T, opts CommitRepoActionOptions, actionBean *models.Action) { func testCorrectRepoAction(t *testing.T, opts *CommitRepoActionOptions, actionBean *models.Action) {
models.AssertNotExistsBean(t, actionBean) models.AssertNotExistsBean(t, actionBean)
assert.NoError(t, CommitRepoAction(opts)) assert.NoError(t, CommitRepoAction(opts))
models.AssertExistsAndLoadBean(t, actionBean) models.AssertExistsAndLoadBean(t, actionBean)
@ -121,7 +121,7 @@ func TestCommitRepoAction(t *testing.T) {
s.action.Repo = repo s.action.Repo = repo
s.action.IsPrivate = repo.IsPrivate s.action.IsPrivate = repo.IsPrivate
testCorrectRepoAction(t, s.commitRepoActionOptions, &s.action) testCorrectRepoAction(t, &s.commitRepoActionOptions, &s.action)
} }
} }

View File

@ -432,6 +432,7 @@ type PushUpdateOptions struct {
RefFullName string RefFullName string
OldCommitID string OldCommitID string
NewCommitID string NewCommitID string
Branch string
} }
// PushUpdate must be called for any push actions in order to // PushUpdate must be called for any push actions in order to
@ -460,60 +461,12 @@ func PushUpdate(repo *models.Repository, branch string, opts PushUpdateOptions)
log.Error("Failed to update size for repository: %v", err) log.Error("Failed to update size for repository: %v", err)
} }
var commits = &models.PushCommits{} commitRepoActionOptions, err := createCommitRepoActionOption(repo, gitRepo, &opts)
if strings.HasPrefix(opts.RefFullName, git.TagPrefix) { if err != nil {
// If is tag reference return err
tagName := opts.RefFullName[len(git.TagPrefix):]
if isDelRef {
err = models.PushUpdateDeleteTag(repo, tagName)
if err != nil {
return fmt.Errorf("PushUpdateDeleteTag: %v", err)
}
} else {
// Clear cache for tag commit count
cache.Remove(repo.GetCommitsCountCacheKey(tagName, true))
err = models.PushUpdateAddTag(repo, gitRepo, tagName)
if err != nil {
return fmt.Errorf("PushUpdateAddTag: %v", err)
}
}
} else if !isDelRef {
// If is branch reference
// Clear cache for branch commit count
cache.Remove(repo.GetCommitsCountCacheKey(opts.RefFullName[len(git.BranchPrefix):], true))
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil {
return fmt.Errorf("gitRepo.GetCommit: %v", err)
}
// Push new branch.
var l *list.List
if isNewRef {
l, err = newCommit.CommitsBeforeLimit(10)
if err != nil {
return fmt.Errorf("newCommit.CommitsBeforeLimit: %v", err)
}
} else {
l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
if err != nil {
return fmt.Errorf("newCommit.CommitsBeforeUntil: %v", err)
}
}
commits = models.ListToPushCommits(l)
} }
if err := CommitRepoAction(CommitRepoActionOptions{ if err := CommitRepoAction(commitRepoActionOptions); err != nil {
PusherName: opts.PusherName,
RepoOwnerID: repo.OwnerID,
RepoName: repo.Name,
RefFullName: opts.RefFullName,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: commits,
}); err != nil {
return fmt.Errorf("CommitRepoAction: %v", err) return fmt.Errorf("CommitRepoAction: %v", err)
} }
@ -532,3 +485,174 @@ func PushUpdate(repo *models.Repository, branch string, opts PushUpdateOptions)
return nil return nil
} }
// PushUpdates generates push action history feeds for push updating multiple refs
func PushUpdates(repo *models.Repository, optsList []*PushUpdateOptions) error {
repoPath := repo.RepoPath()
_, err := git.NewCommand("update-server-info").RunInDir(repoPath)
if err != nil {
return fmt.Errorf("Failed to call 'git update-server-info': %v", err)
}
gitRepo, err := git.OpenRepository(repoPath)
if err != nil {
return fmt.Errorf("OpenRepository: %v", err)
}
if err = repo.UpdateSize(); err != nil {
log.Error("Failed to update size for repository: %v", err)
}
actions, err := createCommitRepoActions(repo, gitRepo, optsList)
if err != nil {
return err
}
if err := CommitRepoAction(actions...); err != nil {
return fmt.Errorf("CommitRepoAction: %v", err)
}
var pusher *models.User
for _, opts := range optsList {
if pusher == nil || pusher.ID != opts.PusherID {
var err error
pusher, err = models.GetUserByID(opts.PusherID)
if err != nil {
return err
}
}
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, opts.Branch, pusher.Name)
go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true)
if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil {
log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err)
}
}
return nil
}
func createCommitRepoActions(repo *models.Repository, gitRepo *git.Repository, optsList []*PushUpdateOptions) ([]*CommitRepoActionOptions, error) {
addTags := make([]string, 0, len(optsList))
delTags := make([]string, 0, len(optsList))
actions := make([]*CommitRepoActionOptions, 0, len(optsList))
for _, opts := range optsList {
isNewRef := opts.OldCommitID == git.EmptySHA
isDelRef := opts.NewCommitID == git.EmptySHA
if isNewRef && isDelRef {
return nil, fmt.Errorf("Old and new revisions are both %s", git.EmptySHA)
}
var commits = &models.PushCommits{}
if strings.HasPrefix(opts.RefFullName, git.TagPrefix) {
// If is tag reference
tagName := opts.RefFullName[len(git.TagPrefix):]
if isDelRef {
delTags = append(delTags, tagName)
} else {
cache.Remove(repo.GetCommitsCountCacheKey(tagName, true))
addTags = append(addTags, tagName)
}
} else if !isDelRef {
// If is branch reference
// Clear cache for branch commit count
cache.Remove(repo.GetCommitsCountCacheKey(opts.RefFullName[len(git.BranchPrefix):], true))
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil {
return nil, fmt.Errorf("gitRepo.GetCommit: %v", err)
}
// Push new branch.
var l *list.List
if isNewRef {
l, err = newCommit.CommitsBeforeLimit(10)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeLimit: %v", err)
}
} else {
l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeUntil: %v", err)
}
}
commits = models.ListToPushCommits(l)
}
actions = append(actions, &CommitRepoActionOptions{
PusherName: opts.PusherName,
RepoOwnerID: repo.OwnerID,
RepoName: repo.Name,
RefFullName: opts.RefFullName,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: commits,
})
}
if err := models.PushUpdateAddDeleteTags(repo, gitRepo, addTags, delTags); err != nil {
return nil, fmt.Errorf("PushUpdateAddDeleteTags: %v", err)
}
return actions, nil
}
func createCommitRepoActionOption(repo *models.Repository, gitRepo *git.Repository, opts *PushUpdateOptions) (*CommitRepoActionOptions, error) {
isNewRef := opts.OldCommitID == git.EmptySHA
isDelRef := opts.NewCommitID == git.EmptySHA
if isNewRef && isDelRef {
return nil, fmt.Errorf("Old and new revisions are both %s", git.EmptySHA)
}
var commits = &models.PushCommits{}
if strings.HasPrefix(opts.RefFullName, git.TagPrefix) {
// If is tag reference
tagName := opts.RefFullName[len(git.TagPrefix):]
if isDelRef {
if err := models.PushUpdateDeleteTag(repo, tagName); err != nil {
return nil, fmt.Errorf("PushUpdateDeleteTag: %v", err)
}
} else {
// Clear cache for tag commit count
cache.Remove(repo.GetCommitsCountCacheKey(tagName, true))
if err := models.PushUpdateAddTag(repo, gitRepo, tagName); err != nil {
return nil, fmt.Errorf("PushUpdateAddTag: %v", err)
}
}
} else if !isDelRef {
// If is branch reference
// Clear cache for branch commit count
cache.Remove(repo.GetCommitsCountCacheKey(opts.RefFullName[len(git.BranchPrefix):], true))
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil {
return nil, fmt.Errorf("gitRepo.GetCommit: %v", err)
}
// Push new branch.
var l *list.List
if isNewRef {
l, err = newCommit.CommitsBeforeLimit(10)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeLimit: %v", err)
}
} else {
l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeUntil: %v", err)
}
}
commits = models.ListToPushCommits(l)
}
return &CommitRepoActionOptions{
PusherName: opts.PusherName,
RepoOwnerID: repo.OwnerID,
RepoName: repo.Name,
RefFullName: opts.RefFullName,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: commits,
}, nil
}

View File

@ -197,11 +197,11 @@ func SyncReleasesWithTags(repo *models.Repository, gitRepo *git.Repository) erro
} }
commitID, err := gitRepo.GetTagCommitID(rel.TagName) commitID, err := gitRepo.GetTagCommitID(rel.TagName)
if err != nil && !git.IsErrNotExist(err) { if err != nil && !git.IsErrNotExist(err) {
return fmt.Errorf("GetTagCommitID: %v", err) return fmt.Errorf("GetTagCommitID: %s: %v", rel.TagName, err)
} }
if git.IsErrNotExist(err) || commitID != rel.Sha1 { if git.IsErrNotExist(err) || commitID != rel.Sha1 {
if err := models.PushUpdateDeleteTag(repo, rel.TagName); err != nil { if err := models.PushUpdateDeleteTag(repo, rel.TagName); err != nil {
return fmt.Errorf("PushUpdateDeleteTag: %v", err) return fmt.Errorf("PushUpdateDeleteTag: %s: %v", rel.TagName, err)
} }
} else { } else {
existingRelTags[strings.ToLower(rel.TagName)] = struct{}{} existingRelTags[strings.ToLower(rel.TagName)] = struct{}{}
@ -215,7 +215,7 @@ func SyncReleasesWithTags(repo *models.Repository, gitRepo *git.Repository) erro
for _, tagName := range tags { for _, tagName := range tags {
if _, ok := existingRelTags[strings.ToLower(tagName)]; !ok { if _, ok := existingRelTags[strings.ToLower(tagName)]; !ok {
if err := models.PushUpdateAddTag(repo, gitRepo, tagName); err != nil { if err := models.PushUpdateAddTag(repo, gitRepo, tagName); err != nil {
return fmt.Errorf("pushUpdateAddTag: %v", err) return fmt.Errorf("pushUpdateAddTag: %s: %v", tagName, err)
} }
} }
} }

View File

@ -22,20 +22,9 @@ import (
) )
// HookPreReceive checks whether a individual commit is acceptable // HookPreReceive checks whether a individual commit is acceptable
func HookPreReceive(ctx *macaron.Context) { func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
ownerName := ctx.Params(":owner") ownerName := ctx.Params(":owner")
repoName := ctx.Params(":repo") repoName := ctx.Params(":repo")
oldCommitID := ctx.QueryTrim("old")
newCommitID := ctx.QueryTrim("new")
refFullName := ctx.QueryTrim("ref")
userID := ctx.QueryInt64("userID")
gitObjectDirectory := ctx.QueryTrim("gitObjectDirectory")
gitAlternativeObjectDirectories := ctx.QueryTrim("gitAlternativeObjectDirectories")
gitQuarantinePath := ctx.QueryTrim("gitQuarantinePath")
prID := ctx.QueryInt64("prID")
isDeployKey := ctx.QueryBool("isDeployKey")
branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil { if err != nil {
log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err) log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err)
@ -45,206 +34,304 @@ func HookPreReceive(ctx *macaron.Context) {
return return
} }
repo.OwnerName = ownerName repo.OwnerName = ownerName
protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
if err != nil { for i := range opts.OldCommitIDs {
log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) oldCommitID := opts.OldCommitIDs[i]
ctx.JSON(500, map[string]interface{}{ newCommitID := opts.NewCommitIDs[i]
"err": err.Error(), refFullName := opts.RefFullNames[i]
})
return branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
} protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
if protectBranch != nil && protectBranch.IsProtected() { if err != nil {
// check and deletion log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
if newCommitID == git.EmptySHA { ctx.JSON(500, map[string]interface{}{
log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) "err": err.Error(),
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("branch %s is protected from deletion", branchName),
}) })
return return
} }
if protectBranch != nil && protectBranch.IsProtected() {
// detect force push // check and deletion
if git.EmptySHA != oldCommitID { if newCommitID == git.EmptySHA {
env := os.Environ() log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
if gitAlternativeObjectDirectories != "" {
env = append(env,
private.GitAlternativeObjectDirectories+"="+gitAlternativeObjectDirectories)
}
if gitObjectDirectory != "" {
env = append(env,
private.GitObjectDirectory+"="+gitObjectDirectory)
}
if gitQuarantinePath != "" {
env = append(env,
private.GitQuarantinePath+"="+gitQuarantinePath)
}
output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env)
if err != nil {
log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Fail to detect force push: %v", err),
})
return
} else if len(output) > 0 {
log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
ctx.JSON(http.StatusForbidden, map[string]interface{}{ ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("branch %s is protected from force push", branchName), "err": fmt.Sprintf("branch %s is protected from deletion", branchName),
})
return
}
}
canPush := false
if isDeployKey {
canPush = protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
} else {
canPush = protectBranch.CanUserPush(userID)
}
if !canPush && prID > 0 {
pr, err := models.GetPullRequestByID(prID)
if err != nil {
log.Error("Unable to get PullRequest %d Error: %v", prID, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", prID, err),
}) })
return return
} }
if !protectBranch.HasEnoughApprovals(pr) {
log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v and pr #%d does not have enough approvals", userID, branchName, repo, pr.Index) // detect force push
if git.EmptySHA != oldCommitID {
env := os.Environ()
if opts.GitAlternativeObjectDirectories != "" {
env = append(env,
private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories)
}
if opts.GitObjectDirectory != "" {
env = append(env,
private.GitObjectDirectory+"="+opts.GitObjectDirectory)
}
if opts.GitQuarantinePath != "" {
env = append(env,
private.GitQuarantinePath+"="+opts.GitQuarantinePath)
}
output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env)
if err != nil {
log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Fail to detect force push: %v", err),
})
return
} else if len(output) > 0 {
log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("branch %s is protected from force push", branchName),
})
return
}
}
canPush := false
if opts.IsDeployKey {
canPush = protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
} else {
canPush = protectBranch.CanUserPush(opts.UserID)
}
if !canPush && opts.ProtectedBranchID > 0 {
pr, err := models.GetPullRequestByID(opts.ProtectedBranchID)
if err != nil {
log.Error("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err),
})
return
}
if !protectBranch.HasEnoughApprovals(pr) {
log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v and pr #%d does not have enough approvals", opts.UserID, branchName, repo, pr.Index)
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d does not have enough approvals", branchName, opts.ProtectedBranchID),
})
return
}
} else if !canPush {
log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", opts.UserID, branchName, repo)
ctx.JSON(http.StatusForbidden, map[string]interface{}{ ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d does not have enough approvals", branchName, prID), "err": fmt.Sprintf("protected branch %s can not be pushed to", branchName),
}) })
return return
} }
} else if !canPush {
log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", userID, branchName, repo)
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("protected branch %s can not be pushed to", branchName),
})
return
} }
} }
ctx.PlainText(http.StatusOK, []byte("ok")) ctx.PlainText(http.StatusOK, []byte("ok"))
} }
// HookPostReceive updates services and users // HookPostReceive updates services and users
func HookPostReceive(ctx *macaron.Context) { func HookPostReceive(ctx *macaron.Context, opts private.HookOptions) {
ownerName := ctx.Params(":owner") ownerName := ctx.Params(":owner")
repoName := ctx.Params(":repo") repoName := ctx.Params(":repo")
oldCommitID := ctx.Query("old")
newCommitID := ctx.Query("new")
refFullName := ctx.Query("ref")
userID := ctx.QueryInt64("userID")
userName := ctx.Query("username")
branch := refFullName var repo *models.Repository
if strings.HasPrefix(refFullName, git.BranchPrefix) { updates := make([]*repofiles.PushUpdateOptions, 0, len(opts.OldCommitIDs))
branch = strings.TrimPrefix(refFullName, git.BranchPrefix) wasEmpty := false
} else if strings.HasPrefix(refFullName, git.TagPrefix) {
branch = strings.TrimPrefix(refFullName, git.TagPrefix) for i := range opts.OldCommitIDs {
refFullName := opts.RefFullNames[i]
branch := opts.RefFullNames[i]
if strings.HasPrefix(branch, git.BranchPrefix) {
branch = strings.TrimPrefix(branch, git.BranchPrefix)
} else {
branch = strings.TrimPrefix(branch, git.TagPrefix)
}
// Only trigger activity updates for changes to branches or
// tags. Updates to other refs (eg, refs/notes, refs/changes,
// or other less-standard refs spaces are ignored since there
// may be a very large number of them).
if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
if repo == nil {
var err error
repo, err = models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil {
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if repo.OwnerName == "" {
repo.OwnerName = ownerName
}
wasEmpty = repo.IsEmpty
}
option := repofiles.PushUpdateOptions{
RefFullName: refFullName,
OldCommitID: opts.OldCommitIDs[i],
NewCommitID: opts.NewCommitIDs[i],
Branch: branch,
PusherID: opts.UserID,
PusherName: opts.UserName,
RepoUserName: ownerName,
RepoName: repoName,
}
updates = append(updates, &option)
if repo.IsEmpty && branch == "master" && strings.HasPrefix(refFullName, git.BranchPrefix) {
// put the master branch first
copy(updates[1:], updates)
updates[0] = &option
}
}
} }
// Only trigger activity updates for changes to branches or if repo != nil && len(updates) > 0 {
// tags. Updates to other refs (eg, refs/notes, refs/changes, if err := repofiles.PushUpdates(repo, updates); err != nil {
// or other less-standard refs spaces are ignored since there log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
// may be a very large number of them). for i, update := range updates {
if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.Branch)
repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) }
if err != nil { log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
"err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if err := repofiles.PushUpdate(repo, branch, repofiles.PushUpdateOptions{
RefFullName: refFullName,
OldCommitID: oldCommitID,
NewCommitID: newCommitID,
PusherID: userID,
PusherName: userName,
RepoUserName: ownerName,
RepoName: repoName,
}); err != nil {
log.Error("Failed to Update: %s/%s Branch: %s Error: %v", ownerName, repoName, branch, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Failed to Update: %s/%s Branch: %s Error: %v", ownerName, repoName, branch, err),
}) })
return return
} }
} }
if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) { results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil {
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
repo.OwnerName = ownerName
pullRequestAllowed := repo.AllowsPulls() // We have to reload the repo in case its state is changed above
if !pullRequestAllowed { repo = nil
ctx.JSON(http.StatusOK, map[string]interface{}{ var baseRepo *models.Repository
"message": false,
})
return
}
baseRepo := repo for i := range opts.OldCommitIDs {
if repo.IsFork { refFullName := opts.RefFullNames[i]
if err := repo.GetBaseRepo(); err != nil { newCommitID := opts.NewCommitIDs[i]
log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ branch := git.RefEndName(opts.RefFullNames[i])
"err": fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) {
if repo == nil {
var err error
repo, err = models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil {
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
RepoWasEmpty: wasEmpty,
})
return
}
if repo.OwnerName == "" {
repo.OwnerName = ownerName
}
if !repo.AllowsPulls() {
// We can stop there's no need to go any further
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
RepoWasEmpty: wasEmpty,
})
return
}
baseRepo = repo
if repo.IsFork {
if err := repo.GetBaseRepo(); err != nil {
log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
RepoWasEmpty: wasEmpty,
})
return
}
baseRepo = repo.BaseRepo
}
}
if !repo.IsFork && branch == baseRepo.DefaultBranch {
results = append(results, private.HookPostReceiveBranchResult{})
continue
}
pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch)
if err != nil && !models.IsErrPullRequestNotExist(err) {
log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf(
"Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
RepoWasEmpty: wasEmpty,
}) })
return return
} }
baseRepo = repo.BaseRepo
}
if !repo.IsFork && branch == baseRepo.DefaultBranch { if pr == nil {
ctx.JSON(http.StatusOK, map[string]interface{}{ if repo.IsFork {
"message": false, branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
}) }
return results = append(results, private.HookPostReceiveBranchResult{
} Message: true,
Create: true,
pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch) Branch: branch,
if err != nil && !models.IsErrPullRequestNotExist(err) { URL: fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)),
log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err) })
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ } else {
"err": fmt.Sprintf( results = append(results, private.HookPostReceiveBranchResult{
"Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err), Message: true,
}) Create: false,
return Branch: branch,
} URL: fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
})
if pr == nil {
if repo.IsFork {
branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
} }
ctx.JSON(http.StatusOK, map[string]interface{}{
"message": true,
"create": true,
"branch": branch,
"url": fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)),
})
} else {
ctx.JSON(http.StatusOK, map[string]interface{}{
"message": true,
"create": false,
"branch": branch,
"url": fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
})
} }
return
} }
ctx.JSON(http.StatusOK, map[string]interface{}{ ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
"message": false, Results: results,
RepoWasEmpty: wasEmpty,
}) })
} }
// SetDefaultBranch updates the default branch
func SetDefaultBranch(ctx *macaron.Context) {
ownerName := ctx.Params(":owner")
repoName := ctx.Params(":repo")
branch := ctx.Params(":branch")
repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil {
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if repo.OwnerName == "" {
repo.OwnerName = ownerName
}
repo.DefaultBranch = branch
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"Err": fmt.Sprintf("Failed to get git repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
gitRepo.Close()
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"Err": fmt.Sprintf("Unable to set default branch onrepository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
}
gitRepo.Close()
if err := repo.UpdateDefaultBranch(); err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"Err": fmt.Sprintf("Unable to set default branch onrepository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
ctx.PlainText(200, []byte("success"))
}

View File

@ -10,8 +10,10 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"gitea.com/macaron/binding"
"gitea.com/macaron/macaron" "gitea.com/macaron/macaron"
) )
@ -77,11 +79,14 @@ func CheckUnitUser(ctx *macaron.Context) {
// RegisterRoutes registers all internal APIs routes to web application. // RegisterRoutes registers all internal APIs routes to web application.
// These APIs will be invoked by internal commands for example `gitea serv` and etc. // These APIs will be invoked by internal commands for example `gitea serv` and etc.
func RegisterRoutes(m *macaron.Macaron) { func RegisterRoutes(m *macaron.Macaron) {
bind := binding.Bind
m.Group("/", func() { m.Group("/", func() {
m.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent) m.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
m.Post("/ssh/:id/update/:repoid", UpdatePublicKeyInRepo) m.Post("/ssh/:id/update/:repoid", UpdatePublicKeyInRepo)
m.Get("/hook/pre-receive/:owner/:repo", HookPreReceive) m.Post("/hook/pre-receive/:owner/:repo", bind(private.HookOptions{}), HookPreReceive)
m.Get("/hook/post-receive/:owner/:repo", HookPostReceive) m.Post("/hook/post-receive/:owner/:repo", bind(private.HookOptions{}), HookPostReceive)
m.Post("/hook/set-default-branch/:owner/:repo/:branch", SetDefaultBranch)
m.Get("/serv/none/:keyid", ServNoCommand) m.Get("/serv/none/:keyid", ServNoCommand)
m.Get("/serv/command/:keyid/:owner/:repo", ServCommand) m.Get("/serv/command/:keyid/:owner/:repo", ServCommand)
}, CheckInternalToken) }, CheckInternalToken)