code.gitea.io/gitea@v1.22.3/cmd/hook.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package cmd
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"code.gitea.io/gitea/modules/git"
    18  	"code.gitea.io/gitea/modules/log"
    19  	"code.gitea.io/gitea/modules/private"
    20  	repo_module "code.gitea.io/gitea/modules/repository"
    21  	"code.gitea.io/gitea/modules/setting"
    22  
    23  	"github.com/urfave/cli/v2"
    24  )
    25  
    26  const (
    27  	hookBatchSize = 30
    28  )
    29  
    30  var (
    31  	// CmdHook represents the available hooks sub-command.
    32  	CmdHook = &cli.Command{
    33  		Name:        "hook",
    34  		Usage:       "(internal) Should only be called by Git",
    35  		Description: "Delegate commands to corresponding Git hooks",
    36  		Before:      PrepareConsoleLoggerLevel(log.FATAL),
    37  		Subcommands: []*cli.Command{
    38  			subcmdHookPreReceive,
    39  			subcmdHookUpdate,
    40  			subcmdHookPostReceive,
    41  			subcmdHookProcReceive,
    42  		},
    43  	}
    44  
    45  	subcmdHookPreReceive = &cli.Command{
    46  		Name:        "pre-receive",
    47  		Usage:       "Delegate pre-receive Git hook",
    48  		Description: "This command should only be called by Git",
    49  		Action:      runHookPreReceive,
    50  		Flags: []cli.Flag{
    51  			&cli.BoolFlag{
    52  				Name: "debug",
    53  			},
    54  		},
    55  	}
    56  	subcmdHookUpdate = &cli.Command{
    57  		Name:        "update",
    58  		Usage:       "Delegate update Git hook",
    59  		Description: "This command should only be called by Git",
    60  		Action:      runHookUpdate,
    61  		Flags: []cli.Flag{
    62  			&cli.BoolFlag{
    63  				Name: "debug",
    64  			},
    65  		},
    66  	}
    67  	subcmdHookPostReceive = &cli.Command{
    68  		Name:        "post-receive",
    69  		Usage:       "Delegate post-receive Git hook",
    70  		Description: "This command should only be called by Git",
    71  		Action:      runHookPostReceive,
    72  		Flags: []cli.Flag{
    73  			&cli.BoolFlag{
    74  				Name: "debug",
    75  			},
    76  		},
    77  	}
    78  	// Note: new hook since git 2.29
    79  	subcmdHookProcReceive = &cli.Command{
    80  		Name:        "proc-receive",
    81  		Usage:       "Delegate proc-receive Git hook",
    82  		Description: "This command should only be called by Git",
    83  		Action:      runHookProcReceive,
    84  		Flags: []cli.Flag{
    85  			&cli.BoolFlag{
    86  				Name: "debug",
    87  			},
    88  		},
    89  	}
    90  )
    91  
    92  type delayWriter struct {
    93  	internal io.Writer
    94  	buf      *bytes.Buffer
    95  	timer    *time.Timer
    96  }
    97  
    98  func newDelayWriter(internal io.Writer, delay time.Duration) *delayWriter {
    99  	timer := time.NewTimer(delay)
   100  	return &delayWriter{
   101  		internal: internal,
   102  		buf:      &bytes.Buffer{},
   103  		timer:    timer,
   104  	}
   105  }
   106  
   107  func (d *delayWriter) Write(p []byte) (n int, err error) {
   108  	if d.buf != nil {
   109  		select {
   110  		case <-d.timer.C:
   111  			_, err := d.internal.Write(d.buf.Bytes())
   112  			if err != nil {
   113  				return 0, err
   114  			}
   115  			d.buf = nil
   116  			return d.internal.Write(p)
   117  		default:
   118  			return d.buf.Write(p)
   119  		}
   120  	}
   121  	return d.internal.Write(p)
   122  }
   123  
   124  func (d *delayWriter) WriteString(s string) (n int, err error) {
   125  	if d.buf != nil {
   126  		select {
   127  		case <-d.timer.C:
   128  			_, err := d.internal.Write(d.buf.Bytes())
   129  			if err != nil {
   130  				return 0, err
   131  			}
   132  			d.buf = nil
   133  			return d.internal.Write([]byte(s))
   134  		default:
   135  			return d.buf.WriteString(s)
   136  		}
   137  	}
   138  	return d.internal.Write([]byte(s))
   139  }
   140  
   141  func (d *delayWriter) Close() error {
   142  	if d == nil {
   143  		return nil
   144  	}
   145  	stopped := d.timer.Stop()
   146  	if stopped || d.buf == nil {
   147  		return nil
   148  	}
   149  	_, err := d.internal.Write(d.buf.Bytes())
   150  	d.buf = nil
   151  	return err
   152  }
   153  
   154  type nilWriter struct{}
   155  
   156  func (n *nilWriter) Write(p []byte) (int, error) {
   157  	return len(p), nil
   158  }
   159  
   160  func (n *nilWriter) WriteString(s string) (int, error) {
   161  	return len(s), nil
   162  }
   163  
   164  func runHookPreReceive(c *cli.Context) error {
   165  	if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
   166  		return nil
   167  	}
   168  	ctx, cancel := installSignals()
   169  	defer cancel()
   170  
   171  	setup(ctx, c.Bool("debug"))
   172  
   173  	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
   174  		if setting.OnlyAllowPushIfGiteaEnvironmentSet {
   175  			return fail(ctx, `Rejecting changes as Gitea environment not set.
   176  If you are pushing over SSH you must push with a key managed by
   177  Gitea or set your environment appropriately.`, "")
   178  		}
   179  		return nil
   180  	}
   181  
   182  	// the environment is set by serv command
   183  	isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
   184  	username := os.Getenv(repo_module.EnvRepoUsername)
   185  	reponame := os.Getenv(repo_module.EnvRepoName)
   186  	userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
   187  	prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
   188  	deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64)
   189  	actionPerm, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionPerm), 10, 64)
   190  
   191  	hookOptions := private.HookOptions{
   192  		UserID:                          userID,
   193  		GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
   194  		GitObjectDirectory:              os.Getenv(private.GitObjectDirectory),
   195  		GitQuarantinePath:               os.Getenv(private.GitQuarantinePath),
   196  		GitPushOptions:                  pushOptions(),
   197  		PullRequestID:                   prID,
   198  		DeployKeyID:                     deployKeyID,
   199  		ActionPerm:                      int(actionPerm),
   200  	}
   201  
   202  	scanner := bufio.NewScanner(os.Stdin)
   203  
   204  	oldCommitIDs := make([]string, hookBatchSize)
   205  	newCommitIDs := make([]string, hookBatchSize)
   206  	refFullNames := make([]git.RefName, hookBatchSize)
   207  	count := 0
   208  	total := 0
   209  	lastline := 0
   210  
   211  	var out io.Writer
   212  	out = &nilWriter{}
   213  	if setting.Git.VerbosePush {
   214  		if setting.Git.VerbosePushDelay > 0 {
   215  			dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
   216  			defer dWriter.Close()
   217  			out = dWriter
   218  		} else {
   219  			out = os.Stdout
   220  		}
   221  	}
   222  
   223  	supportProcReceive := git.DefaultFeatures().SupportProcReceive
   224  
   225  	for scanner.Scan() {
   226  		// TODO: support news feeds for wiki
   227  		if isWiki {
   228  			continue
   229  		}
   230  
   231  		fields := bytes.Fields(scanner.Bytes())
   232  		if len(fields) != 3 {
   233  			continue
   234  		}
   235  
   236  		oldCommitID := string(fields[0])
   237  		newCommitID := string(fields[1])
   238  		refFullName := git.RefName(fields[2])
   239  		total++
   240  		lastline++
   241  
   242  		// If the ref is a branch or tag, check if it's protected
   243  		// if supportProcReceive all ref should be checked because
   244  		// permission check was delayed
   245  		if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
   246  			oldCommitIDs[count] = oldCommitID
   247  			newCommitIDs[count] = newCommitID
   248  			refFullNames[count] = refFullName
   249  			count++
   250  			fmt.Fprintf(out, "*")
   251  
   252  			if count >= hookBatchSize {
   253  				fmt.Fprintf(out, " Checking %d references\n", count)
   254  
   255  				hookOptions.OldCommitIDs = oldCommitIDs
   256  				hookOptions.NewCommitIDs = newCommitIDs
   257  				hookOptions.RefFullNames = refFullNames
   258  				extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
   259  				if extra.HasError() {
   260  					return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
   261  				}
   262  				count = 0
   263  				lastline = 0
   264  			}
   265  		} else {
   266  			fmt.Fprintf(out, ".")
   267  		}
   268  		if lastline >= hookBatchSize {
   269  			fmt.Fprintf(out, "\n")
   270  			lastline = 0
   271  		}
   272  	}
   273  
   274  	if count > 0 {
   275  		hookOptions.OldCommitIDs = oldCommitIDs[:count]
   276  		hookOptions.NewCommitIDs = newCommitIDs[:count]
   277  		hookOptions.RefFullNames = refFullNames[:count]
   278  
   279  		fmt.Fprintf(out, " Checking %d references\n", count)
   280  
   281  		extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
   282  		if extra.HasError() {
   283  			return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
   284  		}
   285  	} else if lastline > 0 {
   286  		fmt.Fprintf(out, "\n")
   287  	}
   288  
   289  	fmt.Fprintf(out, "Checked %d references in total\n", total)
   290  	return nil
   291  }
   292  
   293  // runHookUpdate avoid to do heavy operations on update hook because it will be
   294  // invoked for every ref update which does not like pre-receive and post-receive
   295  func runHookUpdate(c *cli.Context) error {
   296  	if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
   297  		return nil
   298  	}
   299  
   300  	// Update is empty and is kept only for backwards compatibility
   301  	if len(os.Args) < 3 {
   302  		return nil
   303  	}
   304  	refName := git.RefName(os.Args[len(os.Args)-3])
   305  	if refName.IsPull() {
   306  		// ignore update to refs/pull/xxx/head, so we don't need to output any information
   307  		os.Exit(1)
   308  	}
   309  	return nil
   310  }
   311  
   312  func runHookPostReceive(c *cli.Context) error {
   313  	ctx, cancel := installSignals()
   314  	defer cancel()
   315  
   316  	setup(ctx, c.Bool("debug"))
   317  
   318  	// First of all run update-server-info no matter what
   319  	if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil {
   320  		return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
   321  	}
   322  
   323  	// Now if we're an internal don't do anything else
   324  	if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
   325  		return nil
   326  	}
   327  
   328  	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
   329  		if setting.OnlyAllowPushIfGiteaEnvironmentSet {
   330  			return fail(ctx, `Rejecting changes as Gitea environment not set.
   331  If you are pushing over SSH you must push with a key managed by
   332  Gitea or set your environment appropriately.`, "")
   333  		}
   334  		return nil
   335  	}
   336  
   337  	var out io.Writer
   338  	var dWriter *delayWriter
   339  	out = &nilWriter{}
   340  	if setting.Git.VerbosePush {
   341  		if setting.Git.VerbosePushDelay > 0 {
   342  			dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
   343  			defer dWriter.Close()
   344  			out = dWriter
   345  		} else {
   346  			out = os.Stdout
   347  		}
   348  	}
   349  
   350  	// the environment is set by serv command
   351  	repoUser := os.Getenv(repo_module.EnvRepoUsername)
   352  	isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
   353  	repoName := os.Getenv(repo_module.EnvRepoName)
   354  	pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
   355  	prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
   356  	pusherName := os.Getenv(repo_module.EnvPusherName)
   357  
   358  	hookOptions := private.HookOptions{
   359  		UserName:                        pusherName,
   360  		UserID:                          pusherID,
   361  		GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
   362  		GitObjectDirectory:              os.Getenv(private.GitObjectDirectory),
   363  		GitQuarantinePath:               os.Getenv(private.GitQuarantinePath),
   364  		GitPushOptions:                  pushOptions(),
   365  		PullRequestID:                   prID,
   366  		PushTrigger:                     repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
   367  	}
   368  	oldCommitIDs := make([]string, hookBatchSize)
   369  	newCommitIDs := make([]string, hookBatchSize)
   370  	refFullNames := make([]git.RefName, hookBatchSize)
   371  	count := 0
   372  	total := 0
   373  	wasEmpty := false
   374  	masterPushed := false
   375  	results := make([]private.HookPostReceiveBranchResult, 0)
   376  
   377  	scanner := bufio.NewScanner(os.Stdin)
   378  	for scanner.Scan() {
   379  		// TODO: support news feeds for wiki
   380  		if isWiki {
   381  			continue
   382  		}
   383  
   384  		fields := bytes.Fields(scanner.Bytes())
   385  		if len(fields) != 3 {
   386  			continue
   387  		}
   388  
   389  		fmt.Fprintf(out, ".")
   390  		oldCommitIDs[count] = string(fields[0])
   391  		newCommitIDs[count] = string(fields[1])
   392  		refFullNames[count] = git.RefName(fields[2])
   393  
   394  		commitID, _ := git.NewIDFromString(newCommitIDs[count])
   395  		if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
   396  			masterPushed = true
   397  		}
   398  		count++
   399  		total++
   400  
   401  		if count >= hookBatchSize {
   402  			fmt.Fprintf(out, " Processing %d references\n", count)
   403  			hookOptions.OldCommitIDs = oldCommitIDs
   404  			hookOptions.NewCommitIDs = newCommitIDs
   405  			hookOptions.RefFullNames = refFullNames
   406  			resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
   407  			if extra.HasError() {
   408  				_ = dWriter.Close()
   409  				hookPrintResults(results)
   410  				return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
   411  			}
   412  			wasEmpty = wasEmpty || resp.RepoWasEmpty
   413  			results = append(results, resp.Results...)
   414  			count = 0
   415  		}
   416  	}
   417  
   418  	if count == 0 {
   419  		if wasEmpty && masterPushed {
   420  			// We need to tell the repo to reset the default branch to master
   421  			extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
   422  			if extra.HasError() {
   423  				return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
   424  			}
   425  		}
   426  		fmt.Fprintf(out, "Processed %d references in total\n", total)
   427  
   428  		_ = dWriter.Close()
   429  		hookPrintResults(results)
   430  		return nil
   431  	}
   432  
   433  	hookOptions.OldCommitIDs = oldCommitIDs[:count]
   434  	hookOptions.NewCommitIDs = newCommitIDs[:count]
   435  	hookOptions.RefFullNames = refFullNames[:count]
   436  
   437  	fmt.Fprintf(out, " Processing %d references\n", count)
   438  
   439  	resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
   440  	if resp == nil {
   441  		_ = dWriter.Close()
   442  		hookPrintResults(results)
   443  		return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
   444  	}
   445  	wasEmpty = wasEmpty || resp.RepoWasEmpty
   446  	results = append(results, resp.Results...)
   447  
   448  	fmt.Fprintf(out, "Processed %d references in total\n", total)
   449  
   450  	if wasEmpty && masterPushed {
   451  		// We need to tell the repo to reset the default branch to master
   452  		extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
   453  		if extra.HasError() {
   454  			return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
   455  		}
   456  	}
   457  	_ = dWriter.Close()
   458  	hookPrintResults(results)
   459  
   460  	return nil
   461  }
   462  
   463  func hookPrintResults(results []private.HookPostReceiveBranchResult) {
   464  	for _, res := range results {
   465  		hookPrintResult(res.Message, res.Create, res.Branch, res.URL)
   466  	}
   467  }
   468  
   469  func hookPrintResult(output, isCreate bool, branch, url string) {
   470  	if !output {
   471  		return
   472  	}
   473  	fmt.Fprintln(os.Stderr, "")
   474  	if isCreate {
   475  		fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch)
   476  		fmt.Fprintf(os.Stderr, "  %s\n", url)
   477  	} else {
   478  		fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
   479  		fmt.Fprintf(os.Stderr, "  %s\n", url)
   480  	}
   481  	fmt.Fprintln(os.Stderr, "")
   482  	_ = os.Stderr.Sync()
   483  }
   484  
   485  func pushOptions() map[string]string {
   486  	opts := make(map[string]string)
   487  	if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil {
   488  		for idx := 0; idx < pushCount; idx++ {
   489  			opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx))
   490  			kv := strings.SplitN(opt, "=", 2)
   491  			if len(kv) == 2 {
   492  				opts[kv[0]] = kv[1]
   493  			}
   494  		}
   495  	}
   496  	return opts
   497  }
   498  
   499  func runHookProcReceive(c *cli.Context) error {
   500  	ctx, cancel := installSignals()
   501  	defer cancel()
   502  
   503  	setup(ctx, c.Bool("debug"))
   504  
   505  	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
   506  		if setting.OnlyAllowPushIfGiteaEnvironmentSet {
   507  			return fail(ctx, `Rejecting changes as Gitea environment not set.
   508  If you are pushing over SSH you must push with a key managed by
   509  Gitea or set your environment appropriately.`, "")
   510  		}
   511  		return nil
   512  	}
   513  
   514  	if !git.DefaultFeatures().SupportProcReceive {
   515  		return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
   516  	}
   517  
   518  	reader := bufio.NewReader(os.Stdin)
   519  	repoUser := os.Getenv(repo_module.EnvRepoUsername)
   520  	repoName := os.Getenv(repo_module.EnvRepoName)
   521  	pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
   522  	pusherName := os.Getenv(repo_module.EnvPusherName)
   523  
   524  	// 1. Version and features negotiation.
   525  	// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n)
   526  	// S: flush-pkt
   527  	// H: PKT-LINE(version=1\0push-options...)
   528  	// H: flush-pkt
   529  
   530  	rs, err := readPktLine(ctx, reader, pktLineTypeData)
   531  	if err != nil {
   532  		return err
   533  	}
   534  
   535  	const VersionHead string = "version=1"
   536  
   537  	var (
   538  		hasPushOptions bool
   539  		response       = []byte(VersionHead)
   540  		requestOptions []string
   541  	)
   542  
   543  	index := bytes.IndexByte(rs.Data, byte(0))
   544  	if index >= len(rs.Data) {
   545  		return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data))
   546  	}
   547  
   548  	if index < 0 {
   549  		if len(rs.Data) == 10 && rs.Data[9] == '\n' {
   550  			index = 9
   551  		} else {
   552  			return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data))
   553  		}
   554  	}
   555  
   556  	if string(rs.Data[0:index]) != VersionHead {
   557  		return fail(ctx, "Protocol: version error", "Received unsupported version: %s", string(rs.Data[0:index]))
   558  	}
   559  	requestOptions = strings.Split(string(rs.Data[index+1:]), " ")
   560  
   561  	for _, option := range requestOptions {
   562  		if strings.HasPrefix(option, "push-options") {
   563  			response = append(response, byte(0))
   564  			response = append(response, []byte("push-options")...)
   565  			hasPushOptions = true
   566  		}
   567  	}
   568  	response = append(response, '\n')
   569  
   570  	_, err = readPktLine(ctx, reader, pktLineTypeFlush)
   571  	if err != nil {
   572  		return err
   573  	}
   574  
   575  	err = writeDataPktLine(ctx, os.Stdout, response)
   576  	if err != nil {
   577  		return err
   578  	}
   579  
   580  	err = writeFlushPktLine(ctx, os.Stdout)
   581  	if err != nil {
   582  		return err
   583  	}
   584  
   585  	// 2. receive commands from server.
   586  	// S: PKT-LINE(<old-oid> <new-oid> <ref>)
   587  	// S: ... ...
   588  	// S: flush-pkt
   589  	// # [receive push-options]
   590  	// S: PKT-LINE(push-option)
   591  	// S: ... ...
   592  	// S: flush-pkt
   593  	hookOptions := private.HookOptions{
   594  		UserName: pusherName,
   595  		UserID:   pusherID,
   596  	}
   597  	hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
   598  	hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
   599  	hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize)
   600  
   601  	for {
   602  		// note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed
   603  		rs, err = readPktLine(ctx, reader, pktLineTypeUnknow)
   604  		if err != nil {
   605  			return err
   606  		}
   607  
   608  		if rs.Type == pktLineTypeFlush {
   609  			break
   610  		}
   611  		t := strings.SplitN(string(rs.Data), " ", 3)
   612  		if len(t) != 3 {
   613  			continue
   614  		}
   615  		hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0])
   616  		hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1])
   617  		hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2]))
   618  	}
   619  
   620  	hookOptions.GitPushOptions = make(map[string]string)
   621  
   622  	if hasPushOptions {
   623  		for {
   624  			rs, err = readPktLine(ctx, reader, pktLineTypeUnknow)
   625  			if err != nil {
   626  				return err
   627  			}
   628  
   629  			if rs.Type == pktLineTypeFlush {
   630  				break
   631  			}
   632  
   633  			kv := strings.SplitN(string(rs.Data), "=", 2)
   634  			if len(kv) == 2 {
   635  				hookOptions.GitPushOptions[kv[0]] = kv[1]
   636  			}
   637  		}
   638  	}
   639  
   640  	// 3. run hook
   641  	resp, extra := private.HookProcReceive(ctx, repoUser, repoName, hookOptions)
   642  	if extra.HasError() {
   643  		return fail(ctx, extra.UserMsg, "HookProcReceive failed: %v", extra.Error)
   644  	}
   645  
   646  	// 4. response result to service
   647  	// # a. OK, but has an alternate reference.  The alternate reference name
   648  	// # and other status can be given in option directives.
   649  	// H: PKT-LINE(ok <ref>)
   650  	// H: PKT-LINE(option refname <refname>)
   651  	// H: PKT-LINE(option old-oid <old-oid>)
   652  	// H: PKT-LINE(option new-oid <new-oid>)
   653  	// H: PKT-LINE(option forced-update)
   654  	// H: ... ...
   655  	// H: flush-pkt
   656  	// # b. NO, I reject it.
   657  	// H: PKT-LINE(ng <ref> <reason>)
   658  	// # c. Fall through, let 'receive-pack' to execute it.
   659  	// H: PKT-LINE(ok <ref>)
   660  	// H: PKT-LINE(option fall-through)
   661  
   662  	for _, rs := range resp.Results {
   663  		if len(rs.Err) > 0 {
   664  			err = writeDataPktLine(ctx, os.Stdout, []byte("ng "+rs.OriginalRef.String()+" "+rs.Err))
   665  			if err != nil {
   666  				return err
   667  			}
   668  			continue
   669  		}
   670  
   671  		if rs.IsNotMatched {
   672  			err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef.String()))
   673  			if err != nil {
   674  				return err
   675  			}
   676  			err = writeDataPktLine(ctx, os.Stdout, []byte("option fall-through"))
   677  			if err != nil {
   678  				return err
   679  			}
   680  			continue
   681  		}
   682  
   683  		err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef))
   684  		if err != nil {
   685  			return err
   686  		}
   687  		err = writeDataPktLine(ctx, os.Stdout, []byte("option refname "+rs.Ref))
   688  		if err != nil {
   689  			return err
   690  		}
   691  		commitID, _ := git.NewIDFromString(rs.OldOID)
   692  		if !commitID.IsZero() {
   693  			err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID))
   694  			if err != nil {
   695  				return err
   696  			}
   697  		}
   698  		err = writeDataPktLine(ctx, os.Stdout, []byte("option new-oid "+rs.NewOID))
   699  		if err != nil {
   700  			return err
   701  		}
   702  		if rs.IsForcePush {
   703  			err = writeDataPktLine(ctx, os.Stdout, []byte("option forced-update"))
   704  			if err != nil {
   705  				return err
   706  			}
   707  		}
   708  	}
   709  	err = writeFlushPktLine(ctx, os.Stdout)
   710  
   711  	if err == nil {
   712  		for _, res := range resp.Results {
   713  			hookPrintResult(res.ShouldShowMessage, res.IsCreatePR, res.HeadBranch, res.URL)
   714  		}
   715  	}
   716  
   717  	return err
   718  }
   719  
   720  // git PKT-Line api
   721  // pktLineType message type of pkt-line
   722  type pktLineType int64
   723  
   724  const (
   725  	// UnKnow type
   726  	pktLineTypeUnknow pktLineType = 0
   727  	// flush-pkt "0000"
   728  	pktLineTypeFlush pktLineType = iota
   729  	// data line
   730  	pktLineTypeData
   731  )
   732  
   733  // gitPktLine pkt-line api
   734  type gitPktLine struct {
   735  	Type   pktLineType
   736  	Length uint64
   737  	Data   []byte
   738  }
   739  
   740  func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) {
   741  	var (
   742  		err error
   743  		r   *gitPktLine
   744  	)
   745  
   746  	// read prefix
   747  	lengthBytes := make([]byte, 4)
   748  	for i := 0; i < 4; i++ {
   749  		lengthBytes[i], err = in.ReadByte()
   750  		if err != nil {
   751  			return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err)
   752  		}
   753  	}
   754  
   755  	r = new(gitPktLine)
   756  	r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32)
   757  	if err != nil {
   758  		return nil, fail(ctx, "Protocol: format parse error", "Pkt-Line format is wrong :%v", err)
   759  	}
   760  
   761  	if r.Length == 0 {
   762  		if requestType == pktLineTypeData {
   763  			return nil, fail(ctx, "Protocol: format data error", "Pkt-Line format is wrong")
   764  		}
   765  		r.Type = pktLineTypeFlush
   766  		return r, nil
   767  	}
   768  
   769  	if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush {
   770  		return nil, fail(ctx, "Protocol: format length error", "Pkt-Line format is wrong")
   771  	}
   772  
   773  	r.Data = make([]byte, r.Length-4)
   774  	for i := range r.Data {
   775  		r.Data[i], err = in.ReadByte()
   776  		if err != nil {
   777  			return nil, fail(ctx, "Protocol: data error", "Pkt-Line: read stdin failed : %v", err)
   778  		}
   779  	}
   780  
   781  	r.Type = pktLineTypeData
   782  
   783  	return r, nil
   784  }
   785  
   786  func writeFlushPktLine(ctx context.Context, out io.Writer) error {
   787  	l, err := out.Write([]byte("0000"))
   788  	if err != nil || l != 4 {
   789  		return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
   790  	}
   791  	return nil
   792  }
   793  
   794  func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error {
   795  	hexchar := []byte("0123456789abcdef")
   796  	hex := func(n uint64) byte {
   797  		return hexchar[(n)&15]
   798  	}
   799  
   800  	length := uint64(len(data) + 4)
   801  	tmp := make([]byte, 4)
   802  	tmp[0] = hex(length >> 12)
   803  	tmp[1] = hex(length >> 8)
   804  	tmp[2] = hex(length >> 4)
   805  	tmp[3] = hex(length)
   806  
   807  	lr, err := out.Write(tmp)
   808  	if err != nil || lr != 4 {
   809  		return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
   810  	}
   811  
   812  	lr, err = out.Write(data)
   813  	if err != nil || int(length-4) != lr {
   814  		return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
   815  	}
   816  
   817  	return nil
   818  }