0d09acf669
If a repository has
git config --add push.pushOption submit=".sourcehut/*.yml"
it failed when pushed because of the unknown submit push
option. It will be ignored instead.
Filtering out the push options is done in an earlier stage, when the
hook command runs, before it submits the options map to the private
endpoint.
* move all the push options logic to modules/git/pushoptions
* add 100% test coverage for modules/git/pushoptions
Test coverage for the code paths from which code was moved to the
modules/git/pushoptions package:
* cmd/hook.go:runHookPreReceive
* routers/private/hook_pre_receive.go:validatePushOptions
tests/integration/git_push_test.go:TestOptionsGitPush runs through
both. The test verifying the option is rejected was removed and, if
added again, will fail because the option is now ignored instead of
being rejected.
* cmd/hook.go:runHookProcReceive
* services/agit/agit.go:ProcReceive
tests/integration/git_test.go: doCreateAgitFlowPull runs through
both. It uses variations of AGit related push options.
* cmd/hook.go:runHookPostReceive
* routers/private/hook_post_receive.go:HookPostReceive
tests/integration/git_test.go:doPushCreate called by TestGit/HTTP/sha1/PushCreate
runs through both.
Note that although it provides coverage for this code path it does not use push options.
Fixes: https://codeberg.org/forgejo/forgejo/issues/3651
(cherry picked from commit 5561e80b04
)
795 lines
21 KiB
Go
795 lines
21 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package cmd
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"code.gitea.io/gitea/modules/git"
|
||
"code.gitea.io/gitea/modules/git/pushoptions"
|
||
"code.gitea.io/gitea/modules/log"
|
||
"code.gitea.io/gitea/modules/private"
|
||
repo_module "code.gitea.io/gitea/modules/repository"
|
||
"code.gitea.io/gitea/modules/setting"
|
||
|
||
"github.com/urfave/cli/v2"
|
||
)
|
||
|
||
const (
|
||
hookBatchSize = 30
|
||
)
|
||
|
||
var (
|
||
// CmdHook represents the available hooks sub-command.
|
||
CmdHook = &cli.Command{
|
||
Name: "hook",
|
||
Usage: "(internal) Should only be called by Git",
|
||
Description: "Delegate commands to corresponding Git hooks",
|
||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||
Subcommands: []*cli.Command{
|
||
subcmdHookPreReceive,
|
||
subcmdHookUpdate,
|
||
subcmdHookPostReceive,
|
||
subcmdHookProcReceive,
|
||
},
|
||
}
|
||
|
||
subcmdHookPreReceive = &cli.Command{
|
||
Name: "pre-receive",
|
||
Usage: "Delegate pre-receive Git hook",
|
||
Description: "This command should only be called by Git",
|
||
Action: runHookPreReceive,
|
||
Flags: []cli.Flag{
|
||
&cli.BoolFlag{
|
||
Name: "debug",
|
||
},
|
||
},
|
||
}
|
||
subcmdHookUpdate = &cli.Command{
|
||
Name: "update",
|
||
Usage: "Delegate update Git hook",
|
||
Description: "This command should only be called by Git",
|
||
Action: runHookUpdate,
|
||
Flags: []cli.Flag{
|
||
&cli.BoolFlag{
|
||
Name: "debug",
|
||
},
|
||
},
|
||
}
|
||
subcmdHookPostReceive = &cli.Command{
|
||
Name: "post-receive",
|
||
Usage: "Delegate post-receive Git hook",
|
||
Description: "This command should only be called by Git",
|
||
Action: runHookPostReceive,
|
||
Flags: []cli.Flag{
|
||
&cli.BoolFlag{
|
||
Name: "debug",
|
||
},
|
||
},
|
||
}
|
||
// Note: new hook since git 2.29
|
||
subcmdHookProcReceive = &cli.Command{
|
||
Name: "proc-receive",
|
||
Usage: "Delegate proc-receive Git hook",
|
||
Description: "This command should only be called by Git",
|
||
Action: runHookProcReceive,
|
||
Flags: []cli.Flag{
|
||
&cli.BoolFlag{
|
||
Name: "debug",
|
||
},
|
||
},
|
||
}
|
||
)
|
||
|
||
type delayWriter struct {
|
||
internal io.Writer
|
||
buf *bytes.Buffer
|
||
timer *time.Timer
|
||
}
|
||
|
||
func newDelayWriter(internal io.Writer, delay time.Duration) *delayWriter {
|
||
timer := time.NewTimer(delay)
|
||
return &delayWriter{
|
||
internal: internal,
|
||
buf: &bytes.Buffer{},
|
||
timer: timer,
|
||
}
|
||
}
|
||
|
||
func (d *delayWriter) Write(p []byte) (n int, err error) {
|
||
if d.buf != nil {
|
||
select {
|
||
case <-d.timer.C:
|
||
_, err := d.internal.Write(d.buf.Bytes())
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
d.buf = nil
|
||
return d.internal.Write(p)
|
||
default:
|
||
return d.buf.Write(p)
|
||
}
|
||
}
|
||
return d.internal.Write(p)
|
||
}
|
||
|
||
func (d *delayWriter) WriteString(s string) (n int, err error) {
|
||
if d.buf != nil {
|
||
select {
|
||
case <-d.timer.C:
|
||
_, err := d.internal.Write(d.buf.Bytes())
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
d.buf = nil
|
||
return d.internal.Write([]byte(s))
|
||
default:
|
||
return d.buf.WriteString(s)
|
||
}
|
||
}
|
||
return d.internal.Write([]byte(s))
|
||
}
|
||
|
||
func (d *delayWriter) Close() error {
|
||
if d.timer.Stop() {
|
||
d.buf = nil
|
||
}
|
||
if d.buf == nil {
|
||
return nil
|
||
}
|
||
_, err := d.internal.Write(d.buf.Bytes())
|
||
d.buf = nil
|
||
return err
|
||
}
|
||
|
||
type nilWriter struct{}
|
||
|
||
func (n *nilWriter) Write(p []byte) (int, error) {
|
||
return len(p), nil
|
||
}
|
||
|
||
func (n *nilWriter) WriteString(s string) (int, error) {
|
||
return len(s), nil
|
||
}
|
||
|
||
func runHookPreReceive(c *cli.Context) error {
|
||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||
return nil
|
||
}
|
||
ctx, cancel := installSignals()
|
||
defer cancel()
|
||
|
||
setup(ctx, c.Bool("debug"))
|
||
|
||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
|
||
return fail(ctx, `Rejecting changes as Forgejo environment not set.
|
||
If you are pushing over SSH you must push with a key managed by
|
||
Forgejo or set your environment appropriately.`, "")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// the environment is set by serv command
|
||
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
||
username := os.Getenv(repo_module.EnvRepoUsername)
|
||
reponame := os.Getenv(repo_module.EnvRepoName)
|
||
userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||
prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
|
||
deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64)
|
||
actionPerm, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionPerm), 10, 64)
|
||
|
||
hookOptions := private.HookOptions{
|
||
UserID: userID,
|
||
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
|
||
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
|
||
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
|
||
GitPushOptions: pushoptions.New().ReadEnv().Map(),
|
||
PullRequestID: prID,
|
||
DeployKeyID: deployKeyID,
|
||
ActionPerm: int(actionPerm),
|
||
}
|
||
|
||
scanner := bufio.NewScanner(os.Stdin)
|
||
|
||
oldCommitIDs := make([]string, hookBatchSize)
|
||
newCommitIDs := make([]string, hookBatchSize)
|
||
refFullNames := make([]git.RefName, hookBatchSize)
|
||
count := 0
|
||
total := 0
|
||
lastline := 0
|
||
|
||
var out io.Writer
|
||
out = &nilWriter{}
|
||
if setting.Git.VerbosePush {
|
||
if setting.Git.VerbosePushDelay > 0 {
|
||
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
|
||
defer dWriter.Close()
|
||
out = dWriter
|
||
} else {
|
||
out = os.Stdout
|
||
}
|
||
}
|
||
|
||
supportProcReceive := false
|
||
if git.CheckGitVersionAtLeast("2.29") == nil {
|
||
supportProcReceive = true
|
||
}
|
||
|
||
for scanner.Scan() {
|
||
// TODO: support news feeds for wiki
|
||
if isWiki {
|
||
continue
|
||
}
|
||
|
||
fields := bytes.Fields(scanner.Bytes())
|
||
if len(fields) != 3 {
|
||
continue
|
||
}
|
||
|
||
oldCommitID := string(fields[0])
|
||
newCommitID := string(fields[1])
|
||
refFullName := git.RefName(fields[2])
|
||
total++
|
||
lastline++
|
||
|
||
// If the ref is a branch or tag, check if it's protected
|
||
// if supportProcReceive all ref should be checked because
|
||
// permission check was delayed
|
||
if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
|
||
oldCommitIDs[count] = oldCommitID
|
||
newCommitIDs[count] = newCommitID
|
||
refFullNames[count] = refFullName
|
||
count++
|
||
fmt.Fprintf(out, "*")
|
||
|
||
if count >= hookBatchSize {
|
||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||
|
||
hookOptions.OldCommitIDs = oldCommitIDs
|
||
hookOptions.NewCommitIDs = newCommitIDs
|
||
hookOptions.RefFullNames = refFullNames
|
||
extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
|
||
if extra.HasError() {
|
||
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
|
||
}
|
||
count = 0
|
||
lastline = 0
|
||
}
|
||
} else {
|
||
fmt.Fprintf(out, ".")
|
||
}
|
||
if lastline >= hookBatchSize {
|
||
fmt.Fprintf(out, "\n")
|
||
lastline = 0
|
||
}
|
||
}
|
||
|
||
if count > 0 {
|
||
hookOptions.OldCommitIDs = oldCommitIDs[:count]
|
||
hookOptions.NewCommitIDs = newCommitIDs[:count]
|
||
hookOptions.RefFullNames = refFullNames[:count]
|
||
|
||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||
|
||
extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
|
||
if extra.HasError() {
|
||
return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
|
||
}
|
||
} else if lastline > 0 {
|
||
fmt.Fprintf(out, "\n")
|
||
}
|
||
|
||
fmt.Fprintf(out, "Checked %d references in total\n", total)
|
||
return nil
|
||
}
|
||
|
||
// runHookUpdate process the update hook: https://git-scm.com/docs/githooks#update
|
||
func runHookUpdate(c *cli.Context) error {
|
||
// Now if we're an internal don't do anything else
|
||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||
return nil
|
||
}
|
||
|
||
ctx, cancel := installSignals()
|
||
defer cancel()
|
||
|
||
if c.NArg() != 3 {
|
||
return nil
|
||
}
|
||
args := c.Args().Slice()
|
||
|
||
// The arguments given to the hook are in order: reference name, old commit ID and new commit ID.
|
||
refFullName := git.RefName(args[0])
|
||
newCommitID := args[2]
|
||
|
||
// Only process pull references.
|
||
if !refFullName.IsPull() {
|
||
return nil
|
||
}
|
||
|
||
// Empty new commit ID means deletion.
|
||
if git.IsEmptyCommitID(newCommitID, nil) {
|
||
return fail(ctx, fmt.Sprintf("The deletion of %s is skipped as it's an internal reference.", refFullName), "")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func runHookPostReceive(c *cli.Context) error {
|
||
ctx, cancel := installSignals()
|
||
defer cancel()
|
||
|
||
setup(ctx, c.Bool("debug"))
|
||
|
||
// First of all run update-server-info no matter what
|
||
if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil {
|
||
return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
|
||
}
|
||
|
||
// Now if we're an internal don't do anything else
|
||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||
return nil
|
||
}
|
||
|
||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
|
||
return fail(ctx, `Rejecting changes as Forgejo environment not set.
|
||
If you are pushing over SSH you must push with a key managed by
|
||
Forgejo or set your environment appropriately.`, "")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
var out io.Writer
|
||
out = &nilWriter{}
|
||
if setting.Git.VerbosePush {
|
||
if setting.Git.VerbosePushDelay > 0 {
|
||
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
|
||
defer dWriter.Close()
|
||
out = dWriter
|
||
} else {
|
||
out = os.Stdout
|
||
}
|
||
}
|
||
|
||
// the environment is set by serv command
|
||
repoUser := os.Getenv(repo_module.EnvRepoUsername)
|
||
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
||
repoName := os.Getenv(repo_module.EnvRepoName)
|
||
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||
prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
|
||
pusherName := os.Getenv(repo_module.EnvPusherName)
|
||
|
||
hookOptions := private.HookOptions{
|
||
UserName: pusherName,
|
||
UserID: pusherID,
|
||
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
|
||
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
|
||
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
|
||
GitPushOptions: pushoptions.New().ReadEnv().Map(),
|
||
PullRequestID: prID,
|
||
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
|
||
}
|
||
oldCommitIDs := make([]string, hookBatchSize)
|
||
newCommitIDs := make([]string, hookBatchSize)
|
||
refFullNames := make([]git.RefName, hookBatchSize)
|
||
count := 0
|
||
total := 0
|
||
wasEmpty := false
|
||
masterPushed := false
|
||
results := make([]private.HookPostReceiveBranchResult, 0)
|
||
|
||
scanner := bufio.NewScanner(os.Stdin)
|
||
for scanner.Scan() {
|
||
// TODO: support news feeds for wiki
|
||
if isWiki {
|
||
continue
|
||
}
|
||
|
||
fields := bytes.Fields(scanner.Bytes())
|
||
if len(fields) != 3 {
|
||
continue
|
||
}
|
||
|
||
fmt.Fprintf(out, ".")
|
||
oldCommitIDs[count] = string(fields[0])
|
||
newCommitIDs[count] = string(fields[1])
|
||
refFullNames[count] = git.RefName(fields[2])
|
||
|
||
if refFullNames[count] == git.BranchPrefix+"master" && !git.IsEmptyCommitID(newCommitIDs[count], nil) && count == total {
|
||
masterPushed = true
|
||
}
|
||
count++
|
||
total++
|
||
|
||
if count >= hookBatchSize {
|
||
fmt.Fprintf(out, " Processing %d references\n", count)
|
||
hookOptions.OldCommitIDs = oldCommitIDs
|
||
hookOptions.NewCommitIDs = newCommitIDs
|
||
hookOptions.RefFullNames = refFullNames
|
||
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
|
||
if extra.HasError() {
|
||
hookPrintResults(results)
|
||
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
|
||
}
|
||
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
|
||
extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
|
||
if extra.HasError() {
|
||
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
|
||
}
|
||
}
|
||
fmt.Fprintf(out, "Processed %d references in total\n", total)
|
||
|
||
hookPrintResults(results)
|
||
return nil
|
||
}
|
||
|
||
hookOptions.OldCommitIDs = oldCommitIDs[:count]
|
||
hookOptions.NewCommitIDs = newCommitIDs[:count]
|
||
hookOptions.RefFullNames = refFullNames[:count]
|
||
|
||
fmt.Fprintf(out, " Processing %d references\n", count)
|
||
|
||
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
|
||
if resp == nil {
|
||
hookPrintResults(results)
|
||
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
|
||
}
|
||
wasEmpty = wasEmpty || resp.RepoWasEmpty
|
||
results = append(results, resp.Results...)
|
||
|
||
fmt.Fprintf(out, "Processed %d references in total\n", total)
|
||
|
||
if wasEmpty && masterPushed {
|
||
// We need to tell the repo to reset the default branch to master
|
||
extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
|
||
if extra.HasError() {
|
||
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
|
||
}
|
||
}
|
||
|
||
hookPrintResults(results)
|
||
return nil
|
||
}
|
||
|
||
func hookPrintResults(results []private.HookPostReceiveBranchResult) {
|
||
for _, res := range results {
|
||
if !res.Message {
|
||
continue
|
||
}
|
||
|
||
fmt.Fprintln(os.Stderr, "")
|
||
if res.Create {
|
||
fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res.Branch)
|
||
fmt.Fprintf(os.Stderr, " %s\n", res.URL)
|
||
} else {
|
||
fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
|
||
fmt.Fprintf(os.Stderr, " %s\n", res.URL)
|
||
}
|
||
fmt.Fprintln(os.Stderr, "")
|
||
_ = os.Stderr.Sync()
|
||
}
|
||
}
|
||
|
||
func runHookProcReceive(c *cli.Context) error {
|
||
ctx, cancel := installSignals()
|
||
defer cancel()
|
||
|
||
setup(ctx, c.Bool("debug"))
|
||
|
||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
|
||
return fail(ctx, `Rejecting changes as Forgejo environment not set.
|
||
If you are pushing over SSH you must push with a key managed by
|
||
Forgejo or set your environment appropriately.`, "")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
if git.CheckGitVersionAtLeast("2.29") != nil {
|
||
return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
|
||
}
|
||
|
||
reader := bufio.NewReader(os.Stdin)
|
||
repoUser := os.Getenv(repo_module.EnvRepoUsername)
|
||
repoName := os.Getenv(repo_module.EnvRepoName)
|
||
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||
pusherName := os.Getenv(repo_module.EnvPusherName)
|
||
|
||
// 1. Version and features negotiation.
|
||
// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n)
|
||
// S: flush-pkt
|
||
// H: PKT-LINE(version=1\0push-options...)
|
||
// H: flush-pkt
|
||
|
||
rs, err := readPktLine(ctx, reader, pktLineTypeData)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
const VersionHead string = "version=1"
|
||
|
||
var (
|
||
hasPushOptions bool
|
||
response = []byte(VersionHead)
|
||
requestOptions []string
|
||
)
|
||
|
||
index := bytes.IndexByte(rs.Data, byte(0))
|
||
if index >= len(rs.Data) {
|
||
return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data))
|
||
}
|
||
|
||
if index < 0 {
|
||
if len(rs.Data) == 10 && rs.Data[9] == '\n' {
|
||
index = 9
|
||
} else {
|
||
return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data))
|
||
}
|
||
}
|
||
|
||
if string(rs.Data[0:index]) != VersionHead {
|
||
return fail(ctx, "Protocol: version error", "Received unsupported version: %s", string(rs.Data[0:index]))
|
||
}
|
||
requestOptions = strings.Split(string(rs.Data[index+1:]), " ")
|
||
|
||
for _, option := range requestOptions {
|
||
if strings.HasPrefix(option, "push-options") {
|
||
response = append(response, byte(0))
|
||
response = append(response, []byte("push-options")...)
|
||
hasPushOptions = true
|
||
}
|
||
}
|
||
response = append(response, '\n')
|
||
|
||
_, err = readPktLine(ctx, reader, pktLineTypeFlush)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
err = writeDataPktLine(ctx, os.Stdout, response)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
err = writeFlushPktLine(ctx, os.Stdout)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 2. receive commands from server.
|
||
// S: PKT-LINE(<old-oid> <new-oid> <ref>)
|
||
// S: ... ...
|
||
// S: flush-pkt
|
||
// # [receive push-options]
|
||
// S: PKT-LINE(push-option)
|
||
// S: ... ...
|
||
// S: flush-pkt
|
||
hookOptions := private.HookOptions{
|
||
UserName: pusherName,
|
||
UserID: pusherID,
|
||
}
|
||
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
|
||
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
|
||
hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize)
|
||
|
||
for {
|
||
// note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed
|
||
rs, err = readPktLine(ctx, reader, pktLineTypeUnknown)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if rs.Type == pktLineTypeFlush {
|
||
break
|
||
}
|
||
t := strings.SplitN(string(rs.Data), " ", 3)
|
||
if len(t) != 3 {
|
||
continue
|
||
}
|
||
hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0])
|
||
hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1])
|
||
hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2]))
|
||
}
|
||
|
||
hookOptions.GitPushOptions = make(map[string]string)
|
||
|
||
if hasPushOptions {
|
||
pushOptions := pushoptions.NewFromMap(&hookOptions.GitPushOptions)
|
||
for {
|
||
rs, err = readPktLine(ctx, reader, pktLineTypeUnknown)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if rs.Type == pktLineTypeFlush {
|
||
break
|
||
}
|
||
pushOptions.Parse(string(rs.Data))
|
||
}
|
||
}
|
||
|
||
// 3. run hook
|
||
resp, extra := private.HookProcReceive(ctx, repoUser, repoName, hookOptions)
|
||
if extra.HasError() {
|
||
return fail(ctx, extra.UserMsg, "HookProcReceive failed: %v", extra.Error)
|
||
}
|
||
|
||
// 4. response result to service
|
||
// # a. OK, but has an alternate reference. The alternate reference name
|
||
// # and other status can be given in option directives.
|
||
// H: PKT-LINE(ok <ref>)
|
||
// H: PKT-LINE(option refname <refname>)
|
||
// H: PKT-LINE(option old-oid <old-oid>)
|
||
// H: PKT-LINE(option new-oid <new-oid>)
|
||
// H: PKT-LINE(option forced-update)
|
||
// H: ... ...
|
||
// H: flush-pkt
|
||
// # b. NO, I reject it.
|
||
// H: PKT-LINE(ng <ref> <reason>)
|
||
// # c. Fall through, let 'receive-pack' to execute it.
|
||
// H: PKT-LINE(ok <ref>)
|
||
// H: PKT-LINE(option fall-through)
|
||
|
||
for _, rs := range resp.Results {
|
||
if len(rs.Err) > 0 {
|
||
err = writeDataPktLine(ctx, os.Stdout, []byte("ng "+rs.OriginalRef.String()+" "+rs.Err))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
|
||
if rs.IsNotMatched {
|
||
err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef.String()))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
err = writeDataPktLine(ctx, os.Stdout, []byte("option fall-through"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
|
||
err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
err = writeDataPktLine(ctx, os.Stdout, []byte("option refname "+rs.Ref))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !git.IsEmptyCommitID(rs.OldOID, nil) {
|
||
err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
err = writeDataPktLine(ctx, os.Stdout, []byte("option new-oid "+rs.NewOID))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if rs.IsForcePush {
|
||
err = writeDataPktLine(ctx, os.Stdout, []byte("option forced-update"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
err = writeFlushPktLine(ctx, os.Stdout)
|
||
|
||
return err
|
||
}
|
||
|
||
// git PKT-Line api
|
||
// pktLineType message type of pkt-line
|
||
type pktLineType int64
|
||
|
||
const (
|
||
// Unknown type
|
||
pktLineTypeUnknown pktLineType = 0
|
||
// flush-pkt "0000"
|
||
pktLineTypeFlush pktLineType = iota
|
||
// data line
|
||
pktLineTypeData
|
||
)
|
||
|
||
// gitPktLine pkt-line api
|
||
type gitPktLine struct {
|
||
Type pktLineType
|
||
Length uint64
|
||
Data []byte
|
||
}
|
||
|
||
// Reads an Pkt-Line from `in`. If requestType is not unknown, it will a
|
||
func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) {
|
||
// Read length prefix
|
||
lengthBytes := make([]byte, 4)
|
||
if n, err := in.Read(lengthBytes); n != 4 || err != nil {
|
||
return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err)
|
||
}
|
||
|
||
var err error
|
||
r := &gitPktLine{}
|
||
r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32)
|
||
if err != nil {
|
||
return nil, fail(ctx, "Protocol: format parse error", "Pkt-Line format is wrong :%v", err)
|
||
}
|
||
|
||
if r.Length == 0 {
|
||
if requestType == pktLineTypeData {
|
||
return nil, fail(ctx, "Protocol: format data error", "Pkt-Line format is wrong")
|
||
}
|
||
r.Type = pktLineTypeFlush
|
||
return r, nil
|
||
}
|
||
|
||
if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush {
|
||
return nil, fail(ctx, "Protocol: format length error", "Pkt-Line format is wrong")
|
||
}
|
||
|
||
r.Data = make([]byte, r.Length-4)
|
||
if n, err := io.ReadFull(in, r.Data); uint64(n) != r.Length-4 || err != nil {
|
||
return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err)
|
||
}
|
||
|
||
r.Type = pktLineTypeData
|
||
|
||
return r, nil
|
||
}
|
||
|
||
func writeFlushPktLine(ctx context.Context, out io.Writer) error {
|
||
l, err := out.Write([]byte("0000"))
|
||
if err != nil || l != 4 {
|
||
return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Write an Pkt-Line based on `data` to `out` according to the specification.
|
||
// https://git-scm.com/docs/protocol-common
|
||
func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error {
|
||
// Implementations SHOULD NOT send an empty pkt-line ("0004").
|
||
if len(data) == 0 {
|
||
return fail(ctx, "Protocol: write error", "Not allowed to write empty Pkt-Line")
|
||
}
|
||
|
||
length := uint64(len(data) + 4)
|
||
|
||
// The maximum length of a pkt-line’s data component is 65516 bytes.
|
||
// Implementations MUST NOT send pkt-line whose length exceeds 65520 (65516 bytes of payload + 4 bytes of length data).
|
||
if length > 65520 {
|
||
return fail(ctx, "Protocol: write error", "Pkt-Line exceeds maximum of 65520 bytes")
|
||
}
|
||
|
||
lr, err := fmt.Fprintf(out, "%04x", length)
|
||
if err != nil || lr != 4 {
|
||
return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
|
||
}
|
||
|
||
lr, err = out.Write(data)
|
||
if err != nil || int(length-4) != lr {
|
||
return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|