From cd96dee9822c8b744526ba862fd8b5ec0e2c30ff Mon Sep 17 00:00:00 2001
From: Richard Mahn <richmahn@users.noreply.github.com>
Date: Sat, 29 Jun 2019 16:51:10 -0400
Subject: [PATCH] Fixes #7292 - API File Contents bug (#7301)

---
 integrations/api_repo_file_content_test.go    | 117 -----------
 integrations/api_repo_file_create_test.go     |  32 +--
 integrations/api_repo_file_update_test.go     |  38 ++--
 .../api_repo_get_contents_list_test.go        | 156 ++++++++++++++
 integrations/api_repo_get_contents_test.go    | 157 ++++++++++++++
 integrations/repofiles_update_test.go         |  79 +++++---
 modules/git/blob.go                           |  13 ++
 modules/git/repo_object.go                    |  17 ++
 modules/repofiles/content.go                  | 189 ++++++++++++++---
 modules/repofiles/content_test.go             | 154 +++++++++++---
 modules/repofiles/file.go                     |   4 +-
 modules/repofiles/file_test.go                |  35 ++--
 modules/structs/repo_file.go                  |  41 ++--
 routers/api/v1/api.go                         |   3 +-
 routers/api/v1/repo/file.go                   |  53 ++++-
 routers/api/v1/swagger/repo.go                |  15 +-
 templates/swagger/v1_json.tmpl                | 191 ++++++++++++------
 17 files changed, 961 insertions(+), 333 deletions(-)
 delete mode 100644 integrations/api_repo_file_content_test.go
 create mode 100644 integrations/api_repo_get_contents_list_test.go
 create mode 100644 integrations/api_repo_get_contents_test.go

