From 370516883717de0e6e2087c12d368eb1465ee3b0 Mon Sep 17 00:00:00 2001
From: a1012112796 <1012112796@qq.com>
Date: Wed, 28 Jul 2021 17:42:56 +0800
Subject: [PATCH] Add agit flow support in gitea (#14295)

* feature: add agit flow support

ref: https://git-repo.info/en/2020/03/agit-flow-and-git-repo/

example:

```Bash
git checkout -b test
echo "test" >> README.md
git commit -m "test"
git push origin HEAD:refs/for/master -o topic=test
```

Signed-off-by: a1012112796 <1012112796@qq.com>

* fix lint

* simplify code add fix some nits

* update merge help message

* Apply suggestions from code review. Thanks @jiangxin

* add forced-update message

* fix lint

* splite writePktLine

* add refs/for/<target-branch>/<topic-branch> support also

* Add test code add fix api

* fix lint

* fix test

* skip test if git version < 2.29

* try test with git 2.30.1

* fix permission check bug

* fix some nit

* logic implify and test code update

* fix bug

* apply suggestions from code review

* prepare for merge

Signed-off-by: Andrew Thornton <art27@cantab.net>

* fix permission check bug

- test code update
- apply suggestions from code review @zeripath

Signed-off-by: a1012112796 <1012112796@qq.com>

* fix bug when target branch isn't exist

* prevent some special push and fix some nits

* fix lint

* try splite

* Apply suggestions from code review

- fix permission check
- handle user rename

* fix version negotiation

* remane

* fix template

* handle empty repo

* ui: fix  branch link under the title

* fix nits

Co-authored-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 cmd/hook.go                                 | 346 +++++++++++++++++++-
 cmd/hook_test.go                            |  41 +++
 cmd/serv.go                                 |   8 +
 integrations/git_test.go                    | 161 +++++++++
 models/migrations/migrations.go             |   2 +
 models/migrations/v190.go                   |  24 ++
 models/pull.go                              |  39 ++-
 models/pull_list.go                         |   4 +-
 models/pull_test.go                         |   4 +-
 modules/convert/pull.go                     |  20 +-
 modules/git/git.go                          |  61 ++++
 modules/git/repo_branch.go                  |   8 +
 modules/private/hook.go                     |  46 +++
 modules/repository/hooks.go                 |   9 +
 routers/api/v1/repo/pull.go                 |   2 +-
 routers/private/hook.go                     | 142 +++++++-
 routers/private/internal.go                 |   1 +
 routers/private/serv.go                     |   6 +
 routers/web/repo/compare.go                 |   2 +-
 routers/web/repo/http.go                    |   5 +
 routers/web/repo/issue.go                   |   3 +-
 routers/web/repo/pull.go                    |  12 +-
 routers/web/user/setting/profile.go         |   9 +
 routers/web/web.go                          |  16 +
 services/agit/agit.go                       | 288 ++++++++++++++++
 services/pull/commit_status.go              |  14 +-
 services/pull/pull.go                       |  62 +++-
 services/pull/temp_repo.go                  |  12 +-
 services/pull/update.go                     |   9 +
 templates/repo/issue/view_content/pull.tmpl |  10 +-
 30 files changed, 1334 insertions(+), 32 deletions(-)
 create mode 100644 cmd/hook_test.go
 create mode 100644 models/migrations/v190.go
 create mode 100644 services/agit/agit.go

diff --git a/cmd/hook.go b/cmd/hook.go
index 87f1f37562..fb43add8d4 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -38,6 +38,7 @@ var (
 			subcmdHookPreReceive,
 			subcmdHookUpdate,
 			subcmdHookPostReceive,
+			subcmdHookProcReceive,
 		},
 	}
 
@@ -74,6 +75,18 @@ var (
 			},
 		},
 	}
+	// Note: new hook since git 2.29
+	subcmdHookProcReceive = 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 {
@@ -205,6 +218,11 @@ Gitea or set your environment appropriately.`, "")
 		}
 	}
 
+	supportProcRecive := false
+	if git.CheckGitVersionAtLeast("2.29") == nil {
+		supportProcRecive = true
+	}
+
 	for scanner.Scan() {
 		// TODO: support news feeds for wiki
 		if isWiki {
@@ -223,7 +241,9 @@ Gitea or set your environment appropriately.`, "")
 		lastline++
 
 		// If the ref is a branch or tag, check if it's protected
-		if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
+		// if supportProcRecive all ref should be checked because
+		// permission check was delayed
+		if supportProcRecive || strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
 			oldCommitIDs[count] = oldCommitID
 			newCommitIDs[count] = newCommitID
 			refFullNames[count] = refFullName
@@ -463,3 +483,327 @@ func pushOptions() map[string]string {
 	}
 	return opts
 }
