diff --git a/cmd/hook.go b/cmd/hook.go
index aabd9637c..331f6a2d2 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -8,10 +8,12 @@ import (
 	"bufio"
 	"bytes"
 	"fmt"
+	"io"
 	"net/http"
 	"os"
 	"strconv"
 	"strings"
+	"time"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/git"
@@ -58,6 +60,85 @@ var (
 	}
 )
 
+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
+}
+
 func runHookPreReceive(c *cli.Context) error {
 	if os.Getenv(models.EnvIsInternal) == "true" {
 		return nil
@@ -101,6 +182,18 @@ Gitea or set your environment appropriately.`, "")
 	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 {
@@ -124,12 +217,10 @@ Gitea or set your environment appropriately.`, "")
 			newCommitIDs[count] = newCommitID
 			refFullNames[count] = refFullName
 			count++
-			fmt.Fprintf(os.Stdout, "*")
-			os.Stdout.Sync()
+			fmt.Fprintf(out, "*")
 
 			if count >= hookBatchSize {
-				fmt.Fprintf(os.Stdout, " Checking %d branches\n", count)
-				os.Stdout.Sync()
+				fmt.Fprintf(out, " Checking %d branches\n", count)
 
 				hookOptions.OldCommitIDs = oldCommitIDs
 				hookOptions.NewCommitIDs = newCommitIDs
@@ -147,12 +238,10 @@ Gitea or set your environment appropriately.`, "")
 				lastline = 0
 			}
 		} else {
-			fmt.Fprintf(os.Stdout, ".")
-			os.Stdout.Sync()
+			fmt.Fprintf(out, ".")
 		}
 		if lastline >= hookBatchSize {
-			fmt.Fprintf(os.Stdout, "\n")
-			os.Stdout.Sync()
+			fmt.Fprintf(out, "\n")
 			lastline = 0
 		}
 	}
@@ -162,8 +251,7 @@ Gitea or set your environment appropriately.`, "")
 		hookOptions.NewCommitIDs = newCommitIDs[:count]
 		hookOptions.RefFullNames = refFullNames[:count]
 
-		fmt.Fprintf(os.Stdout, " Checking %d branches\n", count)
-		os.Stdout.Sync()
+		fmt.Fprintf(out, " Checking %d branches\n", count)
 
 		statusCode, msg := private.HookPreReceive(username, reponame, hookOptions)
 		switch statusCode {
@@ -173,14 +261,11 @@ Gitea or set your environment appropriately.`, "")
 			fail(msg, "")
 		}
 	} else if lastline > 0 {
-		fmt.Fprintf(os.Stdout, "\n")
-		os.Stdout.Sync()
+		fmt.Fprintf(out, "\n")
 		lastline = 0
 	}
 
-	fmt.Fprintf(os.Stdout, "Checked %d references in total\n", total)
-	os.Stdout.Sync()
-
+	fmt.Fprintf(out, "Checked %d references in total\n", total)
 	return nil
 }
 
@@ -206,6 +291,19 @@ Gitea or set your environment appropriately.`, "")
 		}
 	}
 
+	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
+		}
+	}
+
 	// the environment setted on serv command
 	repoUser := os.Getenv(models.EnvRepoUsername)
 	isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
@@ -241,7 +339,7 @@ Gitea or set your environment appropriately.`, "")
 			continue
 		}
 
-		fmt.Fprintf(os.Stdout, ".")
+		fmt.Fprintf(out, ".")
 		oldCommitIDs[count] = string(fields[0])
 		newCommitIDs[count] = string(fields[1])
 		refFullNames[count] = string(fields[2])
@@ -250,16 +348,15 @@ Gitea or set your environment appropriately.`, "")
 		}
 		count++
 		total++
-		os.Stdout.Sync()
 
 		if count >= hookBatchSize {
-			fmt.Fprintf(os.Stdout, " Processing %d references\n", count)
-			os.Stdout.Sync()
+			fmt.Fprintf(out, " Processing %d references\n", count)
 			hookOptions.OldCommitIDs = oldCommitIDs
 			hookOptions.NewCommitIDs = newCommitIDs
 			hookOptions.RefFullNames = refFullNames
 			resp, err := private.HookPostReceive(repoUser, repoName, hookOptions)
 			if resp == nil {
+				_ = dWriter.Close()
 				hookPrintResults(results)
 				fail("Internal Server Error", err)
 			}
@@ -277,9 +374,9 @@ Gitea or set your environment appropriately.`, "")
 				fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err)
 			}
 		}
-		fmt.Fprintf(os.Stdout, "Processed %d references in total\n", total)
-		os.Stdout.Sync()
+		fmt.Fprintf(out, "Processed %d references in total\n", total)
 
+		_ = dWriter.Close()
 		hookPrintResults(results)
 		return nil
 	}
@@ -288,19 +385,18 @@ Gitea or set your environment appropriately.`, "")
 	hookOptions.NewCommitIDs = newCommitIDs[:count]
 	hookOptions.RefFullNames = refFullNames[:count]
 
-	fmt.Fprintf(os.Stdout, " Processing %d references\n", count)
-	os.Stdout.Sync()
+	fmt.Fprintf(out, " Processing %d references\n", count)
 
 	resp, err := private.HookPostReceive(repoUser, repoName, hookOptions)
 	if resp == nil {
+		_ = dWriter.Close()
 		hookPrintResults(results)
 		fail("Internal Server Error", err)
 	}
 	wasEmpty = wasEmpty || resp.RepoWasEmpty
 	results = append(results, resp.Results...)
 
-	fmt.Fprintf(os.Stdout, "Processed %d references in total\n", total)
-	os.Stdout.Sync()
+	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
@@ -309,7 +405,7 @@ Gitea or set your environment appropriately.`, "")
 			fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err)
 		}
 	}
-
+	_ = dWriter.Close()
 	hookPrintResults(results)
 
 	return nil
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index dc6a1ba34..ea17096ea 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -522,6 +522,8 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false`
 - `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view.
 - `GC_ARGS`: **\<empty\>**: Arguments for command `git gc`, e.g. `--aggressive --auto`. See more on http://git-scm.com/docs/git-gc/
 - `ENABLE_AUTO_GIT_WIRE_PROTOCOL`: **true**: If use git wire protocol version 2 when git version >= 2.18, default is true, set to false when you always want git wire protocol version 1
+- `VERBOSE_PUSH`: **true**: Print status information about pushes as they are being processed.
+- `VERBOSE_PUSH_DELAY`: **5s**: Only print verbose information if push takes longer than this delay.
 
 ## Git - Timeout settings (`git.timeout`)
 - `DEFAUlT`: **360**: Git operations default timeout seconds.
diff --git a/modules/setting/git.go b/modules/setting/git.go
index 8495be883..8c8179cba 100644
--- a/modules/setting/git.go
+++ b/modules/setting/git.go
@@ -21,6 +21,8 @@ var (
 		MaxGitDiffLines           int
 		MaxGitDiffLineCharacters  int
 		MaxGitDiffFiles           int
+		VerbosePush               bool
+		VerbosePushDelay          time.Duration
 		GCArgs                    []string `ini:"GC_ARGS" delim:" "`
 		EnableAutoGitWireProtocol bool
 		Timeout                   struct {
@@ -36,6 +38,8 @@ var (
 		MaxGitDiffLines:           1000,
 		MaxGitDiffLineCharacters:  5000,
 		MaxGitDiffFiles:           100,
+		VerbosePush:               true,
+		VerbosePushDelay:          5 * time.Second,
 		GCArgs:                    []string{},
 		EnableAutoGitWireProtocol: true,
 		Timeout: struct {