diff --git a/integrations/api_pull_commits_test.go b/integrations/api_pull_commits_test.go
new file mode 100644
index 0000000000..30682d9c14
--- /dev/null
+++ b/integrations/api_pull_commits_test.go
@@ -0,0 +1,37 @@
+// 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 integrations
+
+import (
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	api "code.gitea.io/gitea/modules/structs"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAPIPullCommits(t *testing.T) {
+	defer prepareTestEnv(t)()
+	pullIssue := models.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
+	assert.NoError(t, pullIssue.LoadIssue())
+	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: pullIssue.HeadRepoID}).(*models.Repository)
+
+	session := loginUser(t, "user2")
+	req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/commits", repo.OwnerName, repo.Name, pullIssue.Index)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	var commits []*api.Commit
+	DecodeJSON(t, resp, &commits)
+
+	if !assert.Len(t, commits, 2) {
+		return
+	}
+
+	assert.Equal(t, "5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commits[0].SHA)
+	assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", commits[1].SHA)
+}
+
+// TODO add tests for already merged PR and closed PR
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index c6b4ff04de..b6913ea1bc 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -905,6 +905,7 @@ func Routes() *web.Route {
 						m.Get(".diff", repo.DownloadPullDiff)
 						m.Get(".patch", repo.DownloadPullPatch)
 						m.Post("/update", reqToken(), repo.UpdatePullRequest)
+						m.Get("/commits", repo.GetPullRequestCommits)
 						m.Combo("/merge").Get(repo.IsPullRequestMerged).
 							Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
 						m.Group("/reviews", func() {
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index eff998ee99..0c09a9a86b 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -6,7 +6,9 @@ package repo
 
 import (
 	"fmt"
+	"math"
 	"net/http"
+	"strconv"
 	"strings"
 	"time"
 
@@ -1101,3 +1103,122 @@ func UpdatePullRequest(ctx *context.APIContext) {
 
 	ctx.Status(http.StatusOK)
 }
+
+// GetPullRequestCommits gets all commits associated with a given PR
+func GetPullRequestCommits(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits
+	// ---
+	// summary: Get commits for a pull request
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: index
+	//   in: path
+	//   description: index of the pull request to get
+	//   type: integer
+	//   format: int64
+	//   required: true
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/CommitList"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrPullRequestNotExist(err) {
+			ctx.NotFound()
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
+		}
+		return
+	}
+
+	if err := pr.LoadBaseRepo(); err != nil {
+		ctx.InternalServerError(err)
+		return
+	}
+
+	var prInfo *git.CompareInfo
+	baseGitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
+	if err != nil {
+		ctx.ServerError("OpenRepository", err)
+		return
+	}
+	defer baseGitRepo.Close()
+	if pr.HasMerged {
+		prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName())
+	} else {
+		prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName())
+	}
+	if err != nil {
+		ctx.ServerError("GetCompareInfo", err)
+		return
+	}
+	commits := prInfo.Commits
+
+	listOptions := utils.GetListOptions(ctx)
+
+	totalNumberOfCommits := commits.Len()
+	totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize)))
+
+	userCache := make(map[string]*models.User)
+
+	start, end := listOptions.GetStartEnd()
+
+	if end > totalNumberOfCommits {
+		end = totalNumberOfCommits
+	}
+
+	apiCommits := make([]*api.Commit, end-start)
+
+	i := 0
+	addedCommitsCount := 0
+	for commitPointer := commits.Front(); commitPointer != nil; commitPointer = commitPointer.Next() {
+		if i < start {
+			i++
+			continue
+		}
+		if i >= end {
+			break
+		}
+
+		commit := commitPointer.Value.(*git.Commit)
+
+		// Create json struct
+		apiCommits[addedCommitsCount], err = convert.ToCommit(ctx.Repo.Repository, commit, userCache)
+		addedCommitsCount++
+		if err != nil {
+			ctx.ServerError("toCommit", err)
+			return
+		}
+		i++
+	}
+
+	ctx.SetLinkHeader(int(totalNumberOfCommits), listOptions.PageSize)
+
+	ctx.Header().Set("X-Page", strconv.Itoa(listOptions.Page))
+	ctx.Header().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
+	ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", totalNumberOfCommits))
+	ctx.Header().Set("X-PageCount", strconv.Itoa(totalNumberOfPages))
+	ctx.Header().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages))
+	ctx.JSON(http.StatusOK, &apiCommits)
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index dfd08bcc68..a2e449228e 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -7333,6 +7333,62 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/pulls/{index}/commits": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get commits for a pull request",
+        "operationId": "repoGetPullRequestCommits",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "format": "int64",
+            "description": "index of the pull request to get",
+            "name": "index",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/CommitList"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/pulls/{index}/merge": {
       "get": {
         "produces": [