+
+func runHookProcReceive(c *cli.Context) error {
+	setup("hooks/proc-receive.log", c.Bool("debug"))
+
+	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
+		if setting.OnlyAllowPushIfGiteaEnvironmentSet {
+			return 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.`, "")
+		}
+		return nil
+	}
+
+	ctx, cancel := installSignals()
+	defer cancel()
+
+	if git.CheckGitVersionAtLeast("2.29") != nil {
+		return fail("Internal Server Error", "git not support proc-receive.")
+	}
+
+	reader := bufio.NewReader(os.Stdin)
+	repoUser := os.Getenv(models.EnvRepoUsername)
+	repoName := os.Getenv(models.EnvRepoName)
+	pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
+	pusherName := os.Getenv(models.EnvPusherName)
+
+	// 1. Version and features negotiation.
+	// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n)
+	// S: flush-pkt
+	// H: PKT-LINE(version=1\0push-options...)
+	// H: flush-pkt
+
+	rs, err := readPktLine(reader, pktLineTypeData)
+	if err != nil {
+		return err
+	}
+
+	const VersionHead string = "version=1"
+
+	var (
+		hasPushOptions bool
+		response       = []byte(VersionHead)
+		requestOptions []string
+	)
+
+	index := bytes.IndexByte(rs.Data, byte(0))
+	if index >= len(rs.Data) {
+		return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data))
+	}
+
+	if index < 0 {
+		if len(rs.Data) == 10 && rs.Data[9] == '\n' {
+			index = 9
+		} else {
+			return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data))
+		}
+	}
+
+	if string(rs.Data[0:index]) != VersionHead {
+		return fail("Internal Server Error", "Received unsupported version: %s", string(rs.Data[0:index]))
+	}
+	requestOptions = strings.Split(string(rs.Data[index+1:]), " ")
+
+	for _, option := range requestOptions {
+		if strings.HasPrefix(option, "push-options") {
+			response = append(response, byte(0))
+			response = append(response, []byte("push-options")...)
+			hasPushOptions = true
+		}
+	}
+	response = append(response, '\n')
+
+	_, err = readPktLine(reader, pktLineTypeFlush)
+	if err != nil {
+		return err
+	}
+
+	err = writeDataPktLine(os.Stdout, response)
+	if err != nil {
+		return err
+	}
+
+	err = writeFlushPktLine(os.Stdout)
+	if err != nil {
+		return err
+	}
+
+	// 2. receive commands from server.
+	// S: PKT-LINE(<old-oid> <new-oid> <ref>)
+	// S: ... ...
+	// S: flush-pkt
+	// # [receive push-options]
+	// S: PKT-LINE(push-option)
+	// S: ... ...
+	// S: flush-pkt
+	hookOptions := private.HookOptions{
+		UserName: pusherName,
+		UserID:   pusherID,
+	}
+	hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
+	hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
+	hookOptions.RefFullNames = make([]string, 0, hookBatchSize)
+
+	for {
+		// note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed
+		rs, err = readPktLine(reader, pktLineTypeUnknow)
+		if err != nil {
+			return err
+		}
+
+		if rs.Type == pktLineTypeFlush {
+			break
+		}
+		t := strings.SplitN(string(rs.Data), " ", 3)
+		if len(t) != 3 {
+			continue
+		}
+		hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0])
+		hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1])
+		hookOptions.RefFullNames = append(hookOptions.RefFullNames, t[2])
+	}
+
+	hookOptions.GitPushOptions = make(map[string]string)
+
+	if hasPushOptions {
+		for {
+			rs, err = readPktLine(reader, pktLineTypeUnknow)
+			if err != nil {
+				return err
+			}
+
+			if rs.Type == pktLineTypeFlush {
+				break
+			}
+
+			kv := strings.SplitN(string(rs.Data), "=", 2)
+			if len(kv) == 2 {
+				hookOptions.GitPushOptions[kv[0]] = kv[1]
+			}
+		}
+	}
+
+	// 3. run hook
+	resp, err := private.HookProcReceive(ctx, repoUser, repoName, hookOptions)
+	if err != nil {
+		return fail("Internal Server Error", "run proc-receive hook failed :%v", err)
+	}
+
+	// 4. response result to service
+	// # a. OK, but has an alternate reference.  The alternate reference name
+	// # and other status can be given in option directives.
+	// H: PKT-LINE(ok <ref>)
+	// H: PKT-LINE(option refname <refname>)
+	// H: PKT-LINE(option old-oid <old-oid>)
+	// H: PKT-LINE(option new-oid <new-oid>)
+	// H: PKT-LINE(option forced-update)
+	// H: ... ...
+	// H: flush-pkt
+	// # b. NO, I reject it.
+	// H: PKT-LINE(ng <ref> <reason>)
+	// # c. Fall through, let 'receive-pack' to execute it.
+	// H: PKT-LINE(ok <ref>)
+	// H: PKT-LINE(option fall-through)
+
+	for _, rs := range resp.Results {
+		if len(rs.Err) > 0 {
+			err = writeDataPktLine(os.Stdout, []byte("ng "+rs.OriginalRef+" "+rs.Err))
+			if err != nil {
+				return err
+			}
+			continue
+		}
+
+		if rs.IsNotMatched {
+			err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef))
+			if err != nil {
+				return err
+			}
+			err = writeDataPktLine(os.Stdout, []byte("option fall-through"))
+			if err != nil {
+				return err
+			}
+			continue
+		}
+
+		err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef))
+		if err != nil {
+			return err
+		}
+		err = writeDataPktLine(os.Stdout, []byte("option refname "+rs.Ref))
+		if err != nil {
+			return err
+		}
+		if rs.OldOID != git.EmptySHA {
+			err = writeDataPktLine(os.Stdout, []byte("option old-oid "+rs.OldOID))
+			if err != nil {
+				return err
+			}
+		}
+		err = writeDataPktLine(os.Stdout, []byte("option new-oid "+rs.NewOID))
+		if err != nil {
+			return err
+		}
+		if rs.IsForcePush {
+			err = writeDataPktLine(os.Stdout, []byte("option forced-update"))
+			if err != nil {
+				return err
+			}
+		}
+	}
+	err = writeFlushPktLine(os.Stdout)
+
+	return err
+}
+
+// git PKT-Line api
+// pktLineType message type of pkt-line
+type pktLineType int64
+
+const (
+	// UnKnow type
+	pktLineTypeUnknow pktLineType = 0
+	// flush-pkt "0000"
+	pktLineTypeFlush pktLineType = iota
+	// data line
+	pktLineTypeData
+)
+
+// gitPktLine pkt-line api
+type gitPktLine struct {
+	Type   pktLineType
+	Length uint64
+	Data   []byte
+}
+
+func readPktLine(in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) {
+	var (
+		err error
+		r   *gitPktLine
+	)
+
+	// read prefix
+	lengthBytes := make([]byte, 4)
+	for i := 0; i < 4; i++ {
+		lengthBytes[i], err = in.ReadByte()
+		if err != nil {
+			return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err)
+		}
+	}
+
+	r = new(gitPktLine)
+	r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32)
+	if err != nil {
+		return nil, fail("Internal Server Error", "Pkt-Line format is wrong :%v", err)
+	}
+
+	if r.Length == 0 {
+		if requestType == pktLineTypeData {
+			return nil, fail("Internal Server Error", "Pkt-Line format is wrong")
+		}
+		r.Type = pktLineTypeFlush
+		return r, nil
+	}
+
+	if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush {
+		return nil, fail("Internal Server Error", "Pkt-Line format is wrong")
+	}
+
+	r.Data = make([]byte, r.Length-4)
+	for i := range r.Data {
+		r.Data[i], err = in.ReadByte()
+		if err != nil {
+			return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err)
+		}
+	}
+
+	r.Type = pktLineTypeData
+
+	return r, nil
+}
+
+func writeFlushPktLine(out io.Writer) error {
+	l, err := out.Write([]byte("0000"))
+	if err != nil {
+		return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
+	}
+	if l != 4 {
+		return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
+	}
+
+	return nil
+}
+
+func writeDataPktLine(out io.Writer, data []byte) error {
+	hexchar := []byte("0123456789abcdef")
+	hex := func(n uint64) byte {
+		return hexchar[(n)&15]
+	}
+
+	length := uint64(len(data) + 4)
+	tmp := make([]byte, 4)
+	tmp[0] = hex(length >> 12)
+	tmp[1] = hex(length >> 8)
+	tmp[2] = hex(length >> 4)
+	tmp[3] = hex(length)
+
+	lr, err := out.Write(tmp)
+	if err != nil {
+		return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
+	}
+	if 4 != lr {
+		return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
+	}
+
+	lr, err = out.Write(data)
+	if err != nil {
+		return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
+	}
+	if int(length-4) != lr {
+		return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
+	}
+
+	return nil
+}
diff --git a/cmd/hook_test.go b/cmd/hook_test.go
new file mode 100644
index 0000000000..92c7e82a9a
--- /dev/null
+++ b/cmd/hook_test.go
@@ -0,0 +1,41 @@
+// Copyright 2021 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 (
+	"bufio"
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPktLine(t *testing.T) {
+	// test read
+	s := strings.NewReader("0000")
+	r := bufio.NewReader(s)
+	result, err := readPktLine(r, pktLineTypeFlush)
+	assert.NoError(t, err)
+	assert.Equal(t, pktLineTypeFlush, result.Type)
+
+	s = strings.NewReader("0006a\n")
+	r = bufio.NewReader(s)
+	result, err = readPktLine(r, pktLineTypeData)
+	assert.NoError(t, err)
+	assert.Equal(t, pktLineTypeData, result.Type)
+	assert.Equal(t, []byte("a\n"), result.Data)
+
+	// test write
+	w := bytes.NewBuffer([]byte{})
+	err = writeFlushPktLine(w)
+	assert.NoError(t, err)
+	assert.Equal(t, []byte("0000"), w.Bytes())
+
+	w.Reset()
+	err = writeDataPktLine(w, []byte("a\nb"))
+	assert.NoError(t, err)
+	assert.Equal(t, []byte("0007a\nb"), w.Bytes())
+}
diff --git a/cmd/serv.go b/cmd/serv.go
index 4479902ea1..2173a3a38b 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -17,6 +17,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/pprof"
@@ -146,6 +147,13 @@ func runServ(c *cli.Context) error {
 	}
 
 	if len(words) < 2 {
+		if git.CheckGitVersionAtLeast("2.29") == nil {
+			// for AGit Flow
+			if cmd == "ssh_info" {
+				fmt.Print(`{"type":"gitea","version":1}`)
+				return nil
+			}
+		}
 		return fail("Too few arguments", "Too few arguments in cmd: %s", cmd)
 	}
 
diff --git a/integrations/git_test.go b/integrations/git_test.go
index a9848eaa4c..38d7b29b2b 100644
--- a/integrations/git_test.go
+++ b/integrations/git_test.go
@@ -70,6 +70,7 @@ func testGit(t *testing.T, u *url.URL) {
 		rawTest(t, &httpContext, little, big, littleLFS, bigLFS)
 		mediaTest(t, &httpContext, little, big, littleLFS, bigLFS)
 
+		t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "master", "test/head"))
 		t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
 		t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
 		t.Run("MergeFork", func(t *testing.T) {
@@ -111,6 +112,7 @@ func testGit(t *testing.T, u *url.URL) {
 			rawTest(t, &sshContext, little, big, littleLFS, bigLFS)
 			mediaTest(t, &sshContext, little, big, littleLFS, bigLFS)
 
+			t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "master", "test/head2"))
 			t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
 			t.Run("MergeFork", func(t *testing.T) {
 				defer PrintCurrentTest(t)()
@@ -593,3 +595,162 @@ func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testin
 		ctx.Session.MakeRequest(t, req, http.StatusOK)
 	}
 }
+
+func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) {
+	return func(t *testing.T) {
+		defer PrintCurrentTest(t)()
+
+		// skip this test if git version is low
+		if git.CheckGitVersionAtLeast("2.29") != nil {
+			return
+		}
+
+		gitRepo, err := git.OpenRepository(dstPath)
+		if !assert.NoError(t, err) {
+			return
+		}
+		defer gitRepo.Close()
+
+		var (
+			pr1, pr2 *models.PullRequest
+			commit   string
+		)
+		repo, err := models.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
+		if !assert.NoError(t, err) {
+			return
+		}
+
+		pullNum := models.GetCount(t, &models.PullRequest{})
+
+		t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
+
+		t.Run("AddCommit", func(t *testing.T) {
+			err := ioutil.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0666)
+			if !assert.NoError(t, err) {
+				return
+			}
+
+			err = git.AddChanges(dstPath, true)
+			assert.NoError(t, err)
+
+			err = git.CommitChanges(dstPath, git.CommitChangesOptions{
+				Committer: &git.Signature{
+					Email: "user2@example.com",
+					Name:  "user2",
+					When:  time.Now(),
+				},
+				Author: &git.Signature{
+					Email: "user2@example.com",
+					Name:  "user2",
+					When:  time.Now(),
+				},
+				Message: "Testing commit 1",
+			})
+			assert.NoError(t, err)
+			commit, err = gitRepo.GetRefCommitID("HEAD")
+			assert.NoError(t, err)
+		})
+
+		t.Run("Push", func(t *testing.T) {
+			_, err := git.NewCommand("push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).RunInDir(dstPath)
+			if !assert.NoError(t, err) {
+				return
+			}
+			models.AssertCount(t, &models.PullRequest{}, pullNum+1)
+			pr1 = models.AssertExistsAndLoadBean(t, &models.PullRequest{
+				HeadRepoID: repo.ID,
+				Flow:       models.PullRequestFlowAGit,
+			}).(*models.PullRequest)
+			if !assert.NotEmpty(t, pr1) {
+				return
+			}
+			prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
+			if !assert.NoError(t, err) {
+				return
+			}
+			assert.Equal(t, "user2/"+headBranch, pr1.HeadBranch)
+			assert.Equal(t, false, prMsg.HasMerged)
+			assert.Contains(t, "Testing commit 1", prMsg.Body)
+			assert.Equal(t, commit, prMsg.Head.Sha)
+
+			_, err = git.NewCommand("push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunInDir(dstPath)
+			if !assert.NoError(t, err) {
+				return
+			}
+			models.AssertCount(t, &models.PullRequest{}, pullNum+2)
+			pr2 = models.AssertExistsAndLoadBean(t, &models.PullRequest{
+				HeadRepoID: repo.ID,
+				Index:      pr1.Index + 1,
+				Flow:       models.PullRequestFlowAGit,
+			}).(*models.PullRequest)
+			if !assert.NotEmpty(t, pr2) {
+				return
+			}
+			prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
+			if !assert.NoError(t, err) {
+				return
+			}
+			assert.Equal(t, "user2/test/"+headBranch, pr2.HeadBranch)
+			assert.Equal(t, false, prMsg.HasMerged)
+		})
+
+		if pr1 == nil || pr2 == nil {
+			return
+		}
+
+		t.Run("AddCommit2", func(t *testing.T) {
+			err := ioutil.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0666)
+			if !assert.NoError(t, err) {
+				return
+			}
+
+			err = git.AddChanges(dstPath, true)
+			assert.NoError(t, err)
+
+			err = git.CommitChanges(dstPath, git.CommitChangesOptions{
+				Committer: &git.Signature{
+					Email: "user2@example.com",
+					Name:  "user2",
+					When:  time.Now(),
+				},
+				Author: &git.Signature{
+					Email: "user2@example.com",
+					Name:  "user2",
+					When:  time.Now(),
+				},
+				Message: "Testing commit 2",
+			})
+			assert.NoError(t, err)
+			commit, err = gitRepo.GetRefCommitID("HEAD")
+			assert.NoError(t, err)
+		})
+
+		t.Run("Push2", func(t *testing.T) {
+			_, err := git.NewCommand("push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).RunInDir(dstPath)
+			if !assert.NoError(t, err) {
+				return
+			}
+			models.AssertCount(t, &models.PullRequest{}, pullNum+2)
+			prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
+			if !assert.NoError(t, err) {
+				return
+			}
+			assert.Equal(t, false, prMsg.HasMerged)
+			assert.Equal(t, commit, prMsg.Head.Sha)
+
+			_, err = git.NewCommand("push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunInDir(dstPath)
+			if !assert.NoError(t, err) {
+				return
+			}
+			models.AssertCount(t, &models.PullRequest{}, pullNum+2)
+			prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
+			if !assert.NoError(t, err) {
+				return
+			}
+			assert.Equal(t, false, prMsg.HasMerged)
+			assert.Equal(t, commit, prMsg.Head.Sha)
+		})
+		t.Run("Merge", doAPIMergePullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index))
+		t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
+	}
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index fed7b909c1..9b04a364ca 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -329,6 +329,8 @@ var migrations = []Migration{
 	NewMigration("Add key is verified to gpg key", addKeyIsVerified),
 	// v189 -> v190
 	NewMigration("Unwrap ldap.Sources", unwrapLDAPSourceCfg),
+	// v190 -> v191
+	NewMigration("Add agit flow pull request support", addAgitFlowPullRequest),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v190.go b/models/migrations/v190.go
new file mode 100644
index 0000000000..8d1fba8373
--- /dev/null
+++ b/models/migrations/v190.go
@@ -0,0 +1,24 @@
+// Copyright 2021 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 migrations
+
+import (
+	"fmt"
+
+	"xorm.io/xorm"
+)
+
+func addAgitFlowPullRequest(x *xorm.Engine) error {
+	type PullRequestFlow int
+
+	type PullRequest struct {
+		Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"`
+	}
+
+	if err := x.Sync2(new(PullRequest)); err != nil {
+		return fmt.Errorf("sync2: %v", err)
+	}
+	return nil
+}
diff --git a/models/pull.go b/models/pull.go
index 3717878f42..e8e815385d 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -38,6 +38,16 @@ const (
 	PullRequestStatusEmpty
 )
 
