Browse Source

api: support put content (#7114)

Co-authored-by: Joe Chen <[email protected]>
Mateusz Reszka 2 years ago
parent
commit
742bc36edd
4 changed files with 174 additions and 88 deletions
  1. 1 0
      CHANGELOG.md
  2. 6 2
      internal/context/api.go
  3. 3 1
      internal/route/api/v1/api.go
  4. 164 85
      internal/route/api/v1/repo/contents.go

+ 1 - 0
CHANGELOG.md

@@ -8,6 +8,7 @@ All notable changes to Gogs are documented in this file.
 
 - Support using personal access token in the password field. [#3866](https://github.com/gogs/gogs/issues/3866)
 - An unlisted option is added when create or migrate a repository. Unlisted repositories are public but not being listed for users without direct access in the UI. [#5733](https://github.com/gogs/gogs/issues/5733)
+- New API endpoint `PUT /repos/:owner/:repo/contents/:path` for creating and update repository contents. [#5967](https://github.com/gogs/gogs/issues/5967)
 - New configuration option `[git.timeout] DIFF` for customizing operation timeout of `git diff`. [#6315](https://github.com/gogs/gogs/issues/6315)
 - New configuration option `[server] SSH_SERVER_MACS` for setting list of accepted MACs for connections to builtin SSH server. [#6434](https://github.com/gogs/gogs/issues/6434)
 - Support specifying custom schema for PostgreSQL. [#6695](https://github.com/gogs/gogs/pull/6695)

+ 6 - 2
internal/context/api.go

@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/pkg/errors"
 	"github.com/unknwon/paginater"
 	"gopkg.in/macaron.v1"
 	log "unknwon.dev/clog/v2"
@@ -49,8 +50,11 @@ func (c *APIContext) ErrorStatus(status int, err error) {
 
 // Error renders the 500 response.
 func (c *APIContext) Error(err error, msg string) {
-	log.ErrorDepth(5, "%s: %v", msg, err)
-	c.ErrorStatus(http.StatusInternalServerError, err)
+	log.ErrorDepth(4, "%s: %v", msg, err)
+	c.ErrorStatus(
+		http.StatusInternalServerError,
+		errors.New("Something went wrong, please check the server logs for more information."),
+	)
 }
 
 // Errorf renders the 500 response with formatted message.

+ 3 - 1
internal/route/api/v1/api.go

@@ -273,7 +273,9 @@ func RegisterRoutes(m *macaron.Macaron) {
 				m.Get("/raw/*", context.RepoRef(), repo.GetRawFile)
 				m.Group("/contents", func() {
 					m.Get("", repo.GetContents)
-					m.Get("/*", repo.GetContents)
+					m.Combo("/*").
+						Get(repo.GetContents).
+						Put(bind(repo.PutContentsRequest{}), repo.PutContents)
 				})
 				m.Get("/archive/*", repo.GetArchive)
 				m.Group("/git", func() {

+ 164 - 85
internal/route/api/v1/repo/contents.go

@@ -7,17 +7,103 @@ package repo
 import (
 	"encoding/base64"
 	"fmt"
+	"net/http"
 	"path"
 
 	"github.com/gogs/git-module"
 	"github.com/pkg/errors"
 
 	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/gitutil"
+	"gogs.io/gogs/internal/repoutil"
 )
 
+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"`
+}
+
+func toRepoContent(c *context.APIContext, ref, subpath string, commit *git.Commit, 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", repoutil.HTMLURL(c.Repo.Owner.Name, c.Repo.Repository.Name), ref, entry.Name())
+	downloadURL := fmt.Sprintf("%s/raw/%s/%s", repoutil.HTMLURL(c.Repo.Owner.Name, c.Repo.Repository.Name), ref, entry.Name())
+
+	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,
+		},
+	}
+
+	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")
+	}
+
+	content.Links.Git = content.GitURL
+	return content, nil
+}
+
 func GetContents(c *context.APIContext) {
-	gitRepo, err := git.Open(c.Repo.Repository.RepoPath())
+	repoPath := repoutil.RepositoryPath(c.Params(":username"), c.Params(":reponame"))
+	gitRepo, err := git.Open(repoPath)
 	if err != nil {
 		c.Error(err, "open repository")
 		return
@@ -41,90 +127,8 @@ func GetContents(c *context.APIContext) {
 		return
 	}
 
-	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"`
-	}
-
-	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())
-
-		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,
-			},
-		}
-
-		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")
-		}
-
-		content.Links.Git = content.GitURL
-
-		return content, nil
-	}
-
 	if !entry.IsTree() {
-		content, err := toRepoContent(treePath, entry)
+		content, err := toRepoContent(c, ref, treePath, commit, entry)
 		if err != nil {
 			c.Errorf(err, "convert %q to repoContent", treePath)
 			return
@@ -155,7 +159,7 @@ func GetContents(c *context.APIContext) {
 	contents := make([]*repoContent, 0, len(entries))
 	for _, entry := range entries {
 		subpath := path.Join(treePath, entry.Name())
-		content, err := toRepoContent(subpath, entry)
+		content, err := toRepoContent(c, ref, subpath, commit, entry)
 		if err != nil {
 			c.Errorf(err, "convert %q to repoContent", subpath)
 			return
@@ -165,3 +169,78 @@ func GetContents(c *context.APIContext) {
 	}
 	c.JSONSuccess(contents)
 }
+
+// PutContentsRequest is the API message for creating or updating a file.
+type PutContentsRequest struct {
+	Message string `json:"message" binding:"Required"`
+	Content string `json:"content" binding:"Required"`
+	Branch  string `json:"branch"`
+}
+
+// PUT /repos/:username/:reponame/contents/*
+func PutContents(c *context.APIContext, r PutContentsRequest) {
+	content, err := base64.StdEncoding.DecodeString(r.Content)
+	if err != nil {
+		c.Error(err, "decoding base64")
+		return
+	}
+
+	if r.Branch == "" {
+		r.Branch = c.Repo.Repository.DefaultBranch
+	}
+	treePath := c.Params("*")
+	err = c.Repo.Repository.UpdateRepoFile(
+		c.User,
+		db.UpdateRepoFileOptions{
+			OldBranch:   c.Repo.Repository.DefaultBranch,
+			NewBranch:   r.Branch,
+			OldTreeName: treePath,
+			NewTreeName: treePath,
+			Message:     r.Message,
+			Content:     string(content),
+		},
+	)
+	if err != nil {
+		c.Error(err, "updating repository file")
+		return
+	}
+
+	repoPath := repoutil.RepositoryPath(c.Params(":username"), c.Params(":reponame"))
+	gitRepo, err := git.Open(repoPath)
+	if err != nil {
+		c.Error(err, "open repository")
+		return
+	}
+
+	commit, err := gitRepo.CatFileCommit(r.Branch)
+	if err != nil {
+		c.Error(err, "get file commit")
+		return
+	}
+
+	entry, err := commit.TreeEntry(treePath)
+	if err != nil {
+		c.Error(err, "get tree entry")
+		return
+	}
+
+	apiContent, err := toRepoContent(c, r.Branch, treePath, commit, entry)
+	if err != nil {
+		c.Error(err, "convert to *repoContent")
+		return
+	}
+
+	apiCommit, err := gitCommitToAPICommit(commit, c)
+	if err != nil {
+		c.Error(err, "convert to *api.Commit")
+		return
+	}
+
+	c.JSON(
+		http.StatusCreated,
+		map[string]any{
+			"content": apiContent,
+			"commit":  apiCommit,
+		},
+	)
+}