summaryrefslogtreecommitdiffstats
path: root/cmd/hook.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/hook.go')
-rw-r--r--cmd/hook.go795
1 files changed, 795 insertions, 0 deletions
diff --git a/cmd/hook.go b/cmd/hook.go
new file mode 100644
index 0000000..edab611
--- /dev/null
+++ b/cmd/hook.go
@@ -0,0 +1,795 @@
+// 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 %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
+}