84e77c41fa
- This a port of https://github.com/go-gitea/gitea/pull/31931 in a behavior-sense. None of the code was actually ported. - Follow up for #2834, now also don't allow modification. - Integration test added. - Unit test modified.
796 lines
22 KiB
Go
796 lines
22 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), "")
|
||
}
|
||
|
||
// If the new comment isn't empty it means modification.
|
||
return fail(ctx, fmt.Sprintf("The modification of %s is skipped as it's an internal reference.", refFullName), "")
|
||
}
|
||
|
||
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 %s", 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 %s", 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
|
||
}
|