+// PullRequestFlow the flow of pull request
+type PullRequestFlow int
+
+const (
+	// PullRequestFlowGithub github flow from head branch to base branch
+	PullRequestFlowGithub PullRequestFlow = iota
+	// PullRequestFlowAGit Agit flow pull request, head branch is not exist
+	PullRequestFlowAGit
+)
+
 // PullRequest represents relation between pull request and repositories.
 type PullRequest struct {
 	ID              int64 `xorm:"pk autoincr"`
@@ -58,6 +68,7 @@ type PullRequest struct {
 	BaseRepoID      int64       `xorm:"INDEX"`
 	BaseRepo        *Repository `xorm:"-"`
 	HeadBranch      string
+	HeadCommitID    string `xorm:"-"`
 	BaseBranch      string
 	ProtectedBranch *ProtectedBranch `xorm:"-"`
 	MergeBase       string           `xorm:"VARCHAR(40)"`
@@ -69,6 +80,8 @@ type PullRequest struct {
 	MergedUnix     timeutil.TimeStamp `xorm:"updated INDEX"`
 
 	isHeadRepoLoaded bool `xorm:"-"`
+
+	Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"`
 }
 
 // MustHeadUserName returns the HeadRepo's username if failed return blank
@@ -470,11 +483,11 @@ func NewPullRequest(repo *Repository, issue *Issue, labelIDs []int64, uuids []st
 
 // GetUnmergedPullRequest returns a pull request that is open and has not been merged
 // by given head/base and repo/branch.
-func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) {
+func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) {
 	pr := new(PullRequest)
 	has, err := x.
-		Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?",
-			headRepoID, headBranch, baseRepoID, baseBranch, false, false).
+		Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?",
+			headRepoID, headBranch, baseRepoID, baseBranch, false, flow, false).
 		Join("INNER", "issue", "issue.id=pull_request.issue_id").
 		Get(pr)
 	if err != nil {
@@ -491,7 +504,7 @@ func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch
 func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) {
 	pr := new(PullRequest)
 	has, err := x.
-		Where("head_repo_id = ? AND head_branch = ?", repoID, branch).
+		Where("head_repo_id = ? AND head_branch = ? AND flow = ?", repoID, branch, PullRequestFlowGithub).
 		OrderBy("id DESC").
 		Get(pr)
 	if !has {
@@ -566,6 +579,20 @@ func getPullRequestByIssueID(e Engine, issueID int64) (*PullRequest, error) {
 	return pr, pr.loadAttributes(e)
 }
 
+// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
+// By poster id.
+func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) {
+	pulls := make([]*PullRequest, 0, 10)
+
+	err := x.
+		Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?",
+			false, PullRequestFlowAGit, false, uid).
+		Join("INNER", "issue", "issue.id=pull_request.issue_id").
+		Find(&pulls)
+
+	return pulls, err
+}
+
 // GetPullRequestByIssueID returns pull request by given issue ID.
 func GetPullRequestByIssueID(issueID int64) (*PullRequest, error) {
 	return getPullRequestByIssueID(x, issueID)
@@ -663,6 +690,10 @@ func (pr *PullRequest) GetBaseBranchHTMLURL() string {
 
 // GetHeadBranchHTMLURL returns the HTML URL of the head branch
 func (pr *PullRequest) GetHeadBranchHTMLURL() string {
+	if pr.Flow == PullRequestFlowAGit {
+		return ""
+	}
+
 	if err := pr.LoadHeadRepo(); err != nil {
 		log.Error("LoadHeadRepo: %v", err)
 		return ""
diff --git a/models/pull_list.go b/models/pull_list.go
index 989de46891..2f685e19f5 100644
--- a/models/pull_list.go
+++ b/models/pull_list.go
@@ -51,8 +51,8 @@ func listPullRequestStatement(baseRepoID int64, opts *PullRequestsOptions) (*xor
 func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) {
 	prs := make([]*PullRequest, 0, 2)
 	return prs, x.
-		Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ?",
-			repoID, branch, false, false).
+		Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?",
+			repoID, branch, false, false, PullRequestFlowGithub).
 		Join("INNER", "issue", "issue.id = pull_request.issue_id").
 		Find(&prs)
 }
diff --git a/models/pull_test.go b/models/pull_test.go
index 5eaeb60e67..07216da324 100644
--- a/models/pull_test.go
+++ b/models/pull_test.go
@@ -92,11 +92,11 @@ func TestPullRequestsOldest(t *testing.T) {
 
 func TestGetUnmergedPullRequest(t *testing.T) {
 	assert.NoError(t, PrepareTestDatabase())
-	pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master")
+	pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master", PullRequestFlowGithub)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(2), pr.ID)
 
-	_, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master")
+	_, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", PullRequestFlowGithub)
 	assert.Error(t, err)
 	assert.True(t, IsErrPullRequestNotExist(err))
 }
diff --git a/modules/convert/pull.go b/modules/convert/pull.go
index 8bdf17a049..6c5d15c82e 100644
--- a/modules/convert/pull.go
+++ b/modules/convert/pull.go
@@ -95,7 +95,25 @@ func ToAPIPullRequest(pr *models.PullRequest) *api.PullRequest {
 		}
 	}
 
-	if pr.HeadRepo != nil {
+	if pr.Flow == models.PullRequestFlowAGit {
+		gitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
+		if err != nil {
+			log.Error("OpenRepository[%s]: %v", pr.GetGitRefName(), err)
+			return nil
+		}
+		defer gitRepo.Close()
+
+		apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
+		if err != nil {
+			log.Error("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
+			return nil
+		}
+		apiPullRequest.Head.RepoID = pr.BaseRepoID
+		apiPullRequest.Head.Repository = apiPullRequest.Base.Repository
+		apiPullRequest.Head.Name = ""
+	}
+
+	if pr.HeadRepo != nil && pr.Flow == models.PullRequestFlowGithub {
 		apiPullRequest.Head.RepoID = pr.HeadRepo.ID
 		apiPullRequest.Head.Repository = ToRepo(pr.HeadRepo, models.AccessModeNone)
 
diff --git a/modules/git/git.go b/modules/git/git.go
index ef6ec0c2bf..7ab11736e8 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -37,6 +37,9 @@ var (
 
 	// will be checked on Init
 	goVersionLessThan115 = true
+
+	// SupportProcReceive version >= 2.29.0
+	SupportProcReceive bool
 )
 
 // LocalVersion returns current Git version from shell.
@@ -183,6 +186,19 @@ func Init(ctx context.Context) error {
 		}
 	}
 
+	if CheckGitVersionAtLeast("2.29") == nil {
+		// set support for AGit flow
+		if err := checkAndAddConfig("receive.procReceiveRefs", "refs/for"); err != nil {
+			return err
+		}
+		SupportProcReceive = true
+	} else {
+		if err := checkAndRemoveConfig("receive.procReceiveRefs", "refs/for"); err != nil {
+			return err
+		}
+		SupportProcReceive = false
+	}
+
 	if runtime.GOOS == "windows" {
 		if err := checkAndSetConfig("core.longpaths", "true", true); err != nil {
 			return err
@@ -232,6 +248,51 @@ func checkAndSetConfig(key, defaultValue string, forceToDefault bool) error {
 	return nil
 }
 
+func checkAndAddConfig(key, value string) error {
+	_, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value)
+	if err != nil {
+		perr, ok := err.(*process.Error)
+		if !ok {
+			return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr)
+		}
+		eerr, ok := perr.Err.(*exec.ExitError)
+		if !ok || eerr.ExitCode() != 1 {
+			return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr)
+		}
+		if eerr.ExitCode() == 1 {
+			if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--add", key, value); err != nil {
+				return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr)
+			}
+			return nil
+		}
+	}
+
+	return nil
+}
+
+func checkAndRemoveConfig(key, value string) error {
+	_, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value)
+	if err != nil {
+		perr, ok := err.(*process.Error)
+		if !ok {
+			return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr)
+		}
+		eerr, ok := perr.Err.(*exec.ExitError)
+		if !ok || eerr.ExitCode() != 1 {
+			return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr)
+		}
+		if eerr.ExitCode() == 1 {
+			return nil
+		}
+	}
+
+	if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--unset-all", key, value); err != nil {
+		return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr)
+	}
+
+	return nil
+}
+
 // Fsck verifies the connectivity and validity of the objects in the database
 func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args ...string) error {
 	// Make sure timeout makes sense.
diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go
index 58781eb1c7..7c30b1fb20 100644
--- a/modules/git/repo_branch.go
+++ b/modules/git/repo_branch.go
@@ -13,6 +13,14 @@ import (
 // BranchPrefix base dir of the branch information file store on git
 const BranchPrefix = "refs/heads/"
 
+// AGit Flow
+
+// PullRequestPrefix sepcial ref to create a pull request: refs/for/<targe-branch>/<topic-branch>
+// or refs/for/<targe-branch> -o topic='<topic-branch>'
+const PullRequestPrefix = "refs/for/"
+
+// TODO: /refs/for-review for suggest change interface
+
 // IsReferenceExist returns true if given reference exists in the repository.
 func IsReferenceExist(repoPath, name string) bool {
 	_, err := NewCommand("show-ref", "--verify", "--", name).RunInDir(repoPath)
diff --git a/modules/private/hook.go b/modules/private/hook.go
index 9596f5f4da..4d0b5d22bf 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -6,6 +6,7 @@ package private
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -56,6 +57,7 @@ type HookOptions struct {
 	GitPushOptions                  GitPushOptions
 	PullRequestID                   int64
 	IsDeployKey                     bool
+	IsWiki                          bool
 }
 
 // SSHLogOption ssh log options
@@ -79,6 +81,23 @@ type HookPostReceiveBranchResult struct {
 	URL     string
 }
 
+// HockProcReceiveResult represents an individual result from ProcReceive
+type HockProcReceiveResult struct {
+	Results []HockProcReceiveRefResult
+	Err     string
+}
+
+// HockProcReceiveRefResult represents an individual result from ProcReceive
+type HockProcReceiveRefResult struct {
+	OldOID       string
+	NewOID       string
+	Ref          string
+	OriginalRef  string
+	IsForcePush  bool
+	IsNotMatched bool
+	Err          string
+}
+
 // HookPreReceive check whether the provided commits are allowed
 func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (int, string) {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s",
@@ -130,6 +149,33 @@ func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookO
 	return res, ""
 }
 
+// HookProcReceive proc-receive hook
+func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HockProcReceiveResult, error) {
+	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s",
+		url.PathEscape(ownerName),
+		url.PathEscape(repoName),
+	)
+
+	req := newInternalRequest(ctx, reqURL, "POST")
+	req = req.Header("Content-Type", "application/json")
+	req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second)
+	jsonBytes, _ := json.Marshal(opts)
+	req.Body(jsonBytes)
+	resp, err := req.Response()
+	if err != nil {
+		return nil, fmt.Errorf("Unable to contact gitea: %v", err.Error())
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, errors.New(decodeJSONError(resp).Err)
+	}
+	res := &HockProcReceiveResult{}
+	_ = json.NewDecoder(resp.Body).Decode(res)
+
+	return res, nil
+}
+
 // SetDefaultBranch will set the default branch to the provided branch for the provided repository
 func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) error {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s",
diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go
index ed6036851e..8b4e7d6302 100644
--- a/modules/repository/hooks.go
+++ b/modules/repository/hooks.go
@@ -12,6 +12,7 @@ import (
 	"path/filepath"
 
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -75,6 +76,14 @@ done
 		fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s update $1 $2 $3\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)),
 		fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s post-receive\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)),
 	}