diff --git a/integrations/api_repo_file_content_test.go b/integrations/api_repo_file_content_test.go
deleted file mode 100644
index 7a6025d423e..00000000000
--- a/integrations/api_repo_file_content_test.go
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright 2019 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"
-	"net/url"
-	"path/filepath"
-	"testing"
-
-	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/setting"
-	api "code.gitea.io/gitea/modules/structs"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func getExpectedFileContentResponseForFileContents(branch string) *api.FileContentResponse {
-	treePath := "README.md"
-	sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
-	return &api.FileContentResponse{
-		Name:        filepath.Base(treePath),
-		Path:        treePath,
-		SHA:         sha,
-		Size:        30,
-		URL:         setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
-		HTMLURL:     setting.AppURL + "user2/repo1/blob/" + branch + "/" + treePath,
-		GitURL:      setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
-		DownloadURL: setting.AppURL + "user2/repo1/raw/branch/" + branch + "/" + treePath,
-		Type:        "blob",
-		Links: &api.FileLinksResponse{
-			Self:    setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
-			GitURL:  setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
-			HTMLURL: setting.AppURL + "user2/repo1/blob/" + branch + "/" + treePath,
-		},
-	}
-}
-
-func TestAPIGetFileContents(t *testing.T) {
-	onGiteaRun(t, testAPIGetFileContents)
-}
-
-func testAPIGetFileContents(t *testing.T, u *url.URL) {
-	user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)               // owner of the repo1 & repo16
-	user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)               // owner of the repo3, is an org
-	user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User)               // owner of neither repos
-	repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)   // public repo
-	repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)   // public repo
-	repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
-	treePath := "README.md"
-
-	// Get user2's token
-	session := loginUser(t, user2.Name)
-	token2 := getTokenForLoggedInUser(t, session)
-	session = emptyTestSession(t)
-	// Get user4's token
-	session = loginUser(t, user4.Name)
-	token4 := getTokenForLoggedInUser(t, session)
-	session = emptyTestSession(t)
-
-	// Make a second master branch in repo1
-	repo1.CreateNewBranch(user2, repo1.DefaultBranch, "master2")
-
-	// ref is default branch
-	branch := repo1.DefaultBranch
-	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
-	resp := session.MakeRequest(t, req, http.StatusOK)
-	var fileContentResponse api.FileContentResponse
-	DecodeJSON(t, resp, &fileContentResponse)
-	assert.NotNil(t, fileContentResponse)
-	expectedFileContentResponse := getExpectedFileContentResponseForFileContents(branch)
-	assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
-
-	// No ref
-	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
-	resp = session.MakeRequest(t, req, http.StatusOK)
-	DecodeJSON(t, resp, &fileContentResponse)
-	assert.NotNil(t, fileContentResponse)
-	expectedFileContentResponse = getExpectedFileContentResponseForFileContents(repo1.DefaultBranch)
-	assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
-
-	// ref is master2
-	branch = "master2"
-	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
-	resp = session.MakeRequest(t, req, http.StatusOK)
-	DecodeJSON(t, resp, &fileContentResponse)
-	assert.NotNil(t, fileContentResponse)
-	expectedFileContentResponse = getExpectedFileContentResponseForFileContents("master2")
-	assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
-
-	// Test file contents a file with the wrong branch
-	branch = "badbranch"
-	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
-	resp = session.MakeRequest(t, req, http.StatusInternalServerError)
-	expectedAPIError := context.APIError{
-		Message: "object does not exist [id: " + branch + ", rel_path: ]",
-		URL:     setting.API.SwaggerURL,
-	}
-	var apiError context.APIError
-	DecodeJSON(t, resp, &apiError)
-	assert.Equal(t, expectedAPIError, apiError)
-
-	// Test accessing private branch with user token that does not have access - should fail
-	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
-	session.MakeRequest(t, req, http.StatusNotFound)
-
-	// Test access private branch of owner of token
-	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md?token=%s", user2.Name, repo16.Name, token2)
-	session.MakeRequest(t, req, http.StatusOK)
-
-	// Test access of org user3 private repo file by owner user2
-	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
-	session.MakeRequest(t, req, http.StatusOK)
-}
diff --git a/integrations/api_repo_file_create_test.go b/integrations/api_repo_file_create_test.go
index b00583c1918..42898bf259f 100644
--- a/integrations/api_repo_file_create_test.go
+++ b/integrations/api_repo_file_create_test.go
@@ -44,21 +44,29 @@ func getCreateFileOptions() api.CreateFileOptions {
 
 func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileResponse {
 	sha := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
+	encoding := "base64"
+	content := "VGhpcyBpcyBuZXcgdGV4dA=="
+	selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
+	htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
+	gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
+	downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
 	return &api.FileResponse{
-		Content: &api.FileContentResponse{
+		Content: &api.ContentsResponse{
 			Name:        filepath.Base(treePath),
 			Path:        treePath,
 			SHA:         sha,
 			Size:        16,
-			URL:         setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
-			HTMLURL:     setting.AppURL + "user2/repo1/blob/master/" + treePath,
-			GitURL:      setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
-			DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/" + treePath,
-			Type:        "blob",
+			Type:        "file",
+			Encoding:    &encoding,
+			Content:     &content,
+			URL:         &selfURL,
+			HTMLURL:     &htmlURL,
+			GitURL:      &gitURL,
+			DownloadURL: &downloadURL,
 			Links: &api.FileLinksResponse{
-				Self:    setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
-				GitURL:  setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
-				HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath,
+				Self:    &selfURL,
+				GitURL:  &gitURL,
+				HTMLURL: &htmlURL,
 			},
 		},
 		Commit: &api.FileCommitResponse{
@@ -145,11 +153,11 @@ func TestAPICreateFile(t *testing.T) {
 		var fileResponse api.FileResponse
 		DecodeJSON(t, resp, &fileResponse)
 		expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
-		expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/blob/new_branch/new/file%d.txt", fileID)
+		expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/new/file%d.txt", fileID)
 		expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
 		assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
-		assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
-		assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
+		assert.EqualValues(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
+		assert.EqualValues(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
 		assert.EqualValues(t, createFileOptions.Message+"\n", fileResponse.Commit.Message)
 
 		// Test creating a file without a message
diff --git a/integrations/api_repo_file_update_test.go b/integrations/api_repo_file_update_test.go
index 17fa2adb26f..366eb5e9189 100644
--- a/integrations/api_repo_file_update_test.go
+++ b/integrations/api_repo_file_update_test.go
@@ -47,21 +47,29 @@ func getUpdateFileOptions() *api.UpdateFileOptions {
 
 func getExpectedFileResponseForUpdate(commitID, treePath string) *api.FileResponse {
 	sha := "08bd14b2e2852529157324de9c226b3364e76136"
+	encoding := "base64"
+	content := "VGhpcyBpcyB1cGRhdGVkIHRleHQ="
+	selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
+	htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
+	gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
+	downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
 	return &api.FileResponse{
-		Content: &api.FileContentResponse{
+		Content: &api.ContentsResponse{
 			Name:        filepath.Base(treePath),
 			Path:        treePath,
 			SHA:         sha,
+			Type:        "file",
 			Size:        20,
-			URL:         setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
-			HTMLURL:     setting.AppURL + "user2/repo1/blob/master/" + treePath,
-			GitURL:      setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
-			DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/" + treePath,
-			Type:        "blob",
+			Encoding:    &encoding,
+			Content:     &content,
+			URL:         &selfURL,
+			HTMLURL:     &htmlURL,
+			GitURL:      &gitURL,
+			DownloadURL: &downloadURL,
 			Links: &api.FileLinksResponse{
-				Self:    setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
-				GitURL:  setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
-				HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath,
+				Self:    &selfURL,
+				GitURL:  &gitURL,
+				HTMLURL: &htmlURL,
 			},
 		},
 		Commit: &api.FileCommitResponse{
@@ -150,11 +158,11 @@ func TestAPIUpdateFile(t *testing.T) {
 		var fileResponse api.FileResponse
 		DecodeJSON(t, resp, &fileResponse)
 		expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136"
-		expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/blob/new_branch/update/file%d.txt", fileID)
+		expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/update/file%d.txt", fileID)
 		expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
 		assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
-		assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
-		assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
+		assert.EqualValues(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
+		assert.EqualValues(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
 		assert.EqualValues(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message)
 
 		// Test updating a file and renaming it
@@ -170,11 +178,11 @@ func TestAPIUpdateFile(t *testing.T) {
 		resp = session.MakeRequest(t, req, http.StatusOK)
 		DecodeJSON(t, resp, &fileResponse)
 		expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136"
-		expectedHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/blob/master/rename/update/file%d.txt", fileID)
+		expectedHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/master/rename/update/file%d.txt", fileID)
 		expectedDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
 		assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
-		assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
-		assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
+		assert.EqualValues(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
+		assert.EqualValues(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
 
 		// Test updating a file without a message
 		updateFileOptions = getUpdateFileOptions()
diff --git a/integrations/api_repo_get_contents_list_test.go b/integrations/api_repo_get_contents_list_test.go
new file mode 100644
index 00000000000..f74ceb514a0
--- /dev/null
+++ b/integrations/api_repo_get_contents_list_test.go
@@ -0,0 +1,156 @@
+// Copyright 2019 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"
+	"net/url"
+	"path/filepath"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func getExpectedContentsListResponseForContents(ref, refType string) []*api.ContentsResponse {
+	treePath := "README.md"
+	sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
+	selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=" + ref
+	htmlURL := setting.AppURL + "user2/repo1/src/" + refType + "/" + ref + "/" + treePath
+	gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
+	downloadURL := setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath
+	return []*api.ContentsResponse{
+		{
+			Name:        filepath.Base(treePath),
+			Path:        treePath,
+			SHA:         sha,
+			Type:        "file",
+			Size:        30,
+			URL:         &selfURL,
+			HTMLURL:     &htmlURL,
+			GitURL:      &gitURL,
+			DownloadURL: &downloadURL,
+			Links: &api.FileLinksResponse{
+				Self:    &selfURL,
+				GitURL:  &gitURL,
+				HTMLURL: &htmlURL,
+			},
+		},
+	}
+}
+
+func TestAPIGetContentsList(t *testing.T) {
+	onGiteaRun(t, testAPIGetContentsList)
+}
+
+func testAPIGetContentsList(t *testing.T, u *url.URL) {
+	/*** SETUP ***/
+	user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)               // owner of the repo1 & repo16
+	user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)               // owner of the repo3, is an org
+	user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User)               // owner of neither repos
+	repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)   // public repo
+	repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)   // public repo
+	repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
+	treePath := ""                                                                               // root dir
+
+	// Get user2's token
+	session := loginUser(t, user2.Name)
+	token2 := getTokenForLoggedInUser(t, session)
+	session = emptyTestSession(t)
+	// Get user4's token
+	session = loginUser(t, user4.Name)
+	token4 := getTokenForLoggedInUser(t, session)
+	session = emptyTestSession(t)
+
+	// Make a new branch in repo1
+	newBranch := "test_branch"
+	repo1.CreateNewBranch(user2, repo1.DefaultBranch, newBranch)
+	// Get the commit ID of the default branch
+	gitRepo, _ := git.OpenRepository(repo1.RepoPath())
+	commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
+	// Make a new tag in repo1
+	newTag := "test_tag"
+	gitRepo.CreateTag(newTag, commitID)
+	/*** END SETUP ***/
+
+	// ref is default ref
+	ref := repo1.DefaultBranch
+	refType := "branch"
+	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	var contentsListResponse []*api.ContentsResponse
+	DecodeJSON(t, resp, &contentsListResponse)
+	assert.NotNil(t, contentsListResponse)
+	expectedContentsListResponse := getExpectedContentsListResponseForContents(ref, refType)
+	assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+	// No ref
+	refType = "branch"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &contentsListResponse)
+	assert.NotNil(t, contentsListResponse)
+	expectedContentsListResponse = getExpectedContentsListResponseForContents(repo1.DefaultBranch, refType)
+	assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+	// ref is the branch we created above  in setup
+	ref = newBranch
+	refType = "branch"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &contentsListResponse)
+	assert.NotNil(t, contentsListResponse)
+	expectedContentsListResponse = getExpectedContentsListResponseForContents(ref, refType)
+	assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+	// ref is the new tag we created above in setup
+	ref = newTag
+	refType = "tag"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &contentsListResponse)
+	assert.NotNil(t, contentsListResponse)
+	expectedContentsListResponse = getExpectedContentsListResponseForContents(ref, refType)
+	assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+	// ref is a commit
+	ref = commitID
+	refType = "commit"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &contentsListResponse)
+	assert.NotNil(t, contentsListResponse)
+	expectedContentsListResponse = getExpectedContentsListResponseForContents(ref, refType)
+	assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+	// Test file contents a file with a bad ref
+	ref = "badref"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp = session.MakeRequest(t, req, http.StatusInternalServerError)
+	expectedAPIError := context.APIError{
+		Message: "object does not exist [id: " + ref + ", rel_path: ]",
+		URL:     setting.API.SwaggerURL,
+	}
+	var apiError context.APIError
+	DecodeJSON(t, resp, &apiError)
+	assert.Equal(t, expectedAPIError, apiError)
+
+	// Test accessing private ref with user token that does not have access - should fail
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
+	session.MakeRequest(t, req, http.StatusNotFound)
+
+	// Test access private ref of owner of token
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md?token=%s", user2.Name, repo16.Name, token2)
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// Test access of org user3 private repo file by owner user2
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
+	session.MakeRequest(t, req, http.StatusOK)
+}
diff --git a/integrations/api_repo_get_contents_test.go b/integrations/api_repo_get_contents_test.go
new file mode 100644
index 00000000000..f6a43bc5c63
--- /dev/null
+++ b/integrations/api_repo_get_contents_test.go
@@ -0,0 +1,157 @@
+// Copyright 2019 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"
+	"net/url"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func getExpectedContentsResponseForContents(ref, refType string) *api.ContentsResponse {
+	treePath := "README.md"
+	sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
+	encoding := "base64"
+	content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"
+	selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=" + ref
+	htmlURL := setting.AppURL + "user2/repo1/src/" + refType + "/" + ref + "/" + treePath
+	gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
+	downloadURL := setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath
+	return &api.ContentsResponse{
+		Name:        treePath,
+		Path:        treePath,
+		SHA:         sha,
+		Type:        "file",
+		Size:        30,
+		Encoding:    &encoding,
+		Content:     &content,
+		URL:         &selfURL,
+		HTMLURL:     &htmlURL,
+		GitURL:      &gitURL,
+		DownloadURL: &downloadURL,
+		Links: &api.FileLinksResponse{
+			Self:    &selfURL,
+			GitURL:  &gitURL,
+			HTMLURL: &htmlURL,
+		},
+	}
+}
+
+func TestAPIGetContents(t *testing.T) {
+	onGiteaRun(t, testAPIGetContents)
+}
+
+func testAPIGetContents(t *testing.T, u *url.URL) {
+	/*** SETUP ***/
+	user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)               // owner of the repo1 & repo16
+	user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)               // owner of the repo3, is an org
+	user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User)               // owner of neither repos
+	repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)   // public repo
+	repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)   // public repo
+	repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
+	treePath := "README.md"
+
+	// Get user2's token
+	session := loginUser(t, user2.Name)
+	token2 := getTokenForLoggedInUser(t, session)
+	session = emptyTestSession(t)
+	// Get user4's token
+	session = loginUser(t, user4.Name)
+	token4 := getTokenForLoggedInUser(t, session)
+	session = emptyTestSession(t)
+
+	// Make a new branch in repo1
+	newBranch := "test_branch"
+	repo1.CreateNewBranch(user2, repo1.DefaultBranch, newBranch)
+	// Get the commit ID of the default branch
+	gitRepo, _ := git.OpenRepository(repo1.RepoPath())
+	commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
+	// Make a new tag in repo1
+	newTag := "test_tag"
+	gitRepo.CreateTag(newTag, commitID)
+	/*** END SETUP ***/
+
+	// ref is default ref
+	ref := repo1.DefaultBranch
+	refType := "branch"
+	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	var contentsResponse api.ContentsResponse
+	DecodeJSON(t, resp, &contentsResponse)
+	assert.NotNil(t, contentsResponse)
+	expectedContentsResponse := getExpectedContentsResponseForContents(ref, refType)
+	assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+	// No ref
+	refType = "branch"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &contentsResponse)
+	assert.NotNil(t, contentsResponse)
+	expectedContentsResponse = getExpectedContentsResponseForContents(repo1.DefaultBranch, refType)
+	assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+	// ref is the branch we created above  in setup
+	ref = newBranch
+	refType = "branch"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &contentsResponse)
+	assert.NotNil(t, contentsResponse)
+	expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType)
+	assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+	// ref is the new tag we created above in setup
+	ref = newTag
+	refType = "tag"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &contentsResponse)
+	assert.NotNil(t, contentsResponse)
+	expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType)
+	assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+	// ref is a commit
+	ref = commitID
+	refType = "commit"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &contentsResponse)
+	assert.NotNil(t, contentsResponse)
+	expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType)
+	assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+	// Test file contents a file with a bad ref
+	ref = "badref"
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+	resp = session.MakeRequest(t, req, http.StatusInternalServerError)
+	expectedAPIError := context.APIError{
+		Message: "object does not exist [id: " + ref + ", rel_path: ]",
+		URL:     setting.API.SwaggerURL,
+	}
+	var apiError context.APIError
+	DecodeJSON(t, resp, &apiError)
+	assert.Equal(t, expectedAPIError, apiError)
+
+	// Test accessing private ref with user token that does not have access - should fail
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
+	session.MakeRequest(t, req, http.StatusNotFound)
+
+	// Test access private ref of owner of token
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md?token=%s", user2.Name, repo16.Name, token2)
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// Test access of org user3 private repo file by owner user2
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
+	session.MakeRequest(t, req, http.StatusOK)
+}
diff --git a/integrations/repofiles_update_test.go b/integrations/repofiles_update_test.go
index 02a9bbeb168..a4ce16d8479 100644
--- a/integrations/repofiles_update_test.go
+++ b/integrations/repofiles_update_test.go
@@ -6,6 +6,7 @@ package integrations
 
 import (
 	"net/url"
+	"path/filepath"
 	"testing"
 	"time"
 
@@ -47,21 +48,30 @@ func getUpdateRepoFileOptions(repo *models.Repository) *repofiles.UpdateRepoFile
 }
 
 func getExpectedFileResponseForRepofilesCreate(commitID string) *api.FileResponse {
+	treePath := "new/file.txt"
+	encoding := "base64"
+	content := "VGhpcyBpcyBhIE5FVyBmaWxl"
+	selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
+	htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
+	gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885"
+	downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
 	return &api.FileResponse{
-		Content: &api.FileContentResponse{
-			Name:        "file.txt",
-			Path:        "new/file.txt",
+		Content: &api.ContentsResponse{
+			Name:        filepath.Base(treePath),
+			Path:        treePath,
 			SHA:         "103ff9234cefeee5ec5361d22b49fbb04d385885",
+			Type:        "file",
 			Size:        18,
-			URL:         setting.AppURL + "api/v1/repos/user2/repo1/contents/new/file.txt",
-			HTMLURL:     setting.AppURL + "user2/repo1/blob/master/new/file.txt",
-			GitURL:      setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885",
-			DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/new/file.txt",
-			Type:        "blob",
+			Encoding:    &encoding,
+			Content:     &content,
+			URL:         &selfURL,
+			HTMLURL:     &htmlURL,
+			GitURL:      &gitURL,
+			DownloadURL: &downloadURL,
 			Links: &api.FileLinksResponse{
-				Self:    setting.AppURL + "api/v1/repos/user2/repo1/contents/new/file.txt",
-				GitURL:  setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885",
-				HTMLURL: setting.AppURL + "user2/repo1/blob/master/new/file.txt",
+				Self:    &selfURL,
+				GitURL:  &gitURL,
+				HTMLURL: &htmlURL,
 			},
 		},
 		Commit: &api.FileCommitResponse{
@@ -105,22 +115,30 @@ func getExpectedFileResponseForRepofilesCreate(commitID string) *api.FileRespons
 	}
 }
 
-func getExpectedFileResponseForRepofilesUpdate(commitID string) *api.FileResponse {
+func getExpectedFileResponseForRepofilesUpdate(commitID, filename string) *api.FileResponse {
+	encoding := "base64"
+	content := "VGhpcyBpcyBVUERBVEVEIGNvbnRlbnQgZm9yIHRoZSBSRUFETUUgZmlsZQ=="
+	selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + filename + "?ref=master"
+	htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + filename
+	gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647"
+	downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + filename
 	return &api.FileResponse{
-		Content: &api.FileContentResponse{
-			Name:        "README.md",
-			Path:        "README.md",
+		Content: &api.ContentsResponse{
+			Name:        filename,
+			Path:        filename,
 			SHA:         "dbf8d00e022e05b7e5cf7e535de857de57925647",
+			Type:        "file",
 			Size:        43,
-			URL:         setting.AppURL + "api/v1/repos/user2/repo1/contents/README.md",
-			HTMLURL:     setting.AppURL + "user2/repo1/blob/master/README.md",
-			GitURL:      setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647",
-			DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/README.md",
-			Type:        "blob",
+			Encoding:    &encoding,
+			Content:     &content,
+			URL:         &selfURL,
+			HTMLURL:     &htmlURL,
+			GitURL:      &gitURL,
+			DownloadURL: &downloadURL,
 			Links: &api.FileLinksResponse{
-				Self:    setting.AppURL + "api/v1/repos/user2/repo1/contents/README.md",
-				GitURL:  setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647",
-				HTMLURL: setting.AppURL + "user2/repo1/blob/master/README.md",
+				Self:    &selfURL,
+				GitURL:  &gitURL,
+				HTMLURL: &htmlURL,
 			},
 		},
 		Commit: &api.FileCommitResponse{
@@ -213,7 +231,7 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
 		assert.Nil(t, err)
 		gitRepo, _ := git.OpenRepository(repo.RepoPath())
 		commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch)
-		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commitID)
+		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commitID, opts.TreePath)
 		assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
 		assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
 		assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
@@ -234,9 +252,8 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
 		repo := ctx.Repo.Repository
 		doer := ctx.User
 		opts := getUpdateRepoFileOptions(repo)
-		suffix := "_new"
 		opts.FromTreePath = "README.md"
-		opts.TreePath = "README.md" + suffix // new file name, README.md_new
+		opts.TreePath = "README_new.md" // new file name, README_new.md
 
 		// test
 		fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
@@ -245,7 +262,7 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
 		assert.Nil(t, err)
 		gitRepo, _ := git.OpenRepository(repo.RepoPath())
 		commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
-		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String())
+		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath)
 		// assert that the old file no longer exists in the last commit of the branch
 		fromEntry, err := commit.GetTreeEntryByPath(opts.FromTreePath)
 		toEntry, err := commit.GetTreeEntryByPath(opts.TreePath)
@@ -253,9 +270,9 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
 		assert.NotNil(t, toEntry) // Should exist here
 		// assert SHA has remained the same but paths use the new file name
 		assert.EqualValues(t, expectedFileResponse.Content.SHA, fileResponse.Content.SHA)
-		assert.EqualValues(t, expectedFileResponse.Content.Name+suffix, fileResponse.Content.Name)
-		assert.EqualValues(t, expectedFileResponse.Content.Path+suffix, fileResponse.Content.Path)
-		assert.EqualValues(t, expectedFileResponse.Content.URL+suffix, fileResponse.Content.URL)
+		assert.EqualValues(t, expectedFileResponse.Content.Name, fileResponse.Content.Name)
+		assert.EqualValues(t, expectedFileResponse.Content.Path, fileResponse.Content.Path)
+		assert.EqualValues(t, expectedFileResponse.Content.URL, fileResponse.Content.URL)
 		assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
 		assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
 	})
