2017-02-23 06:40:44 +03:00
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
2017-02-25 17:54:40 +03:00
"bufio"
"bytes"
2017-02-23 06:40:44 +03:00
"fmt"
2020-01-12 11:46:03 +03:00
"io"
2019-06-01 18:00:21 +03:00
"net/http"
2017-02-23 06:40:44 +03:00
"os"
2017-02-25 17:54:40 +03:00
"strconv"
"strings"
2020-01-12 11:46:03 +03:00
"time"
2017-02-23 06:40:44 +03:00
"code.gitea.io/gitea/models"
2019-03-27 12:33:00 +03:00
"code.gitea.io/gitea/modules/git"
2017-05-04 08:42:02 +03:00
"code.gitea.io/gitea/modules/private"
2019-11-15 01:39:48 +03:00
"code.gitea.io/gitea/modules/setting"
2017-02-23 06:40:44 +03:00
"github.com/urfave/cli"
)
2019-12-26 14:29:45 +03:00
const (
hookBatchSize = 30
)
2017-02-23 06:40:44 +03:00
var (
// CmdHook represents the available hooks sub-command.
CmdHook = cli . Command {
Name : "hook" ,
Usage : "Delegate commands to corresponding Git hooks" ,
Description : "This should only be called by Git" ,
Subcommands : [ ] cli . Command {
subcmdHookPreReceive ,
2018-01-13 01:16:49 +03:00
subcmdHookUpdate ,
2017-02-23 06:40:44 +03:00
subcmdHookPostReceive ,
} ,
}
subcmdHookPreReceive = cli . Command {
Name : "pre-receive" ,
Usage : "Delegate pre-receive Git hook" ,
Description : "This command should only be called by Git" ,
Action : runHookPreReceive ,
}
2018-01-13 01:16:49 +03:00
subcmdHookUpdate = cli . Command {
2017-02-23 06:40:44 +03:00
Name : "update" ,
Usage : "Delegate update Git hook" ,
Description : "This command should only be called by Git" ,
Action : runHookUpdate ,
}
subcmdHookPostReceive = cli . Command {
Name : "post-receive" ,
Usage : "Delegate post-receive Git hook" ,
Description : "This command should only be called by Git" ,
Action : runHookPostReceive ,
}
)
2020-01-12 11:46:03 +03:00
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 == nil {
return nil
}
stopped := d . timer . Stop ( )
if stopped {
return nil
}
select {
case <- d . timer . C :
default :
}
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
}
2017-02-23 06:40:44 +03:00
func runHookPreReceive ( c * cli . Context ) error {
2019-12-28 00:15:04 +03:00
if os . Getenv ( models . EnvIsInternal ) == "true" {
return nil
}
setup ( "hooks/pre-receive.log" , false )
2017-02-23 06:40:44 +03:00
if len ( os . Getenv ( "SSH_ORIGINAL_COMMAND" ) ) == 0 {
2019-11-15 01:39:48 +03:00
if setting . OnlyAllowPushIfGiteaEnvironmentSet {
fail ( ` 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 . ` , "" )
} else {
return nil
}
2017-02-23 06:40:44 +03:00
}
2017-02-25 17:54:40 +03:00
// the environment setted on serv command
isWiki := ( os . Getenv ( models . EnvRepoIsWiki ) == "true" )
2017-09-14 11:16:22 +03:00
username := os . Getenv ( models . EnvRepoUsername )
reponame := os . Getenv ( models . EnvRepoName )
2019-06-01 18:00:21 +03:00
userID , _ := strconv . ParseInt ( os . Getenv ( models . EnvPusherID ) , 10 , 64 )
2019-07-01 04:18:13 +03:00
prID , _ := strconv . ParseInt ( os . Getenv ( models . ProtectedBranchPRID ) , 10 , 64 )
2019-10-21 11:21:45 +03:00
isDeployKey , _ := strconv . ParseBool ( os . Getenv ( models . EnvIsDeployKey ) )
2017-02-25 17:54:40 +03:00
2019-12-26 14:29:45 +03:00
hookOptions := private . HookOptions {
UserID : userID ,
GitAlternativeObjectDirectories : os . Getenv ( private . GitAlternativeObjectDirectories ) ,
GitObjectDirectory : os . Getenv ( private . GitObjectDirectory ) ,
GitQuarantinePath : os . Getenv ( private . GitQuarantinePath ) ,
ProtectedBranchID : prID ,
IsDeployKey : isDeployKey ,
}
2017-02-25 17:54:40 +03:00
scanner := bufio . NewScanner ( os . Stdin )
2019-12-26 14:29:45 +03:00
oldCommitIDs := make ( [ ] string , hookBatchSize )
newCommitIDs := make ( [ ] string , hookBatchSize )
refFullNames := make ( [ ] string , hookBatchSize )
count := 0
total := 0
lastline := 0
2020-01-12 11:46:03 +03:00
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
}
}
2019-12-26 14:29:45 +03:00
for scanner . Scan ( ) {
2017-02-25 17:54:40 +03:00
// TODO: support news feeds for wiki
if isWiki {
continue
}
fields := bytes . Fields ( scanner . Bytes ( ) )
if len ( fields ) != 3 {
continue
}
2017-09-14 11:16:22 +03:00
oldCommitID := string ( fields [ 0 ] )
2017-02-25 17:54:40 +03:00
newCommitID := string ( fields [ 1 ] )
refFullName := string ( fields [ 2 ] )
2019-12-26 14:29:45 +03:00
total ++
lastline ++
2017-02-25 17:54:40 +03:00
2019-05-14 17:40:27 +03:00
// If the ref is a branch, check if it's protected
if strings . HasPrefix ( refFullName , git . BranchPrefix ) {
2019-12-26 14:29:45 +03:00
oldCommitIDs [ count ] = oldCommitID
newCommitIDs [ count ] = newCommitID
refFullNames [ count ] = refFullName
count ++
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , "*" )
2019-12-26 14:29:45 +03:00
if count >= hookBatchSize {
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , " Checking %d branches\n" , count )
2019-12-26 14:29:45 +03:00
hookOptions . OldCommitIDs = oldCommitIDs
hookOptions . NewCommitIDs = newCommitIDs
hookOptions . RefFullNames = refFullNames
statusCode , msg := private . HookPreReceive ( username , reponame , hookOptions )
switch statusCode {
case http . StatusOK :
// no-op
case http . StatusInternalServerError :
fail ( "Internal Server Error" , msg )
default :
fail ( msg , "" )
}
count = 0
lastline = 0
2017-03-01 18:01:03 +03:00
}
2019-12-26 14:29:45 +03:00
} else {
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , "." )
2019-12-26 14:29:45 +03:00
}
if lastline >= hookBatchSize {
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , "\n" )
2019-12-26 14:29:45 +03:00
lastline = 0
2017-02-25 17:54:40 +03:00
}
}
2019-12-26 14:29:45 +03:00
if count > 0 {
hookOptions . OldCommitIDs = oldCommitIDs [ : count ]
hookOptions . NewCommitIDs = newCommitIDs [ : count ]
hookOptions . RefFullNames = refFullNames [ : count ]
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , " Checking %d branches\n" , count )
2019-12-26 14:29:45 +03:00
statusCode , msg := private . HookPreReceive ( username , reponame , hookOptions )
switch statusCode {
case http . StatusInternalServerError :
fail ( "Internal Server Error" , msg )
case http . StatusForbidden :
fail ( msg , "" )
}
} else if lastline > 0 {
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , "\n" )
2019-12-26 14:29:45 +03:00
lastline = 0
}
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , "Checked %d references in total\n" , total )
2017-02-23 06:40:44 +03:00
return nil
}
func runHookUpdate ( c * cli . Context ) error {
2019-12-27 18:21:33 +03:00
// Update is empty and is kept only for backwards compatibility
2017-02-23 06:40:44 +03:00
return nil
}
func runHookPostReceive ( c * cli . Context ) error {
2019-12-28 00:15:04 +03:00
if os . Getenv ( models . EnvIsInternal ) == "true" {
return nil
}
setup ( "hooks/post-receive.log" , false )
2017-02-23 06:40:44 +03:00
if len ( os . Getenv ( "SSH_ORIGINAL_COMMAND" ) ) == 0 {
2019-11-15 01:39:48 +03:00
if setting . OnlyAllowPushIfGiteaEnvironmentSet {
fail ( ` 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 . ` , "" )
} else {
return nil
}
2017-02-23 06:40:44 +03:00
}
2020-01-12 11:46:03 +03:00
var out io . Writer
var dWriter * delayWriter
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
}
}
2017-02-25 17:54:40 +03:00
// the environment setted on serv command
repoUser := os . Getenv ( models . EnvRepoUsername )
isWiki := ( os . Getenv ( models . EnvRepoIsWiki ) == "true" )
repoName := os . Getenv ( models . EnvRepoName )
pusherID , _ := strconv . ParseInt ( os . Getenv ( models . EnvPusherID ) , 10 , 64 )
pusherName := os . Getenv ( models . EnvPusherName )
2019-12-26 14:29:45 +03:00
hookOptions := private . HookOptions {
UserName : pusherName ,
UserID : pusherID ,
GitAlternativeObjectDirectories : os . Getenv ( private . GitAlternativeObjectDirectories ) ,
GitObjectDirectory : os . Getenv ( private . GitObjectDirectory ) ,
GitQuarantinePath : os . Getenv ( private . GitQuarantinePath ) ,
}
oldCommitIDs := make ( [ ] string , hookBatchSize )
newCommitIDs := make ( [ ] string , hookBatchSize )
refFullNames := make ( [ ] string , hookBatchSize )
count := 0
total := 0
wasEmpty := false
masterPushed := false
results := make ( [ ] private . HookPostReceiveBranchResult , 0 )
2017-02-25 17:54:40 +03:00
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
}
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , "." )
2019-12-26 14:29:45 +03:00
oldCommitIDs [ count ] = string ( fields [ 0 ] )
newCommitIDs [ count ] = string ( fields [ 1 ] )
refFullNames [ count ] = string ( fields [ 2 ] )
if refFullNames [ count ] == git . BranchPrefix + "master" && newCommitIDs [ count ] != git . EmptySHA && count == total {
masterPushed = true
}
count ++
total ++
if count >= hookBatchSize {
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , " Processing %d references\n" , count )
2019-12-26 14:29:45 +03:00
hookOptions . OldCommitIDs = oldCommitIDs
hookOptions . NewCommitIDs = newCommitIDs
hookOptions . RefFullNames = refFullNames
resp , err := private . HookPostReceive ( repoUser , repoName , hookOptions )
if resp == nil {
2020-01-12 11:46:03 +03:00
_ = dWriter . Close ( )
2019-12-26 14:29:45 +03:00
hookPrintResults ( results )
fail ( "Internal Server Error" , err )
}
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
err := private . SetDefaultBranch ( repoUser , repoName , "master" )
if err != nil {
fail ( "Internal Server Error" , "SetDefaultBranch failed with Error: %v" , err )
}
}
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , "Processed %d references in total\n" , total )
2019-12-26 14:29:45 +03:00
2020-01-12 11:46:03 +03:00
_ = dWriter . Close ( )
2019-12-26 14:29:45 +03:00
hookPrintResults ( results )
return nil
}
2017-02-25 17:54:40 +03:00
2019-12-26 14:29:45 +03:00
hookOptions . OldCommitIDs = oldCommitIDs [ : count ]
hookOptions . NewCommitIDs = newCommitIDs [ : count ]
hookOptions . RefFullNames = refFullNames [ : count ]
2018-10-20 09:59:06 +03:00
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , " Processing %d references\n" , count )
2019-12-26 14:29:45 +03:00
resp , err := private . HookPostReceive ( repoUser , repoName , hookOptions )
if resp == nil {
2020-01-12 11:46:03 +03:00
_ = dWriter . Close ( )
2019-12-26 14:29:45 +03:00
hookPrintResults ( results )
fail ( "Internal Server Error" , err )
}
wasEmpty = wasEmpty || resp . RepoWasEmpty
results = append ( results , resp . Results ... )
2020-01-12 11:46:03 +03:00
fmt . Fprintf ( out , "Processed %d references in total\n" , total )
2019-12-26 14:29:45 +03:00
if wasEmpty && masterPushed {
// We need to tell the repo to reset the default branch to master
err := private . SetDefaultBranch ( repoUser , repoName , "master" )
if err != nil {
fail ( "Internal Server Error" , "SetDefaultBranch failed with Error: %v" , err )
2019-06-01 18:00:21 +03:00
}
2019-12-26 14:29:45 +03:00
}
2020-01-12 11:46:03 +03:00
_ = dWriter . Close ( )
2019-12-26 14:29:45 +03:00
hookPrintResults ( results )
2018-10-20 09:59:06 +03:00
2019-12-26 14:29:45 +03:00
return nil
}
func hookPrintResults ( results [ ] private . HookPostReceiveBranchResult ) {
for _ , res := range results {
if ! res . Message {
2019-06-01 18:00:21 +03:00
continue
2018-10-20 09:59:06 +03:00
}
2019-06-01 18:00:21 +03:00
fmt . Fprintln ( os . Stderr , "" )
2019-12-26 14:29:45 +03:00
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 )
2019-06-01 18:00:21 +03:00
} else {
fmt . Fprint ( os . Stderr , "Visit the existing pull request:\n" )
2019-12-26 14:29:45 +03:00
fmt . Fprintf ( os . Stderr , " %s\n" , res . URL )
2019-06-01 18:00:21 +03:00
}
fmt . Fprintln ( os . Stderr , "" )
2019-12-26 14:29:45 +03:00
os . Stderr . Sync ( )
2017-02-25 17:54:40 +03:00
}
2017-02-23 06:40:44 +03:00
}