+
+	if git.SupportProcReceive {
+		hookNames = append(hookNames, "proc-receive")
+		hookTpls = append(hookTpls,
+			fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s proc-receive\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)))
+		giteaHookTpls = append(giteaHookTpls, "")
+	}
+
 	return
 }
 
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 66bcabfd38..de166d7ecb 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -310,7 +310,7 @@ func CreatePullRequest(ctx *context.APIContext) {
 	defer headGitRepo.Close()
 
 	// Check if another PR exists with the same targets
-	existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch)
+	existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestFlowGithub)
 	if err != nil {
 		if !models.IsErrPullRequestNotExist(err) {
 			ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err)
diff --git a/routers/private/hook.go b/routers/private/hook.go
index 9f5579b6ae..4bed86f38a 100644
--- a/routers/private/hook.go
+++ b/routers/private/hook.go
@@ -23,6 +23,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/agit"
 	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
@@ -155,6 +156,56 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
 			private.GitQuarantinePath+"="+opts.GitQuarantinePath)
 	}
 
+	if git.SupportProcReceive {
+		pusher, err := models.GetUserByID(opts.UserID)
+		if err != nil {
+			log.Error("models.GetUserByID:%v", err)
+			ctx.Error(http.StatusInternalServerError, "")
+			return
+		}
+
+		perm, err := models.GetUserRepoPermission(repo, pusher)
+		if err != nil {
+			log.Error("models.GetUserRepoPermission:%v", err)
+			ctx.Error(http.StatusInternalServerError, "")
+			return
+		}
+
+		canCreatePullRequest := perm.CanRead(models.UnitTypePullRequests)
+
+		for _, refFullName := range opts.RefFullNames {
+			// if user want update other refs (branch or tag),
+			// should check code write permission because
+			// this check was delayed.
+			if !strings.HasPrefix(refFullName, git.PullRequestPrefix) {
+				if !perm.CanWrite(models.UnitTypeCode) {
+					ctx.JSON(http.StatusForbidden, map[string]interface{}{
+						"err": "User permission denied.",
+					})
+					return
+				}
+
+				break
+			} else if repo.IsEmpty {
+				ctx.JSON(http.StatusForbidden, map[string]interface{}{
+					"err": "Can't create pull request for an empty repository.",
+				})
+				return
+			} else if !canCreatePullRequest {
+				ctx.JSON(http.StatusForbidden, map[string]interface{}{
+					"err": "User permission denied.",
+				})
+				return
+			} else if opts.IsWiki {
+				// TODO: maybe can do it ...
+				ctx.JSON(http.StatusForbidden, map[string]interface{}{
+					"err": "not support send pull request to wiki.",
+				})
+				return
+			}
+		}
+	}
+
 	protectedTags, err := repo.GetProtectedTags()
 	if err != nil {
 		log.Error("Unable to get protected tags for %-v Error: %v", repo, err)
@@ -392,11 +443,35 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
 				})
 				return
 			}