@@ -284,7 +301,7 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
 		assert.Nil(t, err)
 		gitRepo, _ := git.OpenRepository(repo.RepoPath())
 		commitID, _ := gitRepo.GetBranchCommitID(repo.DefaultBranch)
-		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commitID)
+		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commitID, opts.TreePath)
 		assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
 	})
 }
diff --git a/modules/git/blob.go b/modules/git/blob.go
index 73ac89dfdf9..68147673a35 100644
--- a/modules/git/blob.go
+++ b/modules/git/blob.go
@@ -37,6 +37,19 @@ func (b *Blob) Name() string {
 	return b.name
 }
 
+// GetBlobContent Gets the content of the blob as raw text
+func (b *Blob) GetBlobContent() (string, error) {
+	dataRc, err := b.DataAsync()
+	if err != nil {
+		return "", err
+	}
+	defer dataRc.Close()
+	buf := make([]byte, 1024)
+	n, _ := dataRc.Read(buf)
+	buf = buf[:n]
+	return string(buf), nil
+}
+
 // GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string
 func (b *Blob) GetBlobContentBase64() (string, error) {
 	dataRc, err := b.DataAsync()
diff --git a/modules/git/repo_object.go b/modules/git/repo_object.go
index 67060e30b0b..d4d638a7434 100644
--- a/modules/git/repo_object.go
+++ b/modules/git/repo_object.go
@@ -1,4 +1,5 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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.
 
@@ -22,6 +23,8 @@ const (
 	ObjectBlob ObjectType = "blob"
 	// ObjectTag tag object type
 	ObjectTag ObjectType = "tag"
+	// ObjectBranch branch object type
+	ObjectBranch ObjectType = "branch"
 )
 
 // HashObject takes a reader and returns SHA1 hash for that reader
@@ -44,3 +47,17 @@ func (repo *Repository) hashObject(reader io.Reader) (string, error) {
 	}
 	return strings.TrimSpace(stdout.String()), nil
 }
