code.gitea.io/gitea@v1.21.7/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:       "Delegate commands to corresponding Git hooks",
    35  		Description: "This should only be called by Git",
    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 := false
   224  	if git.CheckGitVersionAtLeast("2.29") == nil {
   225  		supportProcReceive = true
   226  	}
   227  
   228  	for scanner.Scan() {
   229  		// TODO: support news feeds for wiki
   230  		if isWiki {
   231  			continue
   232  		}
   233  
   234  		fields := bytes.Fields(scanner.Bytes())
   235  		if len(fields) != 3 {
   236  			continue
   237  		}
   238  
   239  		oldCommitID := string(fields[0])
   240  		newCommitID := string(fields[1])
   241  		refFullName := git.RefName(fields[2])
   242  		total++
   243  		lastline++
   244  
   245  		// If the ref is a branch or tag, check if it's protected
   246  		// if supportProcReceive all ref should be checked because
   247  		// permission check was delayed
   248  		if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
   249  			oldCommitIDs[count] = oldCommitID
   250  			newCommitIDs[count] = newCommitID
   251  			refFullNames[count] = refFullName
   252  			count++
   253  			fmt.Fprintf(out, "*")
   254  
   255  			if count >= hookBatchSize {
   256  				fmt.Fprintf(out, " Checking %d references\n", count)
   257  
   258  				hookOptions.OldCommitIDs = oldCommitIDs
   259  				hookOptions.NewCommitIDs = newCommitIDs
   260  				hookOptions.RefFullNames = refFullNames
   261  				extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
   262  				if extra.HasError() {
   263  					return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
   264  				}
   265  				count = 0
   266  				lastline = 0
   267  			}
   268  		} else {
   269  			fmt.Fprintf(out, ".")
   270  		}
   271  		if lastline >= hookBatchSize {
   272  			fmt.Fprintf(out, "\n")
   273  			lastline = 0
   274  		}
   275  	}
   276  
   277  	if count > 0 {
   278  		hookOptions.OldCommitIDs = oldCommitIDs[:count]
   279  		hookOptions.NewCommitIDs = newCommitIDs[:count]
   280  		hookOptions.RefFullNames = refFullNames[:count]
   281  
   282  		fmt.Fprintf(out, " Checking %d references\n", count)
   283  
   284  		extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
   285  		if extra.HasError() {
   286  			return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
   287  		}
   288  	} else if lastline > 0 {
   289  		fmt.Fprintf(out, "\n")
   290  	}
   291  
   292  	fmt.Fprintf(out, "Checked %d references in total\n", total)
   293  	return nil
   294  }
   295  
   296  func runHookUpdate(c *cli.Context) error {
   297  	// Update is empty and is kept only for backwards compatibility
   298  	return nil
   299  }
   300  
   301  func runHookPostReceive(c *cli.Context) error {
   302  	ctx, cancel := installSignals()
   303  	defer cancel()
   304  
   305  	setup(ctx, c.Bool("debug"))
   306  
   307  	// First of all run update-server-info no matter what
   308  	if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil {
   309  		return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
   310  	}
   311  
   312  	// Now if we're an internal don't do anything else
   313  	if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
   314  		return nil
   315  	}
   316  
   317  	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
   318  		if setting.OnlyAllowPushIfGiteaEnvironmentSet {
   319  			return fail(ctx, `Rejecting changes as Gitea environment not set.
   320  If you are pushing over SSH you must push with a key managed by
   321  Gitea or set your environment appropriately.`, "")
   322  		}
   323  		return nil
   324  	}
   325  
   326  	var out io.Writer
   327  	var dWriter *delayWriter
   328  	out = &nilWriter{}
   329  	if setting.Git.VerbosePush {
   330  		if setting.Git.VerbosePushDelay > 0 {
   331  			dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
   332  			defer dWriter.Close()
   333  			out = dWriter
   334  		} else {
   335  			out = os.Stdout
   336  		}
   337  	}
   338  
   339  	// the environment is set by serv command
   340  	repoUser := os.Getenv(repo_module.EnvRepoUsername)
   341  	isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
   342  	repoName := os.Getenv(repo_module.EnvRepoName)
   343  	pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
   344  	pusherName := os.Getenv(repo_module.EnvPusherName)
   345  
   346  	hookOptions := private.HookOptions{
   347  		UserName:                        pusherName,
   348  		UserID:                          pusherID,
   349  		GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
   350  		GitObjectDirectory:              os.Getenv(private.GitObjectDirectory),
   351  		GitQuarantinePath:               os.Getenv(private.GitQuarantinePath),
   352  		GitPushOptions:                  pushOptions(),
   353  	}
   354  	oldCommitIDs := make([]string, hookBatchSize)
   355  	newCommitIDs := make([]string, hookBatchSize)
   356  	refFullNames := make([]git.RefName, hookBatchSize)
   357  	count := 0
   358  	total := 0
   359  	wasEmpty := false
   360  	masterPushed := false
   361  	results := make([]private.HookPostReceiveBranchResult, 0)
   362  
   363  	scanner := bufio.NewScanner(os.Stdin)
   364  	for scanner.Scan() {
   365  		// TODO: support news feeds for wiki
   366  		if isWiki {
   367  			continue
   368  		}
   369  
   370  		fields := bytes.Fields(scanner.Bytes())
   371  		if len(fields) != 3 {
   372  			continue
   373  		}
   374  
   375  		fmt.Fprintf(out, ".")
   376  		oldCommitIDs[count] = string(fields[0])
   377  		newCommitIDs[count] = string(fields[1])
   378  		refFullNames[count] = git.RefName(fields[2])
   379  		if refFullNames[count] == git.BranchPrefix+"master" && newCommitIDs[count] != git.EmptySHA && count == total {
   380  			masterPushed = true
   381  		}
   382  		count++
   383  		total++
   384  
   385  		if count >= hookBatchSize {
   386  			fmt.Fprintf(out, " Processing %d references\n", count)
   387  			hookOptions.OldCommitIDs = oldCommitIDs
   388  			hookOptions.NewCommitIDs = newCommitIDs
   389  			hookOptions.RefFullNames = refFullNames
   390  			resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
   391  			if extra.HasError() {
   392  				_ = dWriter.Close()
   393  				hookPrintResults(results)
   394  				return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
   395  			}
   396  			wasEmpty = wasEmpty || resp.RepoWasEmpty
   397  			results = append(results, resp.Results...)
   398  			count = 0
   399  		}
   400  	}
   401  
   402  	if count == 0 {
   403  		if wasEmpty && masterPushed {
   404  			// We need to tell the repo to reset the default branch to master
   405  			extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
   406  			if extra.HasError() {
   407  				return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
   408  			}
   409  		}
   410  		fmt.Fprintf(out, "Processed %d references in total\n", total)
   411  
   412  		_ = dWriter.Close()
   413  		hookPrintResults(results)
   414  		return nil
   415  	}
   416  
   417  	hookOptions.OldCommitIDs = oldCommitIDs[:count]
   418  	hookOptions.NewCommitIDs = newCommitIDs[:count]
   419  	hookOptions.RefFullNames = refFullNames[:count]
   420  
   421  	fmt.Fprintf(out, " Processing %d references\n", count)
   422  
   423  	resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
   424  	if resp == nil {
   425  		_ = dWriter.Close()
   426  		hookPrintResults(results)
   427  		return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
   428  	}
   429  	wasEmpty = wasEmpty || resp.RepoWasEmpty
   430  	results = append(results, resp.Results...)
   431  
   432  	fmt.Fprintf(out, "Processed %d references in total\n", total)
   433  
   434  	if wasEmpty && masterPushed {
   435  		// We need to tell the repo to reset the default branch to master
   436  		extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
   437  		if extra.HasError() {
   438  			return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
   439  		}
   440  	}
   441  	_ = dWriter.Close()
   442  	hookPrintResults(results)
   443  
   444  	return nil
   445  }
   446  
   447  func hookPrintResults(results []private.HookPostReceiveBranchResult) {
   448  	for _, res := range results {
   449  		if !res.Message {
   450  			continue
   451  		}
   452  
   453  		fmt.Fprintln(os.Stderr, "")
   454  		if res.Create {
   455  			fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res.Branch)
   456  			fmt.Fprintf(os.Stderr, "  %s\n", res.URL)
   457  		} else {
   458  			fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
   459  			fmt.Fprintf(os.Stderr, "  %s\n", res.URL)
   460  		}
   461  		fmt.Fprintln(os.Stderr, "")
   462  		os.Stderr.Sync()
   463  	}
   464  }
   465  
   466  func pushOptions() map[string]string {
   467  	opts := make(map[string]string)
   468  	if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil {
   469  		for idx := 0; idx < pushCount; idx++ {
   470  			opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx))
   471  			kv := strings.SplitN(opt, "=", 2)
   472  			if len(kv) == 2 {
   473  				opts[kv[0]] = kv[1]
   474  			}
   475  		}
   476  	}
   477  	return opts
   478  }
   479  
   480  func runHookProcReceive(c *cli.Context) error {
   481  	ctx, cancel := installSignals()
   482  	defer cancel()
   483  
   484  	setup(ctx, c.Bool("debug"))
   485  
   486  	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
   487  		if setting.OnlyAllowPushIfGiteaEnvironmentSet {
   488  			return fail(ctx, `Rejecting changes as Gitea environment not set.
   489  If you are pushing over SSH you must push with a key managed by
   490  Gitea or set your environment appropriately.`, "")
   491  		}
   492  		return nil
   493  	}
   494  
   495  	if git.CheckGitVersionAtLeast("2.29") != nil {
   496  		return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
   497  	}
   498  
   499  	reader := bufio.NewReader(os.Stdin)
   500  	repoUser := os.Getenv(repo_module.EnvRepoUsername)
   501  	repoName := os.Getenv(repo_module.EnvRepoName)
   502  	pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
   503  	pusherName := os.Getenv(repo_module.EnvPusherName)
   504  
   505  	// 1. Version and features negotiation.
   506  	// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n)
   507  	// S: flush-pkt
   508  	// H: PKT-LINE(version=1\0push-options...)
   509  	// H: flush-pkt
   510  
   511  	rs, err := readPktLine(ctx, reader, pktLineTypeData)
   512  	if err != nil {
   513  		return err
   514  	}
   515  
   516  	const VersionHead string = "version=1"
   517  
   518  	var (
   519  		hasPushOptions bool
   520  		response       = []byte(VersionHead)
   521  		requestOptions []string
   522  	)
   523  
   524  	index := bytes.IndexByte(rs.Data, byte(0))
   525  	if index >= len(rs.Data) {
   526  		return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data))
   527  	}
   528  
   529  	if index < 0 {
   530  		if len(rs.Data) == 10 && rs.Data[9] == '\n' {
   531  			index = 9
   532  		} else {
   533  			return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data))
   534  		}
   535  	}
   536  
   537  	if string(rs.Data[0:index]) != VersionHead {
   538  		return fail(ctx, "Protocol: version error", "Received unsupported version: %s", string(rs.Data[0:index]))
   539  	}
   540  	requestOptions = strings.Split(string(rs.Data[index+1:]), " ")
   541  
   542  	for _, option := range requestOptions {
   543  		if strings.HasPrefix(option, "push-options") {
   544  			response = append(response, byte(0))
   545  			response = append(response, []byte("push-options")...)
   546  			hasPushOptions = true
   547  		}
   548  	}
   549  	response = append(response, '\n')
   550  
   551  	_, err = readPktLine(ctx, reader, pktLineTypeFlush)
   552  	if err != nil {
   553  		return err
   554  	}
   555  
   556  	err = writeDataPktLine(ctx, os.Stdout, response)
   557  	if err != nil {
   558  		return err
   559  	}
   560  
   561  	err = writeFlushPktLine(ctx, os.Stdout)
   562  	if err != nil {
   563  		return err
   564  	}
   565  
   566  	// 2. receive commands from server.
   567  	// S: PKT-LINE(<old-oid> <new-oid> <ref>)
   568  	// S: ... ...
   569  	// S: flush-pkt
   570  	// # [receive push-options]
   571  	// S: PKT-LINE(push-option)
   572  	// S: ... ...
   573  	// S: flush-pkt
   574  	hookOptions := private.HookOptions{
   575  		UserName: pusherName,
   576  		UserID:   pusherID,
   577  	}
   578  	hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
   579  	hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
   580  	hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize)
   581  
   582  	for {
   583  		// note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed
   584  		rs, err = readPktLine(ctx, reader, pktLineTypeUnknow)
   585  		if err != nil {
   586  			return err
   587  		}
   588  
   589  		if rs.Type == pktLineTypeFlush {
   590  			break
   591  		}
   592  		t := strings.SplitN(string(rs.Data), " ", 3)
   593  		if len(t) != 3 {
   594  			continue
   595  		}
   596  		hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0])
   597  		hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1])
   598  		hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2]))
   599  	}
   600  
   601  	hookOptions.GitPushOptions = make(map[string]string)
   602  
   603  	if hasPushOptions {
   604  		for {
   605  			rs, err = readPktLine(ctx, reader, pktLineTypeUnknow)
   606  			if err != nil {
   607  				return err
   608  			}
   609  
   610  			if rs.Type == pktLineTypeFlush {
   611  				break
   612  			}
   613  
   614  			kv := strings.SplitN(string(rs.Data), "=", 2)
   615  			if len(kv) == 2 {
   616  				hookOptions.GitPushOptions[kv[0]] = kv[1]
   617  			}
   618  		}
   619  	}
   620  
   621  	// 3. run hook
   622  	resp, extra := private.HookProcReceive(ctx, repoUser, repoName, hookOptions)
   623  	if extra.HasError() {
   624  		return fail(ctx, extra.UserMsg, "HookProcReceive failed: %v", extra.Error)
   625  	}
   626  
   627  	// 4. response result to service
   628  	// # a. OK, but has an alternate reference.  The alternate reference name
   629  	// # and other status can be given in option directives.
   630  	// H: PKT-LINE(ok <ref>)
   631  	// H: PKT-LINE(option refname <refname>)
   632  	// H: PKT-LINE(option old-oid <old-oid>)
   633  	// H: PKT-LINE(option new-oid <new-oid>)
   634  	// H: PKT-LINE(option forced-update)
   635  	// H: ... ...
   636  	// H: flush-pkt
   637  	// # b. NO, I reject it.
   638  	// H: PKT-LINE(ng <ref> <reason>)
   639  	// # c. Fall through, let 'receive-pack' to execute it.
   640  	// H: PKT-LINE(ok <ref>)
   641  	// H: PKT-LINE(option fall-through)
   642  
   643  	for _, rs := range resp.Results {
   644  		if len(rs.Err) > 0 {
   645  			err = writeDataPktLine(ctx, os.Stdout, []byte("ng "+rs.OriginalRef.String()+" "+rs.Err))
   646  			if err != nil {
   647  				return err
   648  			}
   649  			continue
   650  		}
   651  
   652  		if rs.IsNotMatched {
   653  			err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef.String()))
   654  			if err != nil {
   655  				return err
   656  			}
   657  			err = writeDataPktLine(ctx, os.Stdout, []byte("option fall-through"))
   658  			if err != nil {
   659  				return err
   660  			}
   661  			continue
   662  		}
   663  
   664  		err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef))
   665  		if err != nil {
   666  			return err
   667  		}
   668  		err = writeDataPktLine(ctx, os.Stdout, []byte("option refname "+rs.Ref))
   669  		if err != nil {
   670  			return err
   671  		}
   672  		if rs.OldOID != git.EmptySHA {
   673  			err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID))
   674  			if err != nil {
   675  				return err
   676  			}
   677  		}
   678  		err = writeDataPktLine(ctx, os.Stdout, []byte("option new-oid "+rs.NewOID))
   679  		if err != nil {
   680  			return err
   681  		}
   682  		if rs.IsForcePush {
   683  			err = writeDataPktLine(ctx, os.Stdout, []byte("option forced-update"))
   684  			if err != nil {
   685  				return err
   686  			}
   687  		}
   688  	}
   689  	err = writeFlushPktLine(ctx, os.Stdout)
   690  
   691  	return err
   692  }
   693  
   694  // git PKT-Line api
   695  // pktLineType message type of pkt-line
   696  type pktLineType int64
   697  
   698  const (
   699  	// UnKnow type
   700  	pktLineTypeUnknow pktLineType = 0
   701  	// flush-pkt "0000"
   702  	pktLineTypeFlush pktLineType = iota
   703  	// data line
   704  	pktLineTypeData
   705  )
   706  
   707  // gitPktLine pkt-line api
   708  type gitPktLine struct {
   709  	Type   pktLineType
   710  	Length uint64
   711  	Data   []byte
   712  }
   713  
   714  func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) {
   715  	var (
   716  		err error
   717  		r   *gitPktLine
   718  	)
   719  
   720  	// read prefix
   721  	lengthBytes := make([]byte, 4)
   722  	for i := 0; i < 4; i++ {
   723  		lengthBytes[i], err = in.ReadByte()
   724  		if err != nil {
   725  			return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err)
   726  		}
   727  	}
   728  
   729  	r = new(gitPktLine)
   730  	r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32)
   731  	if err != nil {
   732  		return nil, fail(ctx, "Protocol: format parse error", "Pkt-Line format is wrong :%v", err)
   733  	}
   734  
   735  	if r.Length == 0 {
   736  		if requestType == pktLineTypeData {
   737  			return nil, fail(ctx, "Protocol: format data error", "Pkt-Line format is wrong")
   738  		}
   739  		r.Type = pktLineTypeFlush
   740  		return r, nil
   741  	}
   742  
   743  	if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush {
   744  		return nil, fail(ctx, "Protocol: format length error", "Pkt-Line format is wrong")
   745  	}
   746  
   747  	r.Data = make([]byte, r.Length-4)
   748  	for i := range r.Data {
   749  		r.Data[i], err = in.ReadByte()
   750  		if err != nil {
   751  			return nil, fail(ctx, "Protocol: data error", "Pkt-Line: read stdin failed : %v", err)
   752  		}
   753  	}
   754  
   755  	r.Type = pktLineTypeData
   756  
   757  	return r, nil
   758  }
   759  
   760  func writeFlushPktLine(ctx context.Context, out io.Writer) error {
   761  	l, err := out.Write([]byte("0000"))
   762  	if err != nil || l != 4 {
   763  		return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
   764  	}
   765  	return nil
   766  }
   767  
   768  func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error {
   769  	hexchar := []byte("0123456789abcdef")
   770  	hex := func(n uint64) byte {
   771  		return hexchar[(n)&15]
   772  	}
   773  
   774  	length := uint64(len(data) + 4)
   775  	tmp := make([]byte, 4)
   776  	tmp[0] = hex(length >> 12)
   777  	tmp[1] = hex(length >> 8)
   778  	tmp[2] = hex(length >> 4)
   779  	tmp[3] = hex(length)
   780  
   781  	lr, err := out.Write(tmp)
   782  	if err != nil || lr != 4 {
   783  		return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
   784  	}
   785  
   786  	lr, err = out.Write(data)
   787  	if err != nil || int(length-4) != lr {
   788  		return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err)
   789  	}
   790  
   791  	return nil
   792  }