Prechádzať zdrojové kódy

api: overhaul `/repos/owner/repos/contents` (#5980)

* Fix import path renaming

* api: overhaul /repos/owner/repos/contents
ᴜɴᴋɴᴡᴏɴ 5 rokov pred
rodič
commit
880d0ec19f

+ 6 - 1
internal/context/api.go

@@ -39,7 +39,7 @@ func (c *APIContext) Error(status int, title string, obj interface{}) {
 	}
 
 	if status == http.StatusInternalServerError {
-		log.Error("%s: %s", title, message)
+		log.ErrorDepth(5, "%s: %s", title, message)
 	}
 
 	c.JSON(status, map[string]string{
@@ -63,6 +63,11 @@ func (c *APIContext) ServerError(title string, err error) {
 	c.Error(http.StatusInternalServerError, title, err)
 }
 
+// Errorf renders the 500 response with formatted message.
+func (c *APIContext) Errorf(err error, format string, args ...interface{}) {
+	c.Error(http.StatusInternalServerError, fmt.Sprintf(format, args...), err)
+}
+
 // NotFoundOrServerError use error check function to determine if the error
 // is about not found. It responses with 404 status code for not found error,
 // or error context description for logging purpose of 500 server error.

+ 5 - 0
internal/gitutil/error.go

@@ -17,3 +17,8 @@ func IsErrRevisionNotExist(err error) bool {
 func IsErrNoMergeBase(err error) bool {
 	return err == git.ErrNoMergeBase
 }
+
+// IsErrSubmoduleNotExist returns true if the error is git.ErrSubmoduleNotExist.
+func IsErrSubmoduleNotExist(err error) bool {
+	return err == git.ErrSubmoduleNotExist
+}

+ 105 - 105
internal/route/api/v1/api.go

@@ -5,11 +5,6 @@
 package v1
 
 import (
-	admin2 "gogs.io/gogs/internal/route/api/v1/admin"
-	misc2 "gogs.io/gogs/internal/route/api/v1/misc"
-	org2 "gogs.io/gogs/internal/route/api/v1/org"
-	repo2 "gogs.io/gogs/internal/route/api/v1/repo"
-	user2 "gogs.io/gogs/internal/route/api/v1/user"
 	"net/http"
 	"strings"
 
@@ -22,6 +17,11 @@ import (
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/form"
+	"gogs.io/gogs/internal/route/api/v1/admin"
+	"gogs.io/gogs/internal/route/api/v1/misc"
+	"gogs.io/gogs/internal/route/api/v1/org"
+	"gogs.io/gogs/internal/route/api/v1/repo"
+	"gogs.io/gogs/internal/route/api/v1/user"
 )
 
 // repoAssignment extracts information from URL parameters to retrieve the repository,
@@ -176,229 +176,229 @@ func RegisterRoutes(m *macaron.Macaron) {
 		m.Options("/*", func() {})
 
 		// Miscellaneous
-		m.Post("/markdown", bind(api.MarkdownOption{}), misc2.Markdown)
-		m.Post("/markdown/raw", misc2.MarkdownRaw)
+		m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
+		m.Post("/markdown/raw", misc.MarkdownRaw)
 
 		// Users
 		m.Group("/users", func() {
-			m.Get("/search", user2.Search)
+			m.Get("/search", user.Search)
 
 			m.Group("/:username", func() {
-				m.Get("", user2.GetInfo)
+				m.Get("", user.GetInfo)
 
 				m.Group("/tokens", func() {
 					m.Combo("").
-						Get(user2.ListAccessTokens).
-						Post(bind(api.CreateAccessTokenOption{}), user2.CreateAccessToken)
+						Get(user.ListAccessTokens).
+						Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
 				}, reqBasicAuth())
 			})
 		})
 
 		m.Group("/users", func() {
 			m.Group("/:username", func() {
-				m.Get("/keys", user2.ListPublicKeys)
+				m.Get("/keys", user.ListPublicKeys)
 
-				m.Get("/followers", user2.ListFollowers)
+				m.Get("/followers", user.ListFollowers)
 				m.Group("/following", func() {
-					m.Get("", user2.ListFollowing)
-					m.Get("/:target", user2.CheckFollowing)
+					m.Get("", user.ListFollowing)
+					m.Get("/:target", user.CheckFollowing)
 				})
 			})
 		}, reqToken())
 
 		m.Group("/user", func() {
-			m.Get("", user2.GetAuthenticatedUser)
+			m.Get("", user.GetAuthenticatedUser)
 			m.Combo("/emails").
-				Get(user2.ListEmails).
-				Post(bind(api.CreateEmailOption{}), user2.AddEmail).
-				Delete(bind(api.CreateEmailOption{}), user2.DeleteEmail)
+				Get(user.ListEmails).
+				Post(bind(api.CreateEmailOption{}), user.AddEmail).
+				Delete(bind(api.CreateEmailOption{}), user.DeleteEmail)
 
-			m.Get("/followers", user2.ListMyFollowers)
+			m.Get("/followers", user.ListMyFollowers)
 			m.Group("/following", func() {
-				m.Get("", user2.ListMyFollowing)
+				m.Get("", user.ListMyFollowing)
 				m.Combo("/:username").
-					Get(user2.CheckMyFollowing).
-					Put(user2.Follow).
-					Delete(user2.Unfollow)
+					Get(user.CheckMyFollowing).
+					Put(user.Follow).
+					Delete(user.Unfollow)
 			})
 
 			m.Group("/keys", func() {
 				m.Combo("").
-					Get(user2.ListMyPublicKeys).
-					Post(bind(api.CreateKeyOption{}), user2.CreatePublicKey)
+					Get(user.ListMyPublicKeys).
+					Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
 				m.Combo("/:id").
-					Get(user2.GetPublicKey).
-					Delete(user2.DeletePublicKey)
+					Get(user.GetPublicKey).
+					Delete(user.DeletePublicKey)
 			})
 
-			m.Get("/issues", repo2.ListUserIssues)
+			m.Get("/issues", repo.ListUserIssues)
 		}, reqToken())
 
 		// Repositories
-		m.Get("/users/:username/repos", reqToken(), repo2.ListUserRepositories)
-		m.Get("/orgs/:org/repos", reqToken(), repo2.ListOrgRepositories)
+		m.Get("/users/:username/repos", reqToken(), repo.ListUserRepositories)
+		m.Get("/orgs/:org/repos", reqToken(), repo.ListOrgRepositories)
 		m.Combo("/user/repos", reqToken()).
-			Get(repo2.ListMyRepos).
-			Post(bind(api.CreateRepoOption{}), repo2.Create)
-		m.Post("/org/:org/repos", reqToken(), bind(api.CreateRepoOption{}), repo2.CreateOrgRepo)
+			Get(repo.ListMyRepos).
+			Post(bind(api.CreateRepoOption{}), repo.Create)
+		m.Post("/org/:org/repos", reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
 
 		m.Group("/repos", func() {
-			m.Get("/search", repo2.Search)
+			m.Get("/search", repo.Search)
 
-			m.Get("/:username/:reponame", repoAssignment(), repo2.Get)
+			m.Get("/:username/:reponame", repoAssignment(), repo.Get)
 		})
 
 		m.Group("/repos", func() {
-			m.Post("/migrate", bind(form.MigrateRepo{}), repo2.Migrate)
-			m.Delete("/:username/:reponame", repoAssignment(), repo2.Delete)
+			m.Post("/migrate", bind(form.MigrateRepo{}), repo.Migrate)
+			m.Delete("/:username/:reponame", repoAssignment(), repo.Delete)
 
 			m.Group("/:username/:reponame", func() {
 				m.Group("/hooks", func() {
 					m.Combo("").
-						Get(repo2.ListHooks).
-						Post(bind(api.CreateHookOption{}), repo2.CreateHook)
+						Get(repo.ListHooks).
+						Post(bind(api.CreateHookOption{}), repo.CreateHook)
 					m.Combo("/:id").
-						Patch(bind(api.EditHookOption{}), repo2.EditHook).
-						Delete(repo2.DeleteHook)
+						Patch(bind(api.EditHookOption{}), repo.EditHook).
+						Delete(repo.DeleteHook)
 				}, reqRepoAdmin())
 
 				m.Group("/collaborators", func() {
-					m.Get("", repo2.ListCollaborators)
+					m.Get("", repo.ListCollaborators)
 					m.Combo("/:collaborator").
-						Get(repo2.IsCollaborator).
-						Put(bind(api.AddCollaboratorOption{}), repo2.AddCollaborator).
-						Delete(repo2.DeleteCollaborator)
+						Get(repo.IsCollaborator).
+						Put(bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
+						Delete(repo.DeleteCollaborator)
 				}, reqRepoAdmin())
 
-				m.Get("/raw/*", context.RepoRef(), repo2.GetRawFile)
-				m.Get("/contents/*", context.RepoRef(), repo2.GetContents)
-				m.Get("/archive/*", repo2.GetArchive)
+				m.Get("/raw/*", context.RepoRef(), repo.GetRawFile)
+				m.Get("/contents/*", repo.GetContents)
+				m.Get("/archive/*", repo.GetArchive)
 				m.Group("/git/trees", func() {
-					m.Get("/:sha", context.RepoRef(), repo2.GetRepoGitTree)
+					m.Get("/:sha", context.RepoRef(), repo.GetRepoGitTree)
 				})
-				m.Get("/forks", repo2.ListForks)
+				m.Get("/forks", repo.ListForks)
 				m.Group("/branches", func() {
-					m.Get("", repo2.ListBranches)
-					m.Get("/*", repo2.GetBranch)
+					m.Get("", repo.ListBranches)
+					m.Get("/*", repo.GetBranch)
 				})
 				m.Group("/commits", func() {
-					m.Get("/:sha", repo2.GetSingleCommit)
-					m.Get("/*", repo2.GetReferenceSHA)
+					m.Get("/:sha", repo.GetSingleCommit)
+					m.Get("/*", repo.GetReferenceSHA)
 				})
 
 				m.Group("/keys", func() {
 					m.Combo("").
-						Get(repo2.ListDeployKeys).
-						Post(bind(api.CreateKeyOption{}), repo2.CreateDeployKey)
+						Get(repo.ListDeployKeys).
+						Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
 					m.Combo("/:id").
-						Get(repo2.GetDeployKey).
-						Delete(repo2.DeleteDeploykey)
+						Get(repo.GetDeployKey).
+						Delete(repo.DeleteDeploykey)
 				}, reqRepoAdmin())
 
 				m.Group("/issues", func() {
 					m.Combo("").
-						Get(repo2.ListIssues).
-						Post(bind(api.CreateIssueOption{}), repo2.CreateIssue)
+						Get(repo.ListIssues).
+						Post(bind(api.CreateIssueOption{}), repo.CreateIssue)
 					m.Group("/comments", func() {
-						m.Get("", repo2.ListRepoIssueComments)
-						m.Patch("/:id", bind(api.EditIssueCommentOption{}), repo2.EditIssueComment)
+						m.Get("", repo.ListRepoIssueComments)
+						m.Patch("/:id", bind(api.EditIssueCommentOption{}), repo.EditIssueComment)
 					})
 					m.Group("/:index", func() {
 						m.Combo("").
-							Get(repo2.GetIssue).
-							Patch(bind(api.EditIssueOption{}), repo2.EditIssue)
+							Get(repo.GetIssue).
+							Patch(bind(api.EditIssueOption{}), repo.EditIssue)
 
 						m.Group("/comments", func() {
 							m.Combo("").
-								Get(repo2.ListIssueComments).
-								Post(bind(api.CreateIssueCommentOption{}), repo2.CreateIssueComment)
+								Get(repo.ListIssueComments).
+								Post(bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
 							m.Combo("/:id").
-								Patch(bind(api.EditIssueCommentOption{}), repo2.EditIssueComment).
-								Delete(repo2.DeleteIssueComment)
+								Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
+								Delete(repo.DeleteIssueComment)
 						})
 
-						m.Get("/labels", repo2.ListIssueLabels)
+						m.Get("/labels", repo.ListIssueLabels)
 						m.Group("/labels", func() {
 							m.Combo("").
-								Post(bind(api.IssueLabelsOption{}), repo2.AddIssueLabels).
-								Put(bind(api.IssueLabelsOption{}), repo2.ReplaceIssueLabels).
-								Delete(repo2.ClearIssueLabels)
-							m.Delete("/:id", repo2.DeleteIssueLabel)
+								Post(bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
+								Put(bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
+								Delete(repo.ClearIssueLabels)
+							m.Delete("/:id", repo.DeleteIssueLabel)
 						}, reqRepoWriter())
 					})
 				}, mustEnableIssues)
 
 				m.Group("/labels", func() {
-					m.Get("", repo2.ListLabels)
-					m.Get("/:id", repo2.GetLabel)
+					m.Get("", repo.ListLabels)
+					m.Get("/:id", repo.GetLabel)
 				})
 				m.Group("/labels", func() {
-					m.Post("", bind(api.CreateLabelOption{}), repo2.CreateLabel)
+					m.Post("", bind(api.CreateLabelOption{}), repo.CreateLabel)
 					m.Combo("/:id").
-						Patch(bind(api.EditLabelOption{}), repo2.EditLabel).
-						Delete(repo2.DeleteLabel)
+						Patch(bind(api.EditLabelOption{}), repo.EditLabel).
+						Delete(repo.DeleteLabel)
 				}, reqRepoWriter())
 
 				m.Group("/milestones", func() {
-					m.Get("", repo2.ListMilestones)
-					m.Get("/:id", repo2.GetMilestone)
+					m.Get("", repo.ListMilestones)
+					m.Get("/:id", repo.GetMilestone)
 				})
 				m.Group("/milestones", func() {
-					m.Post("", bind(api.CreateMilestoneOption{}), repo2.CreateMilestone)
+					m.Post("", bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
 					m.Combo("/:id").
-						Patch(bind(api.EditMilestoneOption{}), repo2.EditMilestone).
-						Delete(repo2.DeleteMilestone)
+						Patch(bind(api.EditMilestoneOption{}), repo.EditMilestone).
+						Delete(repo.DeleteMilestone)
 				}, reqRepoWriter())
 
-				m.Patch("/issue-tracker", reqRepoWriter(), bind(api.EditIssueTrackerOption{}), repo2.IssueTracker)
-				m.Post("/mirror-sync", reqRepoWriter(), repo2.MirrorSync)
-				m.Get("/editorconfig/:filename", context.RepoRef(), repo2.GetEditorconfig)
+				m.Patch("/issue-tracker", reqRepoWriter(), bind(api.EditIssueTrackerOption{}), repo.IssueTracker)
+				m.Post("/mirror-sync", reqRepoWriter(), repo.MirrorSync)
+				m.Get("/editorconfig/:filename", context.RepoRef(), repo.GetEditorconfig)
 			}, repoAssignment())
 		}, reqToken())
 
-		m.Get("/issues", reqToken(), repo2.ListUserIssues)
+		m.Get("/issues", reqToken(), repo.ListUserIssues)
 
 		// Organizations
 		m.Combo("/user/orgs", reqToken()).
-			Get(org2.ListMyOrgs).
-			Post(bind(api.CreateOrgOption{}), org2.CreateMyOrg)
+			Get(org.ListMyOrgs).
+			Post(bind(api.CreateOrgOption{}), org.CreateMyOrg)
 
-		m.Get("/users/:username/orgs", org2.ListUserOrgs)
+		m.Get("/users/:username/orgs", org.ListUserOrgs)
 		m.Group("/orgs/:orgname", func() {
 			m.Combo("").
-				Get(org2.Get).
-				Patch(bind(api.EditOrgOption{}), org2.Edit)
-			m.Get("/teams", org2.ListTeams)
+				Get(org.Get).
+				Patch(bind(api.EditOrgOption{}), org.Edit)
+			m.Get("/teams", org.ListTeams)
 		}, orgAssignment(true))
 
 		m.Group("/admin", func() {
 			m.Group("/users", func() {
-				m.Post("", bind(api.CreateUserOption{}), admin2.CreateUser)
+				m.Post("", bind(api.CreateUserOption{}), admin.CreateUser)
 
 				m.Group("/:username", func() {
 					m.Combo("").
-						Patch(bind(api.EditUserOption{}), admin2.EditUser).
-						Delete(admin2.DeleteUser)
-					m.Post("/keys", bind(api.CreateKeyOption{}), admin2.CreatePublicKey)
-					m.Post("/orgs", bind(api.CreateOrgOption{}), admin2.CreateOrg)
-					m.Post("/repos", bind(api.CreateRepoOption{}), admin2.CreateRepo)
+						Patch(bind(api.EditUserOption{}), admin.EditUser).
+						Delete(admin.DeleteUser)
+					m.Post("/keys", bind(api.CreateKeyOption{}), admin.CreatePublicKey)
+					m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
+					m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
 				})
 			})
 
 			m.Group("/orgs/:orgname", func() {
 				m.Group("/teams", func() {
-					m.Post("", orgAssignment(true), bind(api.CreateTeamOption{}), admin2.CreateTeam)
+					m.Post("", orgAssignment(true), bind(api.CreateTeamOption{}), admin.CreateTeam)
 				})
 			})
 
 			m.Group("/teams", func() {
 				m.Group("/:teamid", func() {
 					m.Combo("/members/:username").
-						Put(admin2.AddTeamMember).
-						Delete(admin2.RemoveTeamMember)
+						Put(admin.AddTeamMember).
+						Delete(admin.RemoveTeamMember)
 					m.Combo("/repos/:reponame").
-						Put(admin2.AddTeamRepository).
-						Delete(admin2.RemoveTeamRepository)
+						Put(admin.AddTeamRepository).
+						Delete(admin.RemoveTeamRepository)
 				}, orgAssignment(false, true))
 			})
 		}, reqAdmin())

+ 115 - 136
internal/route/api/v1/repo/contents.go

@@ -7,128 +7,141 @@ package repo
 import (
 	"encoding/base64"
 	"fmt"
+	"path"
+
+	"github.com/gogs/git-module"
+	"github.com/pkg/errors"
 
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/gitutil"
 )
 
-type repoContent struct {
-	Type            string `json:"type"`
-	Target          string `json:"target,omitempty"`
-	SubmoduleGitURL string `json:"submodule_git_url,omitempty"`
-	Encoding        string `json:"encoding,omitempty"`
-	Size            int64  `json:"size"`
-	Name            string `json:"name"`
-	Path            string `json:"path"`
-	Content         string `json:"content,omitempty"`
-	Sha             string `json:"sha"`
-	URL             string `json:"url"`
-	GitURL          string `json:"git_url"`
-	HTMLURL         string `json:"html_url"`
-	DownloadURL     string `json:"download_url"`
-	Links           Links  `json:"_links"`
-}
+func GetContents(c *context.APIContext) {
+	gitRepo, err := git.Open(c.Repo.Repository.RepoPath())
+	if err != nil {
+		c.ServerError("open repository", err)
+		return
+	}
 
-type Links struct {
-	Git  string `json:"git"`
-	Self string `json:"self"`
-	HTML string `json:"html"`
-}
+	ref := c.Query("ref")
+	if ref == "" {
+		ref = c.Repo.Repository.DefaultBranch
+	}
 
-func GetContents(c *context.APIContext) {
-	treeEntry, err := c.Repo.Commit.TreeEntry(c.Repo.TreePath)
+	commit, err := gitRepo.CatFileCommit(ref)
+	if err != nil {
+		c.NotFoundOrServerError("get commit", gitutil.IsErrRevisionNotExist, err)
+		return
+	}
+
+	treePath := c.Params("*")
+	entry, err := commit.TreeEntry(treePath)
 	if err != nil {
 		c.NotFoundOrServerError("get tree entry", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
-	username := c.Params(":username")
-	reponame := c.Params(":reponame")
-
-	// TODO: figure out the best way to do this
-	// :base-url/:username/:project/raw/:refs/:path
-	templateDownloadURL := "%s/%s/%s/raw/%s"
-	// :base-url/repos/:username/:project/contents/:path
-	templateSelfLink := "%s/repos/%s/%s/contents/%s"
-	// :baseurl/repos/:username/:project/git/trees/:sha
-	templateGitURLLink := "%s/repos/%s/%s/trees/%s"
-	// :baseurl/repos/:username/:project/tree/:sha
-	templateHTMLLLink := "%s/repos/%s/%s/tree/%s"
-
-	gitURL := fmt.Sprintf(templateGitURLLink, c.BaseURL, username, reponame, treeEntry.ID().String())
-	htmlURL := fmt.Sprintf(templateHTMLLLink, c.BaseURL, username, reponame, treeEntry.ID().String())
-	selfURL := fmt.Sprintf(templateSelfLink, c.BaseURL, username, reponame, c.Repo.TreePath)
-
-	// TODO(unknwon): Make a treeEntryToRepoContent helper.
-	contents := &repoContent{
-		Size:        treeEntry.Size(),
-		Name:        treeEntry.Name(),
-		Path:        c.Repo.TreePath,
-		Sha:         treeEntry.ID().String(),
-		URL:         selfURL,
-		GitURL:      gitURL,
-		HTMLURL:     htmlURL,
-		DownloadURL: fmt.Sprintf(templateDownloadURL, c.BaseURL, username, reponame, c.Repo.TreePath),
-		Links: Links{
-			Git:  gitURL,
-			Self: selfURL,
-			HTML: htmlURL,
-		},
+
+	type links struct {
+		Git  string `json:"git"`
+		Self string `json:"self"`
+		HTML string `json:"html"`
+	}
+	type repoContent struct {
+		Type            string `json:"type"`
+		Target          string `json:"target,omitempty"`
+		SubmoduleGitURL string `json:"submodule_git_url,omitempty"`
+		Encoding        string `json:"encoding,omitempty"`
+		Size            int64  `json:"size"`
+		Name            string `json:"name"`
+		Path            string `json:"path"`
+		Content         string `json:"content,omitempty"`
+		Sha             string `json:"sha"`
+		URL             string `json:"url"`
+		GitURL          string `json:"git_url"`
+		HTMLURL         string `json:"html_url"`
+		DownloadURL     string `json:"download_url"`
+		Links           links  `json:"_links"`
 	}
 
-	// A tree entry can only be one of the following types:
-	//   1. Tree (directory)
-	//   2. SubModule
-	//   3. SymLink
-	//   4. Blob (file)
-	if treeEntry.IsCommit() {
-		// TODO(unknwon): submoduleURL is not set as current git-module doesn't handle it properly
-		contents.Type = "submodule"
-		c.JSONSuccess(contents)
-		return
+	toRepoContent := func(subpath string, entry *git.TreeEntry) (*repoContent, error) {
+		repoURL := fmt.Sprintf("%s/repos/%s/%s", c.BaseURL, c.Params(":username"), c.Params(":reponame"))
+		selfURL := fmt.Sprintf("%s/contents/%s", repoURL, subpath)
+		htmlURL := fmt.Sprintf("%s/src/%s/%s", c.Repo.Repository.HTMLURL(), ref, entry.Name())
+		downloadURL := fmt.Sprintf("%s/raw/%s/%s", c.Repo.Repository.HTMLURL(), ref, entry.Name())
 
-	} else if treeEntry.IsSymlink() {
-		contents.Type = "symlink"
-		blob, err := c.Repo.Commit.Blob(c.Repo.TreePath)
-		if err != nil {
-			c.ServerError("GetBlobByPath", err)
-			return
-		}
-		p, err := blob.Bytes()
-		if err != nil {
-			c.ServerError("Data", err)
-			return
+		content := &repoContent{
+			Size:        entry.Size(),
+			Name:        entry.Name(),
+			Path:        subpath,
+			Sha:         entry.ID().String(),
+			URL:         selfURL,
+			HTMLURL:     htmlURL,
+			DownloadURL: downloadURL,
+			Links: links{
+				Self: selfURL,
+				HTML: htmlURL,
+			},
 		}
-		contents.Target = string(p)
-		c.JSONSuccess(contents)
-		return
 
-	} else if treeEntry.IsBlob() {
-		blob, err := c.Repo.Commit.Blob(c.Repo.TreePath)
-		if err != nil {
-			c.ServerError("GetBlobByPath", err)
-			return
+		switch {
+		case entry.IsBlob(), entry.IsExec():
+			content.Type = "file"
+			p, err := entry.Blob().Bytes()
+			if err != nil {
+				return nil, errors.Wrap(err, "get blob content")
+			}
+			content.Encoding = "base64"
+			content.Content = base64.StdEncoding.EncodeToString(p)
+			content.GitURL = fmt.Sprintf("%s/git/blobs/%s", repoURL, entry.ID().String())
+
+		case entry.IsTree():
+			content.Type = "dir"
+			content.GitURL = fmt.Sprintf("%s/git/trees/%s", repoURL, entry.ID().String())
+
+		case entry.IsSymlink():
+			content.Type = "symlink"
+			p, err := entry.Blob().Bytes()
+			if err != nil {
+				return nil, errors.Wrap(err, "get blob content")
+			}
+			content.Target = string(p)
+
+		case entry.IsCommit():
+			content.Type = "submodule"
+			mod, err := commit.Submodule(subpath)
+			if err != nil {
+				return nil, errors.Wrap(err, "get submodule")
+			}
+			content.SubmoduleGitURL = mod.URL
+
+		default:
+			panic("unreachable")
 		}
-		p, err := blob.Bytes()
+
+		content.Links.Git = content.GitURL
+
+		return content, nil
+	}
+
+	if !entry.IsTree() {
+		content, err := toRepoContent(treePath, entry)
 		if err != nil {
-			c.ServerError("Data", err)
+			c.Errorf(err, "convert %q to repoContent", treePath)
 			return
 		}
-		contents.Content = base64.StdEncoding.EncodeToString(p)
-		contents.Type = "file"
-		c.JSONSuccess(contents)
+
+		c.JSONSuccess(content)
 		return
 	}
 
-	// TODO: treeEntry.IsExec()
-
-	// treeEntry is a directory
-	dirTree, err := c.Repo.GitRepo.LsTree(treeEntry.ID().String())
+	// The entry is a directory
+	dir, err := gitRepo.LsTree(entry.ID().String())
 	if err != nil {
 		c.NotFoundOrServerError("get tree", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
-	entries, err := dirTree.Entries()
+	entries, err := dir.Entries()
 	if err != nil {
 		c.NotFoundOrServerError("list entries", gitutil.IsErrRevisionNotExist, err)
 		return
@@ -139,50 +152,16 @@ func GetContents(c *context.APIContext) {
 		return
 	}
 
-	var results = make([]*repoContent, 0, len(entries))
+	contents := make([]*repoContent, 0, len(entries))
 	for _, entry := range entries {
-		gitURL := fmt.Sprintf(templateGitURLLink, c.BaseURL, username, reponame, entry.ID().String())
-		htmlURL := fmt.Sprintf(templateHTMLLLink, c.BaseURL, username, reponame, entry.ID().String())
-		selfURL := fmt.Sprintf(templateSelfLink, c.BaseURL, username, reponame, c.Repo.TreePath)
-		var contentType string
-		if entry.IsTree() {
-			contentType = "dir"
-		} else if entry.IsCommit() {
-			// TODO(unknwon): submoduleURL is not set as current git-module doesn't handle it properly
-			contentType = "submodule"
-		} else if entry.IsSymlink() {
-			contentType = "symlink"
-			blob, err := c.Repo.Commit.Blob(c.Repo.TreePath)
-			if err != nil {
-				c.ServerError("GetBlobByPath", err)
-				return
-			}
-			p, err := blob.Bytes()
-			if err != nil {
-				c.ServerError("Data", err)
-				return
-			}
-			contents.Target = string(p)
-		} else {
-			contentType = "file"
+		subpath := path.Join(treePath, entry.Name())
+		content, err := toRepoContent(subpath, entry)
+		if err != nil {
+			c.Errorf(err, "convert %q to repoContent", subpath)
+			return
 		}
 
-		results = append(results, &repoContent{
-			Type:        contentType,
-			Size:        entry.Size(),
-			Name:        entry.Name(),
-			Path:        c.Repo.TreePath,
-			Sha:         entry.ID().String(),
-			URL:         selfURL,
-			GitURL:      gitURL,
-			HTMLURL:     htmlURL,
-			DownloadURL: fmt.Sprintf(templateDownloadURL, c.BaseURL, username, reponame, c.Repo.TreePath),
-			Links: Links{
-				Git:  gitURL,
-				Self: selfURL,
-				HTML: htmlURL,
-			},
-		})
+		contents = append(contents, content)
 	}
-	c.JSONSuccess(results)
+	c.JSONSuccess(contents)
 }