diff --git a/gitea/repo_commit.go b/gitea/repo_commit.go index e6fad60..82ef3fa 100644 --- a/gitea/repo_commit.go +++ b/gitea/repo_commit.go @@ -8,6 +8,7 @@ package gitea import ( "fmt" "net/url" + "time" ) // Identity for a person's identity like an author or committer @@ -47,6 +48,12 @@ type Commit struct { Parents []*CommitMeta `json:"parents"` } +// CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE +type CommitDateOptions struct { + Author time.Time `json:"author"` + Committer time.Time `json:"committer"` +} + // GetSingleCommit returns a single commit func (c *Client) GetSingleCommit(user, repo, commitID string) (*Commit, error) { commit := new(Commit) diff --git a/gitea/repo_file.go b/gitea/repo_file.go index 69a32a8..b8919f9 100644 --- a/gitea/repo_file.go +++ b/gitea/repo_file.go @@ -6,11 +6,139 @@ package gitea import ( + "bytes" + "encoding/json" "fmt" ) +// FileOptions options for all file APIs +type FileOptions struct { + // message (optional) for the commit of this file. if not supplied, a default message will be used + Message string `json:"message"` + // branch (optional) to base this file from. if not given, the default branch is used + BranchName string `json:"branch"` + // new_branch (optional) will make a new branch from `branch` before creating the file + NewBranchName string `json:"new_branch"` + // `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) + Author Identity `json:"author"` + Committer Identity `json:"committer"` + Dates CommitDateOptions `json:"dates"` +} + +// CreateFileOptions options for creating files +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) +type CreateFileOptions struct { + FileOptions + // content must be base64 encoded + // required: true + Content string `json:"content"` +} + +// DeleteFileOptions options for deleting files (used for other File structs below) +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) +type DeleteFileOptions struct { + FileOptions + // sha is the SHA for the file that already exists + // required: true + SHA string `json:"sha"` +} + +// UpdateFileOptions options for updating files +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) +type UpdateFileOptions struct { + DeleteFileOptions + // content must be base64 encoded + // required: true + Content string `json:"content"` + // from_path (optional) is the path of the original file which will be moved/renamed to the path in the URL + FromPath string `json:"from_path"` +} + +// FileLinksResponse contains the links for a repo's file +type FileLinksResponse struct { + Self *string `json:"self"` + GitURL *string `json:"git"` + HTMLURL *string `json:"html"` +} + +// 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. +type FileCommitResponse struct { + CommitMeta + HTMLURL string `json:"html_url"` + Author *CommitUser `json:"author"` + Committer *CommitUser `json:"committer"` + Parents []*CommitMeta `json:"parents"` + Message string `json:"message"` + Tree *CommitMeta `json:"tree"` +} + +// FileResponse contains information about a repo's file +type FileResponse struct { + Content *ContentsResponse `json:"content"` + Commit *FileCommitResponse `json:"commit"` + Verification *PayloadCommitVerification `json:"verification"` +} + +// FileDeleteResponse contains information about a repo's file that was deleted +type FileDeleteResponse struct { + Content interface{} `json:"content"` // to be set to nil + Commit *FileCommitResponse `json:"commit"` + Verification *PayloadCommitVerification `json:"verification"` +} + // GetFile downloads a file of repository, ref can be branch/tag/commit. // e.g.: ref -> master, tree -> macaron.go(no leading slash) func (c *Client) GetFile(user, repo, ref, tree string) ([]byte, error) { return c.getResponse("GET", fmt.Sprintf("/repos/%s/%s/raw/%s/%s", user, repo, ref, tree), nil, nil) } + +// GetContents get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir +// ref is optional +func (c *Client) GetContents(owner, repo, ref, filepath string) (*ContentsResponse, error) { + cr := new(ContentsResponse) + return cr, c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/contents/%s?ref=%s", owner, repo, filepath, ref), jsonHeader, nil, cr) + +} + +// CreateFile create a file in a repository +func (c *Client) CreateFile(owner, repo, filepath string, opt CreateFileOptions) (*FileResponse, error) { + body, err := json.Marshal(&opt) + if err != nil { + return nil, err + } + fr := new(FileResponse) + return fr, c.getParsedResponse("POST", fmt.Sprintf("/repos/%s/%s/contents/%s", owner, repo, filepath), jsonHeader, bytes.NewReader(body), fr) +} + +// UpdateFile update a file in a repository +func (c *Client) UpdateFile(owner, repo, filepath string, opt UpdateFileOptions) (*FileResponse, error) { + body, err := json.Marshal(&opt) + if err != nil { + return nil, err + } + fr := new(FileResponse) + return fr, c.getParsedResponse("PUT", fmt.Sprintf("/repos/%s/%s/contents/%s", owner, repo, filepath), jsonHeader, bytes.NewReader(body), fr) +} diff --git a/gitea/repo_file_test.go b/gitea/repo_file_test.go new file mode 100644 index 0000000..73dd750 --- /dev/null +++ b/gitea/repo_file_test.go @@ -0,0 +1,53 @@ +// Copyright 2020 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 gitea + +import ( + "encoding/base64" + "log" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFileCreateUpdateGet(t *testing.T) { + log.Println("== TestFileCreateUpdateGet ==") + c := newTestClient() + + repo, err := createTestRepo(t, "ChangeFiles", c) + assert.NoError(t, err) + assert.NotNil(t, repo) + + raw, err := c.GetFile(repo.Owner.UserName, repo.Name, "master", "README.md") + assert.NoError(t, err) + assert.EqualValues(t, "IyBDaGFuZ2VGaWxlcwoKQSB0ZXN0IFJlcG86IENoYW5nZUZpbGVz", base64.StdEncoding.EncodeToString(raw)) + + newFile, err := c.CreateFile(repo.Owner.UserName, repo.Name, "A", CreateFileOptions{ + FileOptions: FileOptions{ + Message: "create file A", + }, + Content: "ZmlsZUEK", + }) + assert.NoError(t, err) + raw, _ = c.GetFile(repo.Owner.UserName, repo.Name, "master", "A") + assert.EqualValues(t, "ZmlsZUEK", base64.StdEncoding.EncodeToString(raw)) + + updatedFile, err := c.UpdateFile(repo.Owner.UserName, repo.Name, "A", UpdateFileOptions{ + DeleteFileOptions: DeleteFileOptions{ + FileOptions: FileOptions{ + Message: "add a new line", + }, + SHA: newFile.Content.SHA, + }, + Content: "ZmlsZUEKCmFuZCBhIG5ldyBsaW5lCg==", + }) + assert.NoError(t, err) + assert.NotNil(t, updatedFile) + + file, err := c.GetContents(repo.Owner.UserName, repo.Name, "master", "A") + assert.NoError(t, err) + assert.EqualValues(t, updatedFile.Content.SHA, file.SHA) + assert.EqualValues(t, &updatedFile.Content.Content, &file.Content) +}