mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-09 04:30:16 +09:00
20ea3e204d
Creating a repository with an initial commit, via `auto_init` or a template with git content, pushed that commit through receive-pack and three server-side hooks that do nothing for this internal push except refresh server info. Fetching it into the bare repository instead makes such repository creation about 3.7x faster locally (median 790ms to 214ms). E2E tests that create initialized repositories get about 0.2-0.3s faster each. 1. Run `update-server-info` after template content generation, which the push's `post-receive` hook did before. 2. Remove the now unused `InternalPushingEnvironment` and `PushOptions.LocalRefName`.
746 lines
19 KiB
Go
746 lines
19 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"
|
|
|
|
"gitea.dev/modules/git"
|
|
"gitea.dev/modules/git/gitcmd"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/private"
|
|
repo_module "gitea.dev/modules/repository"
|
|
"gitea.dev/modules/setting"
|
|
|
|
"github.com/urfave/cli/v3"
|
|
)
|
|
|
|
const (
|
|
hookBatchSize = 500
|
|
)
|
|
|
|
func newHookCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "hook",
|
|
Usage: "(internal) Should only be called by Git",
|
|
Hidden: true, // internal commands shouldn't be visible
|
|
Description: "Delegate commands to corresponding Git hooks",
|
|
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
|
Commands: []*cli.Command{
|
|
newHookPreReceiveCommand(),
|
|
newHookUpdateCommand(),
|
|
newHookPostReceiveCommand(),
|
|
newHookProcReceiveCommand(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func newHookPreReceiveCommand() *cli.Command {
|
|
return &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",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func newHookUpdateCommand() *cli.Command {
|
|
return &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",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func newHookPostReceiveCommand() *cli.Command {
|
|
return &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
|
|
func newHookProcReceiveCommand() *cli.Command {
|
|
return &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 {
|
|
stopped := d.timer.Stop()
|
|
if stopped || d.buf == nil {
|
|
return nil
|
|
}
|
|
_, err := d.internal.Write(d.buf.Bytes())
|
|
d.buf = nil
|
|
return err
|
|
}
|
|
|
|
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
|
|
fields := strings.Split(line, " ")
|
|
if len(fields) != 3 {
|
|
return "", "", "", false
|
|
}
|
|
return fields[0], fields[1], git.RefName(fields[2]), true
|
|
}
|
|
|
|
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
|
|
setup(ctx, c.Bool("debug"))
|
|
|
|
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
|
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
|
|
return fail(ctx, `Rejecting changes as Gitea environment not set.
|
|
If you are pushing over SSH you must push with a key managed by
|
|
Gitea or set your environment appropriately.`, "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// the environment is set by serv command
|
|
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
|
ownerName := 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)
|
|
|
|
hookOptions := private.HookOptions{
|
|
IsWiki: isWiki,
|
|
|
|
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
|
|
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
|
|
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
|
|
GitPushOptions: pushOptions(),
|
|
|
|
PullRequestID: prID,
|
|
|
|
UserID: userID,
|
|
UserName: os.Getenv(repo_module.EnvPusherName),
|
|
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
|
|
}
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
|
|
oldCommitIDs := make([]string, hookBatchSize)
|
|
newCommitIDs := make([]string, hookBatchSize)
|
|
refFullNames := make([]git.RefName, hookBatchSize)
|
|
count := 0
|
|
total := 0
|
|
|
|
out := io.Discard
|
|
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
|
|
}
|
|
}
|
|
|
|
for scanner.Scan() {
|
|
// TODO: support news feeds for wiki
|
|
if isWiki {
|
|
continue
|
|
}
|
|
|
|
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
total++
|
|
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, ownerName, repoName, hookOptions)
|
|
if extra.HasError() {
|
|
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
|
|
}
|
|
count = 0
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
|
|
}
|
|
|
|
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, ownerName, repoName, hookOptions)
|
|
if extra.HasError() {
|
|
return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(out, "Checked %d references in total\n", total)
|
|
return nil
|
|
}
|
|
|
|
// runHookUpdate avoid to do heavy operations on update hook because it will be
|
|
// invoked for every ref update which does not like pre-receive and post-receive
|
|
func runHookUpdate(_ context.Context, c *cli.Command) error {
|
|
// Update is empty and is kept only for backwards compatibility
|
|
if len(os.Args) < 3 {
|
|
return nil
|
|
}
|
|
refName := git.RefName(os.Args[len(os.Args)-3])
|
|
if refName.IsPull() {
|
|
// ignore update to refs/pull/xxx/head, so we don't need to output any information
|
|
os.Exit(1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runHookPostReceive(ctx context.Context, c *cli.Command) error {
|
|
setup(ctx, c.Bool("debug"))
|
|
|
|
// First of all run update-server-info no matter what
|
|
if err := gitcmd.NewCommand("update-server-info").RunWithStderr(ctx); err != nil {
|
|
return fmt.Errorf("failed to call 'git update-server-info': %w", err)
|
|
}
|
|
|
|
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
|
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
|
|
return fail(ctx, `Rejecting changes as Gitea environment not set.
|
|
If you are pushing over SSH you must push with a key managed by
|
|
Gitea or set your environment appropriately.`, "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
out := io.Discard
|
|
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)
|
|
|
|
hookOptions := private.HookOptions{
|
|
IsWiki: isWiki,
|
|
|
|
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
|
|
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
|
|
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
|
|
GitPushOptions: pushOptions(),
|
|
|
|
PullRequestID: prID,
|
|
|
|
UserID: pusherID,
|
|
UserName: os.Getenv(repo_module.EnvPusherName),
|
|
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
|
|
}
|
|
|
|
oldCommitIDs := make([]string, 0, hookBatchSize)
|
|
newCommitIDs := make([]string, 0, hookBatchSize)
|
|
refFullNames := make([]git.RefName, 0, hookBatchSize)
|
|
results := make([]private.HookPostReceiveBranchResult, 0)
|
|
|
|
defer func() {
|
|
hookPrintResults(results)
|
|
}()
|
|
|
|
processBatch := func() error {
|
|
if len(refFullNames) == 0 {
|
|
return nil
|
|
}
|
|
_, _ = fmt.Fprintf(out, " Processing %d references\n", len(refFullNames))
|
|
hookOptions.OldCommitIDs = oldCommitIDs
|
|
hookOptions.NewCommitIDs = newCommitIDs
|
|
hookOptions.RefFullNames = refFullNames
|
|
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
|
|
if extra.HasError() {
|
|
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
|
|
}
|
|
results = append(results, resp.Results...)
|
|
oldCommitIDs = oldCommitIDs[:0]
|
|
newCommitIDs = newCommitIDs[:0]
|
|
refFullNames = refFullNames[:0]
|
|
return nil
|
|
}
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
for scanner.Scan() {
|
|
// wiki doesn't need "post-receive" at the moment
|
|
if isWiki {
|
|
continue
|
|
}
|
|
|
|
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
|
|
if !ok {
|
|
continue
|
|
}
|
|
_, _ = fmt.Fprintf(out, ".")
|
|
|
|
oldCommitIDs = append(oldCommitIDs, oldCommitID)
|
|
newCommitIDs = append(newCommitIDs, newCommitID)
|
|
refFullNames = append(refFullNames, refFullName)
|
|
if len(refFullNames) >= hookBatchSize {
|
|
// process and start a new batch
|
|
if err := processBatch(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
|
|
}
|
|
return processBatch()
|
|
}
|
|
|
|
func hookPrintResults(results []private.HookPostReceiveBranchResult) {
|
|
for _, res := range results {
|
|
hookPrintResult(res.Message, res.Create, res.Branch, res.URL)
|
|
}
|
|
}
|
|
|
|
func hookPrintResult(output, isCreate bool, branch, url string) {
|
|
if !output {
|
|
return
|
|
}
|
|
fmt.Fprintln(os.Stderr, "")
|
|
if isCreate {
|
|
fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch)
|
|
fmt.Fprintf(os.Stderr, " %s\n", url)
|
|
} else {
|
|
fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
|
|
fmt.Fprintf(os.Stderr, " %s\n", url)
|
|
}
|
|
fmt.Fprintln(os.Stderr, "")
|
|
_ = os.Stderr.Sync()
|
|
}
|
|
|
|
func pushOptions() map[string]string {
|
|
opts := make(map[string]string)
|
|
if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil {
|
|
for idx := range pushCount {
|
|
opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx))
|
|
kv := strings.SplitN(opt, "=", 2)
|
|
if len(kv) == 2 {
|
|
opts[kv[0]] = kv[1]
|
|
}
|
|
}
|
|
}
|
|
return opts
|
|
}
|
|
|
|
func runHookProcReceive(ctx context.Context, c *cli.Command) error {
|
|
setup(ctx, c.Bool("debug"))
|
|
|
|
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
|
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
|
|
return fail(ctx, `Rejecting changes as Gitea environment not set.
|
|
If you are pushing over SSH you must push with a key managed by
|
|
Gitea or set your environment appropriately.`, "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
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)
|
|
|
|
// 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{
|
|
IsWiki: isWiki,
|
|
|
|
GitPushOptions: make(map[string]string),
|
|
|
|
UserID: pusherID,
|
|
UserName: os.Getenv(repo_module.EnvPusherName),
|
|
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
|
|
}
|
|
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
|
|
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
|
|
hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize)
|
|
|
|
for {
|
|
// note: pktLineTypeUnknown 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]))
|
|
}
|
|
|
|
if hasPushOptions {
|
|
for {
|
|
rs, err = readPktLine(ctx, reader, pktLineTypeUnknown)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rs.Type == pktLineTypeFlush {
|
|
break
|
|
}
|
|
hookOptions.GitPushOptions.AddFromKeyValue(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
|
|
}
|
|
commitID, _ := git.NewIDFromString(rs.OldOID)
|
|
if !commitID.IsZero() {
|
|
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)
|
|
|
|
if err == nil {
|
|
for _, res := range resp.Results {
|
|
hookPrintResult(res.ShouldShowMessage, res.IsCreatePR, res.HeadBranch, res.URL)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) {
|
|
var (
|
|
err error
|
|
r *gitPktLine
|
|
)
|
|
|
|
// read prefix
|
|
lengthBytes := make([]byte, 4)
|
|
for i := range 4 {
|
|
lengthBytes[i], err = in.ReadByte()
|
|
if err != nil {
|
|
return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err)
|
|
}
|
|
}
|
|
|
|
r = new(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)
|
|
for i := range r.Data {
|
|
r.Data[i], err = in.ReadByte()
|
|
if err != nil {
|
|
return nil, fail(ctx, "Protocol: data 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
|
|
}
|
|
|
|
func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error {
|
|
hexchar := []byte("0123456789abcdef")
|
|
hex := func(n uint64) byte {
|
|
return hexchar[n&15]
|
|
}
|
|
|
|
length := uint64(len(data) + 4)
|
|
tmp := make([]byte, 4)
|
|
tmp[0] = hex(length >> 12)
|
|
tmp[1] = hex(length >> 8)
|
|
tmp[2] = hex(length >> 4)
|
|
tmp[3] = hex(length)
|
|
|
|
lr, err := out.Write(tmp)
|
|
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
|
|
}
|