792 lines · 21.3 KB · Go
RawDownload
// Copyright 2017 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT package cmd import (	"bufio"	"bytes"	"context"	"fmt"	"io"	"os"	"strconv"	"strings"	"time" 	"forgejo.org/modules/git"	"forgejo.org/modules/git/pushoptions"	"forgejo.org/modules/log"	"forgejo.org/modules/private"	repo_module "forgejo.org/modules/repository"	"forgejo.org/modules/setting" 	"github.com/urfave/cli/v3") const (	hookBatchSize = 30) // CmdHook represents the available hooks sub-command.func cmdHook() *cli.Command {	return &cli.Command{		Name:        "hook",		Usage:       "(internal) Should only be called by Git",		Description: "Delegate commands to corresponding Git hooks",		Before:      PrepareConsoleLoggerLevel(log.FATAL),		Commands: []*cli.Command{			subcmdHookPreReceive(),			subcmdHookUpdate(),			subcmdHookPostReceive(),			subcmdHookProcReceive(),		},	}} func subcmdHookPreReceive() *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 subcmdHookUpdate() *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 subcmdHookPostReceive() *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.29func subcmdHookProcReceive() *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 {	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(ctx context.Context, c *cli.Command) error {	if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {		return nil	}	ctx, cancel := installSignals(ctx)	defer cancel() 	setup(ctx, c.Bool("debug"), true) 	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 byForgejo 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		}	} 	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++ 		// All references should be checked because permission check was delayed.		oldCommitIDs[count] = oldCommitID		newCommitIDs[count] = newCommitID		refFullNames[count] = refFullName		count++		fmt.Fprint(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		}		if lastline >= hookBatchSize {			fmt.Fprint(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.Fprint(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#updatefunc runHookUpdate(ctx context.Context, c *cli.Command) 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(ctx)	defer cancel() 	if c.NArg() != 3 {		return nil	}	args := c.Args().Slice() 	// The arguments given to the hook are in order: reference name, old commit ID and new commit ID.	refFullName := git.RefName(args[0])	newCommitID := args[2] 	// Only process pull references.	if !refFullName.IsPull() {		return nil	} 	// Empty new commit ID means deletion.	if git.IsEmptyCommitID(newCommitID, nil) {		return fail(ctx, fmt.Sprintf("The deletion of %s is skipped as it's an internal reference.", refFullName), "")	} 	// If the new comment isn't empty it means modification.	return fail(ctx, fmt.Sprintf("The modification of %s is skipped as it's an internal reference.", refFullName), "")} func runHookPostReceive(ctx context.Context, c *cli.Command) error {	ctx, cancel := installSignals(ctx)	defer cancel() 	setup(ctx, c.Bool("debug"), true) 	// 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 byForgejo 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.Fprint(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(ctx context.Context, c *cli.Command) error {	ctx, cancel := installSignals(ctx)	defer cancel() 	setup(ctx, c.Bool("debug"), true) 	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {		if setting.OnlyAllowPushIfGiteaEnvironmentSet {

Lines 1–500 of 792.