+		} else if git.SupportProcReceive && strings.HasPrefix(refFullName, git.PullRequestPrefix) {
+			baseBranchName := opts.RefFullNames[i][len(git.PullRequestPrefix):]
+
+			baseBranchExist := false
+			if gitRepo.IsBranchExist(baseBranchName) {
+				baseBranchExist = true
+			}
+
+			if !baseBranchExist {
+				for p, v := range baseBranchName {
+					if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
+						baseBranchExist = true
+						break
+					}
+				}
+			}
+
+			if !baseBranchExist {
+				ctx.JSON(http.StatusForbidden, private.Response{
+					Err: fmt.Sprintf("Unexpected ref: %s", refFullName),
+				})
+				return
+			}
 		} else {
 			log.Error("Unexpected ref: %s", refFullName)
 			ctx.JSON(http.StatusInternalServerError, private.Response{
 				Err: fmt.Sprintf("Unexpected ref: %s", refFullName),
 			})
+			return
 		}
 	}
 
@@ -537,7 +612,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 				continue
 			}
 
-			pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch)
+			pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, models.PullRequestFlowGithub)
 			if err != nil && !models.IsErrPullRequestNotExist(err) {
 				log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
 				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
@@ -574,6 +649,30 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 	})
 }
 
+// HookProcReceive proc-receive hook
+func HookProcReceive(ctx *gitea_context.PrivateContext) {
+	opts := web.GetForm(ctx).(*private.HookOptions)
+	if !git.SupportProcReceive {
+		ctx.Status(http.StatusNotFound)
+		return
+	}
+
+	cancel := loadRepositoryAndGitRepoByParams(ctx)
+	if ctx.Written() {
+		return
+	}
+	defer cancel()
+
+	results := agit.ProcRecive(ctx, opts)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.JSON(http.StatusOK, private.HockProcReceiveResult{
+		Results: results,
+	})
+}
+
 // SetDefaultBranch updates the default branch
 func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
 	ownerName := ctx.Params(":owner")