+
+// GetRefType gets the type of the ref based on the string
+func (repo *Repository) GetRefType(ref string) ObjectType {
+	if repo.IsTagExist(ref) {
+		return ObjectTag
+	} else if repo.IsBranchExist(ref) {
+		return ObjectBranch
+	} else if repo.IsCommitExist(ref) {
+		return ObjectCommit
+	} else if _, err := repo.GetBlob(ref); err == nil {
+		return ObjectBlob
+	}
+	return ObjectType("invalid")
+}
diff --git a/modules/repofiles/content.go b/modules/repofiles/content.go
index 3098087dc67..9637658e78b 100644
--- a/modules/repofiles/content.go
+++ b/modules/repofiles/content.go
@@ -5,26 +5,52 @@
 package repofiles
 
 import (
+	"fmt"
 	"net/url"
+	"path"
+	"strings"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/git"
 	api "code.gitea.io/gitea/modules/structs"
 )
 
-// GetFileContents gets the meta data on a file's contents
-func GetFileContents(repo *models.Repository, treePath, ref string) (*api.FileContentResponse, error) {
+// ContentType repo content type
+type ContentType string
+
+// The string representations of different content types
+const (
+	// ContentTypeRegular regular content type (file)
+	ContentTypeRegular ContentType = "file"
+	// ContentTypeDir dir content type (dir)
+	ContentTypeDir ContentType = "dir"
+	// ContentLink link content type (symlink)
+	ContentTypeLink ContentType = "symlink"
+	// ContentTag submodule content type (submodule)
+	ContentTypeSubmodule ContentType = "submodule"
+)
+
+// String gets the string of ContentType
+func (ct *ContentType) String() string {
+	return string(*ct)
+}
+
+// GetContentsOrList gets the meta data of a file's contents (*ContentsResponse) if treePath not a tree
+// directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag
+func GetContentsOrList(repo *models.Repository, treePath, ref string) (interface{}, error) {
 	if ref == "" {
 		ref = repo.DefaultBranch
 	}
+	origRef := ref
 
 	// Check that the path given in opts.treePath is valid (not a git path)
-	treePath = CleanUploadFileName(treePath)
-	if treePath == "" {
+	cleanTreePath := CleanUploadFileName(treePath)
+	if cleanTreePath == "" && treePath != "" {
 		return nil, models.ErrFilenameInvalid{
 			Path: treePath,
 		}
 	}
+	treePath = cleanTreePath
 
 	gitRepo, err := git.OpenRepository(repo.RepoPath())
 	if err != nil {
@@ -42,32 +68,145 @@ func GetFileContents(repo *models.Repository, treePath, ref string) (*api.FileCo
 		return nil, err
 	}
 
-	urlRef := ref
-	if _, err := gitRepo.GetBranchCommit(ref); err == nil {
-		urlRef = "branch/" + ref
+	if entry.Type() != "tree" {
+		return GetContents(repo, treePath, origRef, false)
+	}
+
+	// We are in a directory, so we return a list of FileContentResponse objects
+	var fileList []*api.ContentsResponse
+
+	gitTree, err := commit.SubTree(treePath)
+	if err != nil {
+		return nil, err
+	}
+	entries, err := gitTree.ListEntries()
+	if err != nil {
+		return nil, err
+	}
+	for _, e := range entries {
+		subTreePath := path.Join(treePath, e.Name())
+		fileContentResponse, err := GetContents(repo, subTreePath, origRef, true)
+		if err != nil {
+			return nil, err
+		}
+		fileList = append(fileList, fileContentResponse)
+	}
+	return fileList, nil
+}
+
+// GetContents gets the meta data on a file's contents. Ref can be a branch, commit or tag
+func GetContents(repo *models.Repository, treePath, ref string, forList bool) (*api.ContentsResponse, error) {
+	if ref == "" {
+		ref = repo.DefaultBranch
 	}
+	origRef := ref
 
-	selfURL, _ := url.Parse(repo.APIURL() + "/contents/" + treePath)
-	gitURL, _ := url.Parse(repo.APIURL() + "/git/blobs/" + entry.ID.String())
-	downloadURL, _ := url.Parse(repo.HTMLURL() + "/raw/" + urlRef + "/" + treePath)
-	htmlURL, _ := url.Parse(repo.HTMLURL() + "/blob/" + ref + "/" + treePath)
+	// Check that the path given in opts.treePath is valid (not a git path)
+	cleanTreePath := CleanUploadFileName(treePath)
+	if cleanTreePath == "" && treePath != "" {
+		return nil, models.ErrFilenameInvalid{
+			Path: treePath,
+		}
+	}
+	treePath = cleanTreePath
+
+	gitRepo, err := git.OpenRepository(repo.RepoPath())
+	if err != nil {
+		return nil, err
+	}
 
-	fileContent := &api.FileContentResponse{
-		Name:        entry.Name(),
-		Path:        treePath,
-		SHA:         entry.ID.String(),
-		Size:        entry.Size(),
-		URL:         selfURL.String(),
-		HTMLURL:     htmlURL.String(),
-		GitURL:      gitURL.String(),
-		DownloadURL: downloadURL.String(),
-		Type:        entry.Type(),
+	// Get the commit object for the ref
+	commit, err := gitRepo.GetCommit(ref)
+	if err != nil {
+		return nil, err
+	}
+	commitID := commit.ID.String()
+	if len(ref) >= 4 && strings.HasPrefix(commitID, ref) {
+		ref = commit.ID.String()
+	}
+
+	entry, err := commit.GetTreeEntryByPath(treePath)
+	if err != nil {
+		return nil, err
+	}
+
+	refType := gitRepo.GetRefType(ref)
+	if refType == "invalid" {
+		return nil, fmt.Errorf("no commit found for the ref [ref: %s]", ref)
+	}
+
+	selfURL, err := url.Parse(fmt.Sprintf("%s/contents/%s?ref=%s", repo.APIURL(), treePath, origRef))
+	if err != nil {
+		return nil, err
+	}
+	selfURLString := selfURL.String()
+
+	// All content types have these fields in populated
+	contentsResponse := &api.ContentsResponse{
+		Name: entry.Name(),
+		Path: treePath,
+		SHA:  entry.ID.String(),
+		Size: entry.Size(),
+		URL:  &selfURLString,
 		Links: &api.FileLinksResponse{
-			Self:    selfURL.String(),
-			GitURL:  gitURL.String(),
-			HTMLURL: htmlURL.String(),
+			Self: &selfURLString,
 		},
 	}
 
-	return fileContent, nil
+	// Now populate the rest of the ContentsResponse based on entry type
+	if entry.IsRegular() {
+		contentsResponse.Type = string(ContentTypeRegular)
+		if blobResponse, err := GetBlobBySHA(repo, entry.ID.String()); err != nil {
+			return nil, err
+		} else if !forList {
+			// We don't show the content if we are getting a list of FileContentResponses
+			contentsResponse.Encoding = &blobResponse.Encoding
+			contentsResponse.Content = &blobResponse.Content
+		}
+	} else if entry.IsDir() {
+		contentsResponse.Type = string(ContentTypeDir)
+	} else if entry.IsLink() {
+		contentsResponse.Type = string(ContentTypeLink)
+		// The target of a symlink file is the content of the file
+		targetFromContent, err := entry.Blob().GetBlobContent()
+		if err != nil {
+			return nil, err
+		}
+		contentsResponse.Target = &targetFromContent
+	} else if entry.IsSubModule() {
+		contentsResponse.Type = string(ContentTypeSubmodule)
+		submodule, err := commit.GetSubModule(treePath)
+		if err != nil {
+			return nil, err
+		}
+		contentsResponse.SubmoduleGitURL = &submodule.URL
+	}
+	// Handle links
+	if entry.IsRegular() || entry.IsLink() {
+		downloadURL, err := url.Parse(fmt.Sprintf("%s/raw/%s/%s/%s", repo.HTMLURL(), refType, ref, treePath))
+		if err != nil {
+			return nil, err
+		}
+		downloadURLString := downloadURL.String()
+		contentsResponse.DownloadURL = &downloadURLString
+	}
+	if !entry.IsSubModule() {
+		htmlURL, err := url.Parse(fmt.Sprintf("%s/src/%s/%s/%s", repo.HTMLURL(), refType, ref, treePath))
+		if err != nil {
+			return nil, err
+		}
+		htmlURLString := htmlURL.String()
+		contentsResponse.HTMLURL = &htmlURLString
+		contentsResponse.Links.HTMLURL = &htmlURLString
+
+		gitURL, err := url.Parse(fmt.Sprintf("%s/git/blobs/%s", repo.APIURL(), entry.ID.String()))
+		if err != nil {
+			return nil, err
+		}
+		gitURLString := gitURL.String()
+		contentsResponse.GitURL = &gitURLString
+		contentsResponse.Links.GitURL = &gitURLString
+	}
+
+	return contentsResponse, nil
 }
diff --git a/modules/repofiles/content_test.go b/modules/repofiles/content_test.go
index ce3f5f3678b..ef6c5eafc22 100644
--- a/modules/repofiles/content_test.go
+++ b/modules/repofiles/content_test.go
@@ -9,7 +9,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/structs"
+	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 
 	"github.com/stretchr/testify/assert"
@@ -19,7 +19,36 @@ func TestMain(m *testing.M) {
 	models.MainTest(m, filepath.Join("..", ".."))
 }
 
-func TestGetFileContents(t *testing.T) {
+func getExpectedReadmeContentsResponse() *api.ContentsResponse {
+	treePath := "README.md"
+	sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
+	encoding := "base64"
+	content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"
+	selfURL := "https://try.gitea.io/api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
+	htmlURL := "https://try.gitea.io/user2/repo1/src/branch/master/" + treePath
+	gitURL := "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/" + sha
+	downloadURL := "https://try.gitea.io/user2/repo1/raw/branch/master/" + treePath
+	return &api.ContentsResponse{
+		Name:        treePath,
+		Path:        treePath,
+		SHA:         "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
+		Type:        "file",
+		Size:        30,
+		Encoding:    &encoding,
+		Content:     &content,
+		URL:         &selfURL,
+		HTMLURL:     &htmlURL,
+		GitURL:      &gitURL,
+		DownloadURL: &downloadURL,
+		Links: &api.FileLinksResponse{
+			Self:    &selfURL,
+			GitURL:  &gitURL,
+			HTMLURL: &htmlURL,
+		},
+	}
+}
+
+func TestGetContents(t *testing.T) {
 	models.PrepareTestEnv(t)
 	ctx := test.MockContext(t, "user2/repo1")
 	ctx.SetParams(":id", "1")
@@ -30,37 +59,110 @@ func TestGetFileContents(t *testing.T) {
 	treePath := "README.md"
 	ref := ctx.Repo.Repository.DefaultBranch
 
-	expectedFileContentResponse := &structs.FileContentResponse{
-		Name:        treePath,
-		Path:        treePath,
-		SHA:         "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
-		Size:        30,
-		URL:         "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
-		HTMLURL:     "https://try.gitea.io/user2/repo1/blob/master/README.md",
-		GitURL:      "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
-		DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/README.md",
-		Type:        "blob",
-		Links: &structs.FileLinksResponse{
-			Self:    "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
-			GitURL:  "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
-			HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md",
-		},
+	expectedContentsResponse := getExpectedReadmeContentsResponse()
+
+	t.Run("Get README.md contents with GetContents()", func(t *testing.T) {
+		fileContentResponse, err := GetContents(ctx.Repo.Repository, treePath, ref, false)
+		assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
+		assert.Nil(t, err)
+	})
+
+	t.Run("Get REAMDE.md contents with ref as empty string (should then use the repo's default branch) with GetContents()", func(t *testing.T) {
+		fileContentResponse, err := GetContents(ctx.Repo.Repository, treePath, "", false)
+		assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
+		assert.Nil(t, err)
+	})
+}
+
+func TestGetContentsOrListForDir(t *testing.T) {
+	models.PrepareTestEnv(t)
+	ctx := test.MockContext(t, "user2/repo1")
+	ctx.SetParams(":id", "1")
+	test.LoadRepo(t, ctx, 1)
+	test.LoadRepoCommit(t, ctx)
+	test.LoadUser(t, ctx, 2)
+	test.LoadGitRepo(t, ctx)
+	treePath := "" // root dir
+	ref := ctx.Repo.Repository.DefaultBranch
+
+	readmeContentsResponse := getExpectedReadmeContentsResponse()
+	// because will be in a list, doesn't have encoding and content
+	readmeContentsResponse.Encoding = nil
+	readmeContentsResponse.Content = nil
+
+	expectedContentsListResponse := []*api.ContentsResponse{
+		readmeContentsResponse,
 	}
 
-	t.Run("Get README.md contents", func(t *testing.T) {
-		fileContentResponse, err := GetFileContents(ctx.Repo.Repository, treePath, ref)
-		assert.EqualValues(t, expectedFileContentResponse, fileContentResponse)
+	t.Run("Get root dir contents with GetContentsOrList()", func(t *testing.T) {
+		fileContentResponse, err := GetContentsOrList(ctx.Repo.Repository, treePath, ref)
+		assert.EqualValues(t, expectedContentsListResponse, fileContentResponse)
+		assert.Nil(t, err)
+	})
+
+	t.Run("Get root dir contents with ref as empty string (should then use the repo's default branch) with GetContentsOrList()", func(t *testing.T) {
+		fileContentResponse, err := GetContentsOrList(ctx.Repo.Repository, treePath, "")
+		assert.EqualValues(t, expectedContentsListResponse, fileContentResponse)
+		assert.Nil(t, err)
+	})
+}
+
+func TestGetContentsOrListForFile(t *testing.T) {
+	models.PrepareTestEnv(t)
+	ctx := test.MockContext(t, "user2/repo1")
+	ctx.SetParams(":id", "1")
+	test.LoadRepo(t, ctx, 1)
+	test.LoadRepoCommit(t, ctx)
+	test.LoadUser(t, ctx, 2)
+	test.LoadGitRepo(t, ctx)
+	treePath := "README.md"
+	ref := ctx.Repo.Repository.DefaultBranch
+
+	expectedContentsResponse := getExpectedReadmeContentsResponse()
+
+	t.Run("Get README.md contents with GetContentsOrList()", func(t *testing.T) {
+		fileContentResponse, err := GetContentsOrList(ctx.Repo.Repository, treePath, ref)
+		assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
 		assert.Nil(t, err)
 	})
 
-	t.Run("Get REAMDE.md contents with ref as empty string (should then use the repo's default branch)", func(t *testing.T) {
-		fileContentResponse, err := GetFileContents(ctx.Repo.Repository, treePath, "")
-		assert.EqualValues(t, expectedFileContentResponse, fileContentResponse)
+	t.Run("Get REAMDE.md contents with ref as empty string (should then use the repo's default branch) with GetContentsOrList()", func(t *testing.T) {
+		fileContentResponse, err := GetContentsOrList(ctx.Repo.Repository, treePath, "")
+		assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
 		assert.Nil(t, err)
 	})
 }
 
-func TestGetFileContentsErrors(t *testing.T) {
+func TestGetContentsErrors(t *testing.T) {
+	models.PrepareTestEnv(t)
+	ctx := test.MockContext(t, "user2/repo1")
+	ctx.SetParams(":id", "1")
+	test.LoadRepo(t, ctx, 1)
+	test.LoadRepoCommit(t, ctx)
+	test.LoadUser(t, ctx, 2)
+	test.LoadGitRepo(t, ctx)
+	repo := ctx.Repo.Repository
+	treePath := "README.md"
+	ref := repo.DefaultBranch
+
+	t.Run("bad treePath", func(t *testing.T) {
+		badTreePath := "bad/tree.md"
+		fileContentResponse, err := GetContents(repo, badTreePath, ref, false)
+		assert.Error(t, err)
+		assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]")
+		assert.Nil(t, fileContentResponse)
+	})
+
+	t.Run("bad ref", func(t *testing.T) {
+		badRef := "bad_ref"
+		fileContentResponse, err := GetContents(repo, treePath, badRef, false)
+		assert.Error(t, err)
+		assert.EqualError(t, err, "object does not exist [id: "+badRef+", rel_path: ]")
+		assert.Nil(t, fileContentResponse)
+	})
+}
+
+func TestGetContentsOrListErrors(t *testing.T) {
 	models.PrepareTestEnv(t)
 	ctx := test.MockContext(t, "user2/repo1")
 	ctx.SetParams(":id", "1")
@@ -74,7 +176,7 @@ func TestGetFileContentsErrors(t *testing.T) {
 
 	t.Run("bad treePath", func(t *testing.T) {
 		badTreePath := "bad/tree.md"
-		fileContentResponse, err := GetFileContents(repo, badTreePath, ref)
+		fileContentResponse, err := GetContentsOrList(repo, badTreePath, ref)
 		assert.Error(t, err)
 		assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]")
 		assert.Nil(t, fileContentResponse)
@@ -82,7 +184,7 @@ func TestGetFileContentsErrors(t *testing.T) {
 
 	t.Run("bad ref", func(t *testing.T) {
 		badRef := "bad_ref"
-		fileContentResponse, err := GetFileContents(repo, treePath, badRef)
+		fileContentResponse, err := GetContentsOrList(repo, treePath, badRef)
 		assert.Error(t, err)
 		assert.EqualError(t, err, "object does not exist [id: "+badRef+", rel_path: ]")
 		assert.Nil(t, fileContentResponse)
diff --git a/modules/repofiles/file.go b/modules/repofiles/file.go
index 70fd57bba0e..801f770e02f 100644
--- a/modules/repofiles/file.go
+++ b/modules/repofiles/file.go
@@ -17,8 +17,8 @@ import (
 
 // GetFileResponseFromCommit Constructs a FileResponse from a Commit object
 func GetFileResponseFromCommit(repo *models.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) {
-	fileContents, _ := GetFileContents(repo, treeName, branch)   // ok if fails, then will be nil
-	fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
+	fileContents, _ := GetContents(repo, treeName, branch, false) // ok if fails, then will be nil
+	fileCommitResponse, _ := GetFileCommitResponse(repo, commit)  // ok if fails, then will be nil
 	verification := GetPayloadCommitVerification(commit)
 	fileResponse := &api.FileResponse{
 		Content:      fileContents,
diff --git a/modules/repofiles/file_test.go b/modules/repofiles/file_test.go
index 5f6320a938a..00feb93fffa 100644
--- a/modules/repofiles/file_test.go
+++ b/modules/repofiles/file_test.go
@@ -5,6 +5,7 @@
 package repofiles
 
 import (
+	"code.gitea.io/gitea/modules/setting"
 	"testing"
 
 	"code.gitea.io/gitea/models"
@@ -16,21 +17,31 @@ import (
 )
 
 func getExpectedFileResponse() *api.FileResponse {
+	treePath := "README.md"
+	sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
+	encoding := "base64"
+	content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"
+	selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
+	htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
+	gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
+	downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
 	return &api.FileResponse{
-		Content: &api.FileContentResponse{
-			Name:        "README.md",
-			Path:        "README.md",
-			SHA:         "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
+		Content: &api.ContentsResponse{
+			Name:        treePath,
+			Path:        treePath,
+			SHA:         sha,
+			Type:        "file",
 			Size:        30,
-			URL:         "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
-			HTMLURL:     "https://try.gitea.io/user2/repo1/blob/master/README.md",
-			GitURL:      "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
-			DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/README.md",
-			Type:        "blob",
+			Encoding:    &encoding,
+			Content:     &content,
+			URL:         &selfURL,
+			HTMLURL:     &htmlURL,
+			GitURL:      &gitURL,
+			DownloadURL: &downloadURL,
 			Links: &api.FileLinksResponse{
-				Self:    "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
-				GitURL:  "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
-				HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md",
+				Self:    &selfURL,
+				GitURL:  &gitURL,
+				HTMLURL: &htmlURL,
 			},
 		},
 		Commit: &api.FileCommitResponse{
diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go
index e5be9ce1083..b2eeb7f13ad 100644
--- a/modules/structs/repo_file.go
+++ b/modules/structs/repo_file.go
@@ -49,23 +49,32 @@ type UpdateFileOptions struct {
 
 // FileLinksResponse contains the links for a repo's file
 type FileLinksResponse struct {
-	Self    string `json:"url"`
-	GitURL  string `json:"git_url"`
-	HTMLURL string `json:"html_url"`
+	Self    *string `json:"self"`
+	GitURL  *string `json:"git"`
+	HTMLURL *string `json:"html"`
 }
 
-// FileContentResponse contains information about a repo's file stats and content
-type FileContentResponse struct {
-	Name        string             `json:"name"`
-	Path        string             `json:"path"`
-	SHA         string             `json:"sha"`
-	Size        int64              `json:"size"`
-	URL         string             `json:"url"`
-	HTMLURL     string             `json:"html_url"`
-	GitURL      string             `json:"git_url"`
-	DownloadURL string             `json:"download_url"`
-	Type        string             `json:"type"`
-	Links       *FileLinksResponse `json:"_links"`
+// ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content
+type ContentsResponse struct {
+	Name string `json:"name"`
+	Path string `json:"path"`
+	SHA  string `json:"sha"`
+	// `type` will be `file`, `dir`, `symlink`, or `submodule`
+	Type string `json:"type"`
+	Size int64  `json:"size"`
+	// `encoding` is populated when `type` is `file`, otherwise null
+	Encoding *string `json:"encoding"`
+	// `content` is populated when `type` is `file`, otherwise null
+	Content *string `json:"content"`
+	// `target` is populated when `type` is `symlink`, otherwise null
+	Target      *string `json:"target"`
+	URL         *string `json:"url"`
+	HTMLURL     *string `json:"html_url"`
+	GitURL      *string `json:"git_url"`
+	DownloadURL *string `json:"download_url"`
+	// `submodule_git_url` is populated when `type` is `submodule`, otherwise null
+	SubmoduleGitURL *string            `json:"submodule_git_url"`
+	Links           *FileLinksResponse `json:"_links"`
 }
 
 // FileCommitResponse contains information generated from a Git commit for a repo's file.
@@ -81,7 +90,7 @@ type FileCommitResponse struct {
 
 // FileResponse contains information about a repo's file
 type FileResponse struct {
-	Content      *FileContentResponse       `json:"content"`
+	Content      *ContentsResponse          `json:"content"`
 	Commit       *FileCommitResponse        `json:"commit"`
 	Verification *PayloadCommitVerification `json:"verification"`
 }
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 2268c1be38e..8e7a74eca2f 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -766,7 +766,8 @@ func RegisterRoutes(m *macaron.Macaron) {
 					m.Get("/tags/:sha", context.RepoRef(), repo.GetTag)
 				}, reqRepoReader(models.UnitTypeCode))
 				m.Group("/contents", func() {
-					m.Get("/*", repo.GetFileContents)
+					m.Get("", repo.GetContentsList)
+					m.Get("/*", repo.GetContents)
 					m.Group("/*", func() {
 						m.Post("", bind(api.CreateFileOptions{}), repo.CreateFile)
 						m.Put("", bind(api.UpdateFileOptions{}), repo.UpdateFile)
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index d5107562833..ae20e1e96be 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -366,11 +366,11 @@ func DeleteFile(ctx *context.APIContext, apiOpts api.DeleteFileOptions) {
 	}
 }
 
-// GetFileContents Get the contents of a fle in a repository
-func GetFileContents(ctx *context.APIContext) {
-	// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetFileContents
+// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
+func GetContents(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
 	// ---
-	// summary: Gets the contents of a file or directory in a repository
+	// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
 	// produces:
 	// - application/json
 	// parameters:
@@ -386,20 +386,20 @@ func GetFileContents(ctx *context.APIContext) {
 	//   required: true
 	// - name: filepath
 	//   in: path
-	//   description: path of the file to delete
+	//   description: path of the dir, file, symlink or submodule in the repo
 	//   type: string
 	//   required: true
 	// - name: ref
 	//   in: query
 	//   description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
-	//   required: false
 	//   type: string
+	//   required: false
 	// responses:
 	//   "200":
-	//     "$ref": "#/responses/FileContentResponse"
+	//     "$ref": "#/responses/ContentsResponse"
 
 	if !CanReadFiles(ctx.Repo) {
-		ctx.Error(http.StatusInternalServerError, "GetFileContents", models.ErrUserDoesNotHaveAccessToRepo{
+		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", models.ErrUserDoesNotHaveAccessToRepo{
 			UserID:   ctx.User.ID,
 			RepoName: ctx.Repo.Repository.LowerName,
 		})
@@ -409,9 +409,40 @@ func GetFileContents(ctx *context.APIContext) {
 	treePath := ctx.Params("*")
 	ref := ctx.QueryTrim("ref")
 
-	if fileContents, err := repofiles.GetFileContents(ctx.Repo.Repository, treePath, ref); err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetFileContents", err)
+	if fileList, err := repofiles.GetContentsOrList(ctx.Repo.Repository, treePath, ref); err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
 	} else {
-		ctx.JSON(http.StatusOK, fileContents)
+		ctx.JSON(http.StatusOK, fileList)
 	}
 }
+
+// GetContentsList Get the metadata of all the entries of the root dir
+func GetContentsList(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
+	// ---
+	// summary: Gets the metadata of all the entries of the root dir
+	// 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: ref
+	//   in: query
+	//   description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
+	//   type: string
+	//   required: false
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/ContentsListResponse"
+
+	// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
+	GetContents(ctx)
+}
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index 25354b3d666..2cab5b0ed42 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -197,11 +197,18 @@ type swaggerFileResponse struct {
 	Body api.FileResponse `json:"body"`
 }
 
-// FileContentResponse
-// swagger:response FileContentResponse
-type swaggerFileContentResponse struct {
+// ContentsResponse
+// swagger:response ContentsResponse
+type swaggerContentsResponse struct {
 	//in: body
-	Body api.FileContentResponse `json:"body"`
+	Body api.ContentsResponse `json:"body"`
+}
+
+// ContentsListResponse
+// swagger:response ContentsListResponse
+type swaggerContentsListResponse struct {
+	// in:body
+	Body []api.ContentsResponse `json:"body"`
 }
 
 // FileDeleteResponse
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 6c2708dd963..d6d501ed22c 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1570,6 +1570,45 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/contents": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Gets the metadata of all the entries of the root dir",
+        "operationId": "repoGetContentsList",
+        "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": "string",
+            "description": "The name of the commit/branch/tag. Default the repository’s default branch (usually master)",
+            "name": "ref",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ContentsListResponse"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/contents/{filepath}": {
       "get": {
         "produces": [
@@ -1578,8 +1617,8 @@
         "tags": [
           "repository"
         ],
-        "summary": "Gets the contents of a file or directory in a repository",
-        "operationId": "repoGetFileContents",
+        "summary": "Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir",
+        "operationId": "repoGetContents",
         "parameters": [
           {
             "type": "string",
@@ -1597,7 +1636,7 @@
           },
           {
             "type": "string",
-            "description": "path of the file to delete",
+            "description": "path of the dir, file, symlink or submodule in the repo",
             "name": "filepath",
             "in": "path",
             "required": true
@@ -1611,7 +1650,7 @@
         ],
         "responses": {
           "200": {
-            "$ref": "#/responses/FileContentResponse"
+            "$ref": "#/responses/ContentsResponse"
           }
         }
       },
@@ -7017,6 +7056,74 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "ContentsResponse": {
+      "description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content",
+      "type": "object",
+      "properties": {
+        "_links": {
+          "$ref": "#/definitions/FileLinksResponse"
+        },
+        "content": {
+          "description": "`content` is populated when `type` is `file`, otherwise null",
+          "type": "string",
+          "x-go-name": "Content"
+        },
+        "download_url": {
+          "type": "string",
+          "x-go-name": "DownloadURL"
+        },
+        "encoding": {
+          "description": "`encoding` is populated when `type` is `file`, otherwise null",
+          "type": "string",
+          "x-go-name": "Encoding"
+        },
+        "git_url": {
+          "type": "string",
+          "x-go-name": "GitURL"
+        },
+        "html_url": {
+          "type": "string",
+          "x-go-name": "HTMLURL"
+        },
+        "name": {
+          "type": "string",
+          "x-go-name": "Name"
+        },
+        "path": {
+          "type": "string",
+          "x-go-name": "Path"
+        },
+        "sha": {
+          "type": "string",
+          "x-go-name": "SHA"
+        },
+        "size": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "Size"
+        },
+        "submodule_git_url": {
+          "description": "`submodule_git_url` is populated when `type` is `submodule`, otherwise null",
+          "type": "string",
+          "x-go-name": "SubmoduleGitURL"
+        },
+        "target": {
+          "description": "`target` is populated when `type` is `symlink`, otherwise null",
+          "type": "string",
+          "x-go-name": "Target"
+        },
+        "type": {
+          "description": "`type` will be `file`, `dir`, `symlink`, or `submodule`",
+          "type": "string",
+          "x-go-name": "Type"
+        },
+        "url": {
+          "type": "string",
+          "x-go-name": "URL"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "CreateEmailOption": {
       "description": "CreateEmailOption options when creating email addresses",
       "type": "object",
@@ -8179,53 +8286,6 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
-    "FileContentResponse": {
-      "description": "FileContentResponse contains information about a repo's file stats and content",
-      "type": "object",
-      "properties": {
-        "_links": {
-          "$ref": "#/definitions/FileLinksResponse"
-        },
-        "download_url": {
-          "type": "string",
-          "x-go-name": "DownloadURL"
-        },
-        "git_url": {
-          "type": "string",
-          "x-go-name": "GitURL"
-        },
-        "html_url": {
-          "type": "string",
-          "x-go-name": "HTMLURL"
-        },
-        "name": {
-          "type": "string",
-          "x-go-name": "Name"
-        },
-        "path": {
-          "type": "string",
-          "x-go-name": "Path"
-        },
-        "sha": {
-          "type": "string",
-          "x-go-name": "SHA"
-        },
-        "size": {
-          "type": "integer",
-          "format": "int64",
-          "x-go-name": "Size"
-        },
-        "type": {
-          "type": "string",
-          "x-go-name": "Type"
-        },
-        "url": {
-          "type": "string",
-          "x-go-name": "URL"
-        }
-      },
-      "x-go-package": "code.gitea.io/gitea/modules/structs"
-    },
     "FileDeleteResponse": {
       "description": "FileDeleteResponse contains information about a repo's file that was deleted",
       "type": "object",
@@ -8247,15 +8307,15 @@
       "description": "FileLinksResponse contains the links for a repo's file",
       "type": "object",
       "properties": {
-        "git_url": {
+        "git": {
           "type": "string",
           "x-go-name": "GitURL"
         },
-        "html_url": {
+        "html": {
           "type": "string",
           "x-go-name": "HTMLURL"
         },
-        "url": {
+        "self": {
           "type": "string",
           "x-go-name": "Self"
         }
@@ -8270,7 +8330,7 @@
           "$ref": "#/definitions/FileCommitResponse"
         },
         "content": {
-          "$ref": "#/definitions/FileContentResponse"
+          "$ref": "#/definitions/ContentsResponse"
         },
         "verification": {
           "$ref": "#/definitions/PayloadCommitVerification"
@@ -9898,6 +9958,21 @@
         "$ref": "#/definitions/Commit"
       }
     },
+    "ContentsListResponse": {
+      "description": "ContentsListResponse",
+      "schema": {
+        "type": "array",
+        "items": {
+          "$ref": "#/definitions/ContentsResponse"
+        }
+      }
+    },
+    "ContentsResponse": {
+      "description": "ContentsResponse",
+      "schema": {
+        "$ref": "#/definitions/ContentsResponse"
+      }
+    },
     "DeployKey": {
       "description": "DeployKey",
       "schema": {
@@ -9922,12 +9997,6 @@
         }
       }
     },
-    "FileContentResponse": {
-      "description": "FileContentResponse",
-      "schema": {
-        "$ref": "#/definitions/FileContentResponse"
-      }
-    },
     "FileDeleteResponse": {
       "description": "FileDeleteResponse",
       "schema": {