@@ -618,3 +717,44 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
 	}
 	ctx.PlainText(http.StatusOK, []byte("success"))
 }
+
+func loadRepositoryAndGitRepoByParams(ctx *gitea_context.PrivateContext) context.CancelFunc {
+	ownerName := ctx.Params(":owner")
+	repoName := ctx.Params(":repo")
+
+	repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
+	if err != nil {
+		log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
+		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+			"Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
+		})
+		return nil
+	}
+	if repo.OwnerName == "" {
+		repo.OwnerName = ownerName
+	}
+
+	gitRepo, err := git.OpenRepository(repo.RepoPath())
+	if err != nil {
+		log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
+		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+			"Err": fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
+		})
+		return nil
+	}
+
+	ctx.Repo = &gitea_context.Repository{
+		Repository: repo,
+		GitRepo:    gitRepo,
+	}
+
+	// We opened it, we should close it
+	cancel := func() {
+		// If it's been set to nil then assume someone else has closed it.
+		if ctx.Repo.GitRepo != nil {
+			ctx.Repo.GitRepo.Close()
+		}
+	}
+
+	return cancel
+}
diff --git a/routers/private/internal.go b/routers/private/internal.go
index 9202e67218..155e8c036b 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -58,6 +58,7 @@ func Routes() *web.Route {
 	r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog)
 	r.Post("/hook/pre-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPreReceive)
 	r.Post("/hook/post-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPostReceive)
+	r.Post("/hook/proc-receive/{owner}/{repo}", bind(private.HookOptions{}), HookProcReceive)
 	r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", SetDefaultBranch)
 	r.Get("/serv/none/{keyid}", ServNoCommand)
 	r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand)
diff --git a/routers/private/serv.go b/routers/private/serv.go
index 6e39790eb5..f80d16a7f8 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -12,6 +12,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
@@ -288,6 +289,11 @@ func ServCommand(ctx *context.PrivateContext) {
 				return
 			}
 		} else {
+			// Because of special ref "refs/for" .. , need delay write permission check
+			if git.SupportProcReceive && unitType == models.UnitTypeCode {
+				mode = models.AccessModeRead
+			}
+
 			perm, err := models.GetUserRepoPermission(repo, user)
 			if err != nil {
 				log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err)
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index e66aa614cb..f405362cf5 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -653,7 +653,7 @@ func CompareDiff(ctx *context.Context) {
 	ctx.Data["HeadTags"] = headTags
 
 	if ctx.Data["PageIsComparePull"] == true {
-		pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch)
+		pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestFlowGithub)
 		if err != nil {
 			if !models.IsErrPullRequestNotExist(err) {
 				ctx.ServerError("GetUnmergedPullRequest", err)
diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go
index 3390f026a0..7947776f16 100644
--- a/routers/web/repo/http.go
+++ b/routers/web/repo/http.go
@@ -198,6 +198,11 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
 				return
 			}
 
+			// Because of special ref "refs/for" .. , need delay write permission check
+			if git.SupportProcReceive {
+				accessMode = models.AccessModeRead
+			}
+
 			if !perm.CanAccess(accessMode, unitType) {
 				ctx.HandleText(http.StatusForbidden, "User permission denied")
 				return
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 9639ea8201..8518917828 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -2047,7 +2047,7 @@ func NewComment(ctx *context.Context) {
 			if form.Status == "reopen" && issue.IsPull {
 				pull := issue.PullRequest
 				var err error
-				pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch)
+				pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
 				if err != nil {
 					if !models.IsErrPullRequestNotExist(err) {
 						ctx.ServerError("GetUnmergedPullRequest", err)
@@ -2057,6 +2057,7 @@ func NewComment(ctx *context.Context) {
 
 				// Regenerate patch and test conflict.
 				if pr == nil {
+					issue.PullRequest.HeadCommitID = ""
 					pull_service.AddToTaskQueue(issue.PullRequest)
 				}
 			}
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 703bbd837a..565c645801 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -427,10 +427,18 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
 		}
 		defer headGitRepo.Close()
 
-		headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
+		if pull.Flow == models.PullRequestFlowGithub {
+			headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
+		} else {
+			headBranchExist = git.IsReferenceExist(baseGitRepo.Path, pull.GetGitRefName())
+		}
 
 		if headBranchExist {
-			headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
+			if pull.Flow != models.PullRequestFlowGithub {
+				headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName())
+			} else {
+				headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
+			}
 			if err != nil {
 				ctx.ServerError("GetBranchCommitID", err)
 				return nil
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 682f920578..bec523509c 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -23,6 +23,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/agit"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/unknwon/i18n"
@@ -76,6 +77,14 @@ func HandleUsernameChange(ctx *context.Context, user *models.User, newName strin
 			return err
 		}
 	}
+
+	// update all agit flow pull request header
+	err := agit.UserNameChanged(user, newName)
+	if err != nil {
+		ctx.ServerError("agit.UserNameChanged", err)
+		return err
+	}
+
 	log.Trace("User name changed: %s -> %s", user.Name, newName)
 	return nil
 }
diff --git a/routers/web/web.go b/routers/web/web.go
index 26e6c31a47..a47fd518ac 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -12,6 +12,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/metrics"
@@ -146,6 +147,21 @@ func Routes() *web.Route {
 		routes.Get("/metrics", append(common, Metrics)...)
 	}
 
+	routes.Get("/ssh_info", func(rw http.ResponseWriter, req *http.Request) {
+		if !git.SupportProcReceive {
+			rw.WriteHeader(404)
+			return
+		}
+		rw.Header().Set("content-type", "text/json;charset=UTF-8")
+		_, err := rw.Write([]byte(`{"type":"gitea","version":1}`))
+		if err != nil {
+			log.Error("fail to write result: err: %v", err)
+			rw.WriteHeader(500)
+			return
+		}
+		rw.WriteHeader(200)
+	})
+
 	// Removed: toolbox.Toolboxer middleware will provide debug information which seems unnecessary
 	common = append(common, context.Contexter())
 
diff --git a/services/agit/agit.go b/services/agit/agit.go
new file mode 100644
index 0000000000..a89c255fe7
--- /dev/null
+++ b/services/agit/agit.go
@@ -0,0 +1,288 @@
+// Copyright 2021 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 agit
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/notification"
+	"code.gitea.io/gitea/modules/private"
+	pull_service "code.gitea.io/gitea/services/pull"
+)
+
+// ProcRecive handle proc receive work
+func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []private.HockProcReceiveRefResult {
+	// TODO: Add more options?
+	var (
+		topicBranch string
+		title       string
+		description string
+		forcePush   bool
+	)
+
+	results := make([]private.HockProcReceiveRefResult, 0, len(opts.OldCommitIDs))
+	repo := ctx.Repo.Repository
+	gitRepo := ctx.Repo.GitRepo
+	ownerName := ctx.Repo.Repository.OwnerName
+	repoName := ctx.Repo.Repository.Name
+
+	topicBranch = opts.GitPushOptions["topic"]
+	_, forcePush = opts.GitPushOptions["force-push"]
+
+	for i := range opts.OldCommitIDs {
+		if opts.NewCommitIDs[i] == git.EmptySHA {
+			results = append(results, private.HockProcReceiveRefResult{
+				OriginalRef: opts.RefFullNames[i],
+				OldOID:      opts.OldCommitIDs[i],
+				NewOID:      opts.NewCommitIDs[i],
+				Err:         "Can't delete not exist branch",
+			})
+			continue
+		}
+
+		if !strings.HasPrefix(opts.RefFullNames[i], git.PullRequestPrefix) {
+			results = append(results, private.HockProcReceiveRefResult{
+				IsNotMatched: true,
+				OriginalRef:  opts.RefFullNames[i],
+			})
+			continue
+		}
+
+		baseBranchName := opts.RefFullNames[i][len(git.PullRequestPrefix):]
+		curentTopicBranch := ""
+		if !gitRepo.IsBranchExist(baseBranchName) {
+			// try match refs/for/<target-branch>/<topic-branch>
+			for p, v := range baseBranchName {
+				if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
+					curentTopicBranch = baseBranchName[p+1:]
+					baseBranchName = baseBranchName[:p]
+					break
+				}
+			}
+		}
+
+		if len(topicBranch) == 0 && len(curentTopicBranch) == 0 {
+			results = append(results, private.HockProcReceiveRefResult{
+				OriginalRef: opts.RefFullNames[i],
+				OldOID:      opts.OldCommitIDs[i],
+				NewOID:      opts.NewCommitIDs[i],
+				Err:         "topic-branch is not set",
+			})
+			continue
+		}
+
+		headBranch := ""
+		userName := strings.ToLower(opts.UserName)
+
+		if len(curentTopicBranch) == 0 {
+			curentTopicBranch = topicBranch
+		}
+
+		// because different user maybe want to use same topic,
+		// So it's better to make sure the topic branch name
+		// has user name prefix
+		if !strings.HasPrefix(curentTopicBranch, userName+"/") {
+			headBranch = userName + "/" + curentTopicBranch
+		} else {
+			headBranch = curentTopicBranch
+		}
+
+		pr, err := models.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, models.PullRequestFlowAGit)
+		if err != nil {
+			if !models.IsErrPullRequestNotExist(err) {
+				log.Error("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err)
+				ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+					"Err": fmt.Sprintf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err),
+				})
+				return nil
+			}
+
+			// create a new pull request
+			if len(title) == 0 {
+				has := false
+				title, has = opts.GitPushOptions["title"]
+				if !has || len(title) == 0 {
+					commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i])
+					if err != nil {
+						log.Error("Failed to get commit %s in repository: %s/%s Error: %v", opts.NewCommitIDs[i], ownerName, repoName, err)
+						ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+							"Err": fmt.Sprintf("Failed to get commit %s in repository: %s/%s Error: %v", opts.NewCommitIDs[i], ownerName, repoName, err),
+						})
+						return nil
+					}
+					title = strings.Split(commit.CommitMessage, "\n")[0]
+				}
+				description = opts.GitPushOptions["description"]
+			}
+
+			pusher, err := models.GetUserByID(opts.UserID)
+			if err != nil {
+				log.Error("Failed to get user. Error: %v", err)
+				ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+					"Err": fmt.Sprintf("Failed to get user. Error: %v", err),
+				})
+				return nil
+			}
+
+			prIssue := &models.Issue{
+				RepoID:   repo.ID,
+				Title:    title,
+				PosterID: pusher.ID,
+				Poster:   pusher,
+				IsPull:   true,
+				Content:  description,
+			}
+
+			pr := &models.PullRequest{
+				HeadRepoID:   repo.ID,
+				BaseRepoID:   repo.ID,
+				HeadBranch:   headBranch,
+				HeadCommitID: opts.NewCommitIDs[i],
+				BaseBranch:   baseBranchName,
+				HeadRepo:     repo,
+				BaseRepo:     repo,
+				MergeBase:    "",
+				Type:         models.PullRequestGitea,
+				Flow:         models.PullRequestFlowAGit,
+			}
+
+			if err := pull_service.NewPullRequest(repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil {
+				if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+					ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
+					return nil
+				}
+				ctx.Error(http.StatusInternalServerError, "NewPullRequest", err.Error())
+				return nil
+			}
+
+			log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
+
+			results = append(results, private.HockProcReceiveRefResult{
+				Ref:         pr.GetGitRefName(),
+				OriginalRef: opts.RefFullNames[i],
+				OldOID:      git.EmptySHA,
+				NewOID:      opts.NewCommitIDs[i],
+			})
+			continue
+		}
+
+		// update exist pull request
+		if err := pr.LoadBaseRepo(); err != nil {
+			log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
+			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+				"Err": fmt.Sprintf("Unable to load base repository for PR[%d] Error: %v", pr.ID, err),
+			})
+			return nil
+		}
+
+		oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
+		if err != nil {
+			log.Error("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err)
+			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+				"Err": fmt.Sprintf("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err),
+			})
+			return nil
+		}
+
+		if oldCommitID == opts.NewCommitIDs[i] {
+			results = append(results, private.HockProcReceiveRefResult{
+				OriginalRef: opts.RefFullNames[i],
+				OldOID:      opts.OldCommitIDs[i],
+				NewOID:      opts.NewCommitIDs[i],
+				Err:         "new commit is same with old commit",
+			})
+			continue
+		}
+
+		if !forcePush {
+			output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+opts.NewCommitIDs[i]).RunInDirWithEnv(repo.RepoPath(), os.Environ())
+			if err != nil {
+				log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, opts.NewCommitIDs[i], repo, err)
+				ctx.JSON(http.StatusInternalServerError, private.Response{
+					Err: fmt.Sprintf("Fail to detect force push: %v", err),
+				})
+				return nil
+			} else if len(output) > 0 {
+				results = append(results, private.HockProcReceiveRefResult{
+					OriginalRef: oldCommitID,
+					OldOID:      opts.OldCommitIDs[i],
+					NewOID:      opts.NewCommitIDs[i],
+					Err:         "request `force-push` push option",
+				})
+				continue
+			}
+		}
+
+		pr.HeadCommitID = opts.NewCommitIDs[i]
+		if err = pull_service.UpdateRef(pr); err != nil {
+			log.Error("Failed to update pull ref. Error: %v", err)
+			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+				"Err": fmt.Sprintf("Failed to update pull ref. Error: %v", err),
+			})
+			return nil
+		}
+
+		pull_service.AddToTaskQueue(pr)
+		pusher, err := models.GetUserByID(opts.UserID)
+		if err != nil {
+			log.Error("Failed to get user. Error: %v", err)
+			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+				"Err": fmt.Sprintf("Failed to get user. Error: %v", err),
+			})
+			return nil
+		}
+		err = pr.LoadIssue()
+		if err != nil {
+			log.Error("Failed to load pull issue. Error: %v", err)
+			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+				"Err": fmt.Sprintf("Failed to load pull issue. Error: %v", err),
+			})
+			return nil
+		}
+		comment, err := models.CreatePushPullComment(pusher, pr, oldCommitID, opts.NewCommitIDs[i])
+		if err == nil && comment != nil {
+			notification.NotifyPullRequestPushCommits(pusher, pr, comment)
+		}
+		notification.NotifyPullRequestSynchronized(pusher, pr)
+		isForcePush := comment != nil && comment.IsForcePush
+
+		results = append(results, private.HockProcReceiveRefResult{
+			OldOID:      oldCommitID,
+			NewOID:      opts.NewCommitIDs[i],
+			Ref:         pr.GetGitRefName(),
+			OriginalRef: opts.RefFullNames[i],
+			IsForcePush: isForcePush,
+		})
+	}
+
+	return results
+}
+
+// UserNameChanged hanle user name change for agit flow pull
+func UserNameChanged(user *models.User, newName string) error {
+	pulls, err := models.GetAllUnmergedAgitPullRequestByPoster(user.ID)
+	if err != nil {
+		return err
+	}
+
+	newName = strings.ToLower(newName)
+
+	for _, pull := range pulls {
+		pull.HeadBranch = strings.TrimPrefix(pull.HeadBranch, user.LowerName+"/")
+		pull.HeadBranch = newName + "/" + pull.HeadBranch
+		if err = pull.UpdateCols("head_branch"); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index b8fb109440..c5c930ee0d 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -108,13 +108,21 @@ func GetPullRequestCommitStatusState(pr *models.PullRequest) (structs.CommitStat
 	}
 	defer headGitRepo.Close()
 
-	if !headGitRepo.IsBranchExist(pr.HeadBranch) {
+	if pr.Flow == models.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
+		return "", errors.New("Head branch does not exist, can not merge")
+	}
+	if pr.Flow == models.PullRequestFlowAGit && !git.IsReferenceExist(headGitRepo.Path, pr.GetGitRefName()) {
 		return "", errors.New("Head branch does not exist, can not merge")
 	}
 
-	sha, err := headGitRepo.GetBranchCommitID(pr.HeadBranch)
+	var sha string
+	if pr.Flow == models.PullRequestFlowGithub {
+		sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch)
+	} else {
+		sha, err = headGitRepo.GetRefCommitID(pr.GetGitRefName())
+	}
 	if err != nil {
-		return "", errors.Wrap(err, "GetBranchCommitID")
+		return "", err
 	}
 
 	if err := pr.LoadBaseRepo(); err != nil {
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 6c108c224f..b33f641130 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -49,7 +49,12 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6
 	pr.Issue = pull
 	pull.PullRequest = pr
 
-	if err := PushToBaseRepo(pr); err != nil {
+	if pr.Flow == models.PullRequestFlowGithub {
+		err = PushToBaseRepo(pr)
+	} else {
+		err = UpdateRef(pr)
+	}
+	if err != nil {
 		return err
 	}
 
@@ -145,7 +150,7 @@ func ChangeTargetBranch(pr *models.PullRequest, doer *models.User, targetBranch
 	}
 
 	// Check if pull request for the new target branch already exists
-	existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch)
+	existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, models.PullRequestFlowGithub)
 	if existingPr != nil {
 		return models.ErrPullRequestAlreadyExists{
 			ID:         existingPr.ID,
@@ -281,8 +286,12 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy
 
 		for _, pr := range prs {
 			log.Trace("Updating PR[%d]: composing new test task", pr.ID)
-			if err := PushToBaseRepo(pr); err != nil {
-				log.Error("PushToBaseRepo: %v", err)
+			if pr.Flow == models.PullRequestFlowGithub {
+				if err := PushToBaseRepo(pr); err != nil {
+					log.Error("PushToBaseRepo: %v", err)
+					continue
+				}
+			} else {
 				continue
 			}
 
@@ -451,6 +460,22 @@ func pushToBaseRepoHelper(pr *models.PullRequest, prefixHeadBranch string) (err
 	return nil
 }
 
+// UpdateRef update refs/pull/id/head directly for agit flow pull request
+func UpdateRef(pr *models.PullRequest) (err error) {
+	log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName())
+	if err := pr.LoadBaseRepo(); err != nil {
+		log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
+		return err
+	}
+
+	_, err = git.NewCommand("update-ref", pr.GetGitRefName(), pr.HeadCommitID).RunInDir(pr.BaseRepo.RepoPath())
+	if err != nil {
+		log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err)
+	}
+
+	return err
+}
+
 type errlist []error
 
 func (errs errlist) Error() string {
@@ -562,7 +587,17 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string {
 	}
 	defer gitRepo.Close()
 
-	headCommit, err := gitRepo.GetBranchCommit(pr.HeadBranch)
+	var headCommit *git.Commit
+	if pr.Flow == models.PullRequestFlowGithub {
+		headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch)
+	} else {
+		pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
+		if err != nil {
+			log.Error("Unable to get head commit: %s Error: %v", pr.GetGitRefName(), err)
+			return ""
+		}
+		headCommit, err = gitRepo.GetCommit(pr.HeadCommitID)
+	}
 	if err != nil {
 		log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err)
 		return ""
@@ -781,9 +816,20 @@ func IsHeadEqualWithBranch(pr *models.PullRequest, branchName string) (bool, err
 	}
 	defer headGitRepo.Close()
 
-	headCommit, err := headGitRepo.GetBranchCommit(pr.HeadBranch)
-	if err != nil {
-		return false, err
+	var headCommit *git.Commit
+	if pr.Flow == models.PullRequestFlowGithub {
+		headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch)
+		if err != nil {
+			return false, err
+		}
+	} else {
+		pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitRefName())
+		if err != nil {
+			return false, err
+		}
+		if headCommit, err = baseGitRepo.GetCommit(pr.HeadCommitID); err != nil {
+			return false, err
+		}
 	}
 	return baseCommit.HasPreviousCommit(headCommit.ID)
 }
diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go
index 19b488790a..54d09c8158 100644
--- a/services/pull/temp_repo.go
+++ b/services/pull/temp_repo.go
@@ -140,7 +140,15 @@ func createTemporaryRepo(pr *models.PullRequest) (string, error) {
 
 	trackingBranch := "tracking"
 	// Fetch head branch
-	if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, git.BranchPrefix+pr.HeadBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
+	var headBranch string
+	if pr.Flow == models.PullRequestFlowGithub {
+		headBranch = git.BranchPrefix + pr.HeadBranch
+	} else if len(pr.HeadCommitID) == 40 { // for not created pull request
+		headBranch = pr.HeadCommitID
+	} else {
+		headBranch = pr.GetGitRefName()
+	}
+	if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, headBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
 		if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
 			log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
 		}
@@ -150,7 +158,7 @@ func createTemporaryRepo(pr *models.PullRequest) (string, error) {
 			}
 		}
 		log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, outbuf.String(), errbuf.String())
-		return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, err, outbuf.String(), errbuf.String())
+		return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), headBranch, err, outbuf.String(), errbuf.String())
 	}
 	outbuf.Reset()
 	errbuf.Reset()
diff --git a/services/pull/update.go b/services/pull/update.go
index f35e47cbf8..c2c13845e3 100644
--- a/services/pull/update.go
+++ b/services/pull/update.go
@@ -22,6 +22,11 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error {
 		BaseBranch: pull.HeadBranch,
 	}
 
+	if pull.Flow == models.PullRequestFlowAGit {
+		// TODO: Not support update agit flow pull request's head branch
+		return fmt.Errorf("Not support update agit flow pull request's head branch")
+	}
+
 	if err := pr.LoadHeadRepo(); err != nil {
 		log.Error("LoadHeadRepo: %v", err)
 		return fmt.Errorf("LoadHeadRepo: %v", err)
@@ -48,6 +53,10 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error {
 
 // IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
 func IsUserAllowedToUpdate(pull *models.PullRequest, user *models.User) (bool, error) {
+	if pull.Flow == models.PullRequestFlowAGit {
+		return false, nil
+	}
+
 	if user == nil {
 		return false, nil
 	}
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index fcb3597ae8..15ce63b4d3 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -461,13 +461,17 @@
 									{{end}}
 								</div>
 							</div>
-							<div class="instruct-toggle ml-3">{{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}}</div>
+							<div class="instruct-toggle ml-3"> {{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}} </div>
 							<div class="instruct-content" style="display:none">
 								<div class="ui divider"></div>
 								<div><h3 class="di">{{$.i18n.Tr "step1"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step1_desc"}}</div>
 								<div class="ui secondary segment">
-									<div>git checkout -b {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{.Issue.PullRequest.HeadBranch}} {{.Issue.PullRequest.BaseBranch}}</div>
-									<div>git pull {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.HTMLURL}}{{else}}origin{{end}} {{.Issue.PullRequest.HeadBranch}}</div>
+									{{if eq .Issue.PullRequest.Flow 0}}
+										<div>git checkout -b {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{.Issue.PullRequest.HeadBranch}} {{.Issue.PullRequest.BaseBranch}}</div>
+										<div>git pull {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.HTMLURL}}{{else}}origin{{end}} {{.Issue.PullRequest.HeadBranch}}</div>
+									{{else}}
+										<div>git fetch origin {{.Issue.PullRequest.GetGitRefName}}:{{.Issue.PullRequest.HeadBranch}}</div>
+									{{end}}
 								</div>
 								<div><h3 class="di">{{$.i18n.Tr "step2"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step2_desc"}}</div>
 								<div class="ui secondary segment">