CloudIDE代码阅读功能部分api #58

Merged
yystopf merged 10 commits from yystopf/gitea-1156:hh_code_read into develop 2022-07-14 14:26:29 +08:00
10 changed files with 1038 additions and 1 deletions

View File

@ -12,6 +12,7 @@ import (
"os"
"os/exec"
"regexp"
"time"
"code.gitea.io/gitea/modules/process"
)
@ -22,6 +23,25 @@ type BlamePart struct {
Lines []string
}
type ApiBlameCommit struct {
ID string `json:"id"`
Author *Signature `json:"author"`
Commiter *Signature `json:"commiter"`
CommitMessage string `json:"commit_message"`
Parents []string `json:"parents"`
AuthoredTime time.Time `json:"authored_time"`
CommittedTime time.Time `json:"committed_time"`
CreatedTime time.Time `json:"created_time"`
}
type ApiBlamePart struct {
Sha string `json:"-"`
Commit *ApiBlameCommit `json:"commit"`
CurrentNumber int `json:"current_number"`
EffectLine int `json:"effect_line"`
Lines []string `json:"lines"`
}
// BlameReader returns part of file blame one by one
type BlameReader struct {
cmd *exec.Cmd
@ -34,6 +54,95 @@ type BlameReader struct {
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
func GetBlameCommit(repo *Repository, sha string) *ApiBlameCommit {
commit, err := repo.GetCommit(sha)
var apiParents []string
for i := 0; i < commit.ParentCount(); i++ {
sha, _ := commit.ParentID(i)
apiParents = append(apiParents, sha.String())
}
if err != nil {
return &ApiBlameCommit{}
} else {
return &ApiBlameCommit{
ID: sha,
Author: commit.Author,
Commiter: commit.Committer,
CommitMessage: commit.CommitMessage,
Parents: apiParents,
AuthoredTime: commit.Author.When,
CommittedTime: commit.Committer.When,
CreatedTime: commit.Committer.When,
}
}
}
func (r *BlameReader) NextApiPart(repo *Repository) (*ApiBlamePart, error) {
var blamePart *ApiBlamePart
reader := r.reader
effectLine := 0
if r.lastSha != nil {
blamePart = &ApiBlamePart{*r.lastSha, GetBlameCommit(repo, *r.lastSha), 0, effectLine, make([]string, 0)}
}
var line []byte
var isPrefix bool
var err error
for err != io.EOF {
line, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
if len(line) == 0 {
// isPrefix will be false
continue
}
lines := shaLineRegex.FindSubmatch(line)
if lines != nil {
sha1 := string(lines[1])
if blamePart == nil {
blamePart = &ApiBlamePart{sha1, GetBlameCommit(repo, sha1), 0, effectLine, make([]string, 0)}
}
if blamePart.Sha != sha1 {
r.lastSha = &sha1
// need to munch to end of line...
for isPrefix {
_, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
}
return blamePart, nil
}
} else if line[0] == '\t' {
code := line[1:]
effectLine += 1
blamePart.Lines = append(blamePart.Lines, string(code))
}
blamePart.EffectLine = effectLine
// need to munch to end of line...
for isPrefix {
_, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
}
}
r.lastSha = nil
return blamePart, nil
}
// NextPart returns next part of blame (sequential code lines with the same commit)
func (r *BlameReader) NextPart() (*BlamePart, error) {
var blamePart *BlamePart

View File

@ -15,6 +15,21 @@ import (
api "code.gitea.io/gitea/modules/structs"
)
func GetBatchFileResponseFromCommit(repo *models.Repository, commit *git.Commit, branch string, treeNames []string) (*api.BatchFileResponse, error) {
fileCommitResponse, _ := GetFileCommitResponse(repo, commit)
verification := GetPayloadCommitVerification(commit)
batchFileResponse := &api.BatchFileResponse{
Commit: fileCommitResponse,
Verification: verification,
}
for _, treeName := range treeNames {
fileContent, _ := GetContents(repo, treeName, branch, false)
batchFileResponse.Contents = append(batchFileResponse.Contents, fileContent)
}
return batchFileResponse, nil
}
// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
func GetFileResponseFromCommit(repo *models.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) {
fileContents, _ := GetContents(repo, treeName, branch, false) // ok if fails, then will be nil

View File

@ -20,11 +20,30 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
stdcharset "golang.org/x/net/html/charset"
"golang.org/x/text/transform"
)
type FileActionType int
const (
ActionTypeCreate FileActionType = iota + 1
ActionTypeUpdate
ActionTypeDelete
)
var fileActionTypes = map[string]FileActionType{
"create": ActionTypeCreate,
"update": ActionTypeUpdate,
"delete": ActionTypeDelete,
}
func ToFileActionType(name string) FileActionType {
return fileActionTypes[name]
}
// IdentityOptions for a person's identity like an author or committer
type IdentityOptions struct {
Name string
@ -54,6 +73,31 @@ type UpdateRepoFileOptions struct {
Signoff bool
}
type ExchangeFileOption struct {
FileChan chan BatchSingleFileOption
StopChan chan bool
ErrChan chan error
}
type BatchSingleFileOption struct {
Content string
TreePath string
FromTreePath string
ActionType FileActionType
}
type BatchUpdateFileOptions struct {
Files []BatchSingleFileOption
LastCommitID string
OldBranch string
NewBranch string
Message string
SHA string
Author *IdentityOptions
Commiter *IdentityOptions
Dates *CommitDateOptions
Signoff bool
}
func detectEncodingAndBOM(entry *git.TreeEntry, repo *models.Repository) (string, bool) {
reader, err := entry.Blob().DataAsync()
if err != nil {
@ -263,7 +307,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
} else if changed {
return nil, models.ErrCommitIDDoesNotMatch{
GivenCommitID: opts.LastCommitID,
CurrentCommitID: opts.LastCommitID,
CurrentCommitID: commit.ID.String(),
}
}
// The file wasn't modified, so we are good to delete it
@ -466,3 +510,403 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
}
return file, nil
}
func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts *BatchUpdateFileOptions, exchange *ExchangeFileOption) (*structs.BatchFileResponse, error) {
var protectedPatterns []glob.Glob
if opts.OldBranch == "" {
opts.OldBranch = repo.DefaultBranch
}
if opts.NewBranch == "" {
opts.NewBranch = opts.OldBranch
}
// oldBranch must exist for this operation
if _, err := repo_module.GetBranch(repo, opts.OldBranch); err != nil {
return nil, err
}
if opts.NewBranch != opts.OldBranch {
existingBranch, err := repo_module.GetBranch(repo, opts.NewBranch)
if existingBranch != nil {
return nil, models.ErrBranchAlreadyExists{
BranchName: opts.NewBranch,
}
}
if err != nil && !git.IsErrBranchNotExist(err) {
return nil, err
}
} else {
protectedBranch, err := repo.GetBranchProtection(opts.OldBranch)
if err != nil {
return nil, err
}
if protectedBranch != nil {
if !protectedBranch.CanUserPush(doer.ID) {
return nil, models.ErrUserCannotCommit{
UserName: doer.LowerName,
}
}
if protectedBranch.RequireSignedCommits {
_, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch)
if err != nil {
if !models.IsErrWontSign(err) {
return nil, err
}
return nil, models.ErrUserCannotCommit{
UserName: doer.LowerName,
}
}
}
protectedPatterns = protectedBranch.GetProtectedFilePatterns()
}
}
message := strings.TrimSpace(opts.Message)
author, commiter := GetAuthorAndCommitterUsers(opts.Author, opts.Commiter, doer)
t, err := NewTemporaryUploadRepository(repo)
if err != nil {
log.Error("%v", err)
}
defer t.Close()
if err := t.Clone(opts.OldBranch); err != nil {
return nil, err
}
if err := t.SetDefaultIndex(); err != nil {
return nil, err
}
// Get the commit of the original branch
commit, err := t.GetBranchCommit(opts.OldBranch)
if err != nil {
return nil, err
}
if opts.LastCommitID == "" {
opts.LastCommitID = commit.ID.String()
} else {
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
if err != nil {
return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %v", err)
}
opts.LastCommitID = lastCommitID.String()
}
var commitHash string
var treeNames []string
for {
select {
case file := <-exchange.FileChan:
for _, pat := range protectedPatterns {
if pat.Match(strings.ToLower(file.TreePath)) {
return nil, models.ErrFilePathProtected{
Path: file.TreePath,
}
}
}
optTreePath := file.TreePath
optFromTreePath := file.FromTreePath
optActionType := file.ActionType
optContent := file.Content
if optTreePath != "" && optFromTreePath == "" {
optFromTreePath = optTreePath
}
treePath := CleanUploadFileName(optTreePath)
if treePath == "" {
return nil, models.ErrFilenameInvalid{
Path: optTreePath,
}
}
fromTreePath := CleanUploadFileName(optFromTreePath)
if fromTreePath == "" && optFromTreePath != "" {
return nil, models.ErrFilenameInvalid{
Path: optFromTreePath,
}
}
if optActionType == ActionTypeDelete {
// Get the files in the index
filesInIndex, err := t.LsFiles(optTreePath)
if err != nil {
return nil, fmt.Errorf("DeleteRepoFile: %v", err)
}
// Find the file we want to delete in the index
inFilelist := false
for _, file := range filesInIndex {
if file == optTreePath {
inFilelist = true
break
}
}
if !inFilelist {
return nil, models.ErrRepoFileDoesNotExist{
Path: optTreePath,
}
}
// Get the entry of treePath and check if the SHA given is the same as the file
entry, err := commit.GetTreeEntryByPath(treePath)
if err != nil {
return nil, err
}
if opts.SHA != "" {
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
if opts.SHA != entry.ID.String() {
return nil, models.ErrSHADoesNotMatch{
Path: treePath,
GivenSHA: opts.SHA,
CurrentSHA: entry.ID.String(),
}
}
} else if opts.LastCommitID != "" {
// If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
// an error, but only if we aren't creating a new branch.
if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch {
// CommitIDs don't match, but we don't want to throw a ErrCommitIDDoesNotMatch unless
// this specific file has been edited since opts.LastCommitID
if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil {
return nil, err
} else if changed {
return nil, models.ErrCommitIDDoesNotMatch{
GivenCommitID: opts.LastCommitID,
CurrentCommitID: opts.LastCommitID,
}
}
// The file wasn't modified, so we are good to delete it
}
} else {
// When deleting a file, a lastCommitID or SHA needs to be given to make sure other commits haven't been
// made. We throw an error if one wasn't provided.
return nil, models.ErrSHAOrCommitIDNotProvided{}
}
// Remove the file from the index
if err := t.RemoveFilesFromIndex(optTreePath); err != nil {
return nil, err
}
} else {
encoding := "UTF-8"
bom := false
executable := false
if optActionType == ActionTypeUpdate {
fromEntry, err := commit.GetTreeEntryByPath(fromTreePath)
if err != nil {
return nil, err
}
if opts.SHA != "" {
if opts.SHA != fromEntry.ID.String() {
return nil, models.ErrSHADoesNotMatch{
Path: optTreePath,
GivenSHA: opts.SHA,
CurrentSHA: fromEntry.ID.String(),
}
}
} else if opts.LastCommitID != "" {
if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch {
if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil {
return nil, err
} else if changed {
return nil, models.ErrCommitIDDoesNotMatch{
GivenCommitID: opts.LastCommitID,
CurrentCommitID: opts.LastCommitID,
}
}
}
} else {
return nil, models.ErrSHAOrCommitIDNotProvided{}
}
encoding, bom = detectEncodingAndBOM(fromEntry, repo)
executable = fromEntry.IsExecutable()
}
treePathParts := strings.Split(treePath, "/")
subTreePath := ""
for index, part := range treePathParts {
subTreePath = path.Join(subTreePath, part)
entry, err := commit.GetTreeEntryByPath(subTreePath)
if err != nil {
if git.IsErrNotExist(err) {
break
}
return nil, err
}
if index < len(treePathParts)-1 {
if !entry.IsDir() {
return nil, models.ErrFilePathInvalid{
Message: fmt.Sprintf("a file exists where youre trying to create a subdirectory [path: %s]", subTreePath),
Path: subTreePath,
Name: part,
Type: git.EntryModeBlob,
}
}
} else if entry.IsLink() {
return nil, models.ErrFilePathInvalid{
Message: fmt.Sprintf("a symbolic link exists where youre trying to create a subdirectory [path: %s]", subTreePath),
Path: subTreePath,
Name: part,
Type: git.EntryModeSymlink,
}
} else if entry.IsDir() {
return nil, models.ErrFilePathInvalid{
Message: fmt.Sprintf("a directory exists where youre trying to create a file [path: %s]", subTreePath),
Path: subTreePath,
Name: part,
Type: git.EntryModeTree,
}
} else if fromTreePath != treePath || optActionType == ActionTypeCreate {
return nil, models.ErrRepoFileAlreadyExists{
Path: treePath,
}
}
}
// Get the two paths (might be the same if not moving) from the index if they exist
filesInIndex, err := t.LsFiles(optTreePath, optFromTreePath)
if err != nil {
return nil, fmt.Errorf("UpdateRepoFile: %v", err)
}
if optActionType == ActionTypeCreate {
for _, file := range filesInIndex {
if file == optTreePath {
return nil, models.ErrRepoFileAlreadyExists{
Path: optTreePath,
}
}
}
}
// Remove the old path from the tree
if fromTreePath != treePath && len(filesInIndex) > 0 {
for _, file := range filesInIndex {
if file == fromTreePath {
if err := t.RemoveFilesFromIndex(optFromTreePath); err != nil {
return nil, err
}
}
}
}
content := optContent
if bom {
content = string(charset.UTF8BOM) + content
}
if encoding != "UTF-8" {
charsetEncoding, _ := stdcharset.Lookup(encoding)
if charsetEncoding != nil {
result, _, err := transform.String(charsetEncoding.NewEncoder(), content)
if err != nil {
log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", optTreePath, optFromTreePath, encoding, err)
result = content
}
content = result
} else {
log.Error("Unknown encoding: %s", encoding)
}
}
optContent = content
var lfsMetaObject *models.LFSMetaObject
if setting.LFS.StartServer {
filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{
Attributes: []string{"filter"},
Filenames: []string{treePath},
})
if err != nil {
return nil, err
}
if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" {
pointer, err := lfs.GeneratePointer(strings.NewReader(optContent))
if err != nil {
return nil, err
}
lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID}
content = pointer.StringContent()
}
}
objectHash, err := t.HashObject(strings.NewReader(content))
if err != nil {
return nil, err
}
if executable {
if err := t.AddObjectToIndex("100755", objectHash, treePath); err != nil {
return nil, err
}
} else {
if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
return nil, err
}
}
if lfsMetaObject != nil {
lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject)
if err != nil {
return nil, err
}
contentStore := lfs.NewContentStore()
exist, err := contentStore.Exists(lfsMetaObject.Pointer)
if err != nil {
return nil, err
}
if !exist {
if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(optContent)); err != nil {
if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil {
return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err)
}
return nil, err
}
}
}
}
opts.Files = append(opts.Files, file)
treeNames = append(treeNames, file.TreePath)
case err := <-exchange.ErrChan:
return nil, err
case _ = <-exchange.StopChan:
goto end
}
}
end:
// Now write the tree
treeHash, err := t.WriteTree()
if err != nil {
return nil, err
}
// Now commit the tree
if opts.Dates != nil {
commitHash, err = t.CommitTreeWithDate(author, commiter, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
} else {
commitHash, err = t.CommitTree(author, commiter, treeHash, message, opts.Signoff)
}
if err != nil {
return nil, err
}
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
log.Error("%T %v", err, err)
return nil, err
}
commit, err = t.GetCommit(commitHash)
if err != nil {
return nil, err
}
file, err := GetBatchFileResponseFromCommit(repo, commit, opts.NewBranch, treeNames)
if err != nil {
return nil, err
}
return file, nil
}

View File

@ -30,6 +30,19 @@ type CreateFileOptions struct {
Content string `json:"content"`
}
// BatchCreateFileOptions options for creating more files
type BatchChangeFileOptions struct {
Header FileOptions `json:"header"`
Files []struct {
// enum: text,base64
Encoding string `json:"encoding"`
FilePath string `json:"file_path"`
Content string `json:"content"`
// enum: create,update,delete
ActionType string `json:"action_type"`
} `json:"files"`
}
// 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 {
@ -106,6 +119,12 @@ type FileResponse struct {
Verification *PayloadCommitVerification `json:"verification"`
}
type BatchFileResponse struct {
Contents []*ContentsResponse `json:"contents"`
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

View File

@ -1032,6 +1032,9 @@ func Routes() *web.Route {
m.Put("", bind(api.UpdateFileOptions{}), repo.UpdateFile)
m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
}, reqRepoWriter(models.UnitTypeCode), reqToken())
m.Group("/batch", func() {
m.Post("", bind(api.BatchChangeFileOptions{}), repo.BatchChangeFile)
}, reqRepoWriter(models.UnitTypeCode), reqToken())
}, reqRepoReader(models.UnitTypeCode))
m.Get("/signing-key.gpg", misc.SigningKey)
m.Group("/topics", func() {
@ -1045,6 +1048,7 @@ func Routes() *web.Route {
m.Get("/issue_templates", context.ReferencesGitRepo(false), repo.GetIssueTemplates)
m.Get("/languages", reqRepoReader(models.UnitTypeCode), repo.GetLanguages)
m.Get("/diffs", context.RepoRef(), repo.GetRepoDiffs)
m.Get("/blame", context.RepoRef(), repo.GetRepoRefBlame)
}, repoAssignment())
})

View File

@ -0,0 +1,115 @@
package repo
import (
"fmt"
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
)
type APIBlameResponse struct {
FileSize int64 `json:"file_size"`
FileName string `json:"file_name"`
NumberLines int `json:"num_lines"`
BlameParts []git.ApiBlamePart `json:"blame_parts"`
}
func GetRepoRefBlame(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/blame repository repoGetRefBlame
// ---
// summary: Get blame from a repository by sha and filepath***
// 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: sha
// in: query
// description: repo commit sha or branch
// type: string
// required: true
// - name: filepath
// in: query
// description: filepath in repository
// type: string
// required: true
// responses:
// 200:
// description: success
// "404":
// "$ref": "#/responses/notFound"
if ctx.Repo.Repository.IsEmpty {
ctx.NotFound()
return
}
var commit *git.Commit
if sha := ctx.QueryTrim("sha"); len(sha) > 0 {
var err error
commit, err = ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
}
return
}
}
fmt.Println(commit)
filepath := ctx.QueryTrim("filepath")
fmt.Println(filepath)
entry, err := commit.GetTreeEntryByPath(filepath)
if err != nil {
ctx.NotFoundOrServerError("commit.GetTreeEntryByPath", git.IsErrNotExist, err)
return
}
blob := entry.Blob()
numLines, err := blob.GetBlobLineCount()
if err != nil {
ctx.NotFound("GetBlobLineCount", err)
return
}
blameReader, err := git.CreateBlameReader(ctx, models.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name), commit.ID.String(), filepath)
if err != nil {
ctx.NotFound("CreateBlameReader", err)
return
}
defer blameReader.Close()
blameParts := make([]git.ApiBlamePart, 0)
currentNumber := 1
for {
blamePart, err := blameReader.NextApiPart(ctx.Repo.GitRepo)
if err != nil {
ctx.NotFound("NextPart", err)
return
}
if blamePart == nil {
break
}
blamePart.CurrentNumber = currentNumber
blameParts = append(blameParts, *blamePart)
currentNumber += blamePart.EffectLine
}
ctx.JSON(http.StatusOK, APIBlameResponse{
FileSize: blob.Size(),
FileName: blob.Name(),
NumberLines: numLines,
BlameParts: blameParts,
})
}

View File

@ -539,7 +539,34 @@ func GetFileAllCommits(ctx *context.APIContext) {
}
// 获取 commit diff
// Diff get diffs by commit on a repository
func Diff(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/diff repository repoGetDiffs***
// ---
// summary: Get diffs by commit from a repository
// 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: sha
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// 200:
// description: success
// "404":
// "$ref": "#/responses/notFound"
commitID := ctx.Params(":sha")

View File

@ -280,11 +280,76 @@ func CreateFile(ctx *context.APIContext) {
if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
handleCreateOrUpdateFileError(ctx, err)
return
} else {
ctx.JSON(http.StatusCreated, fileResponse)
}
}
// BatchChangeFile handles API call for change some files***
func BatchChangeFile(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/contents/batch repository repoBatchChangeFile
// ---
// summary: Change some files in a repository***
// consumes:
// - application/json
// 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: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/BatchChangeFileOptions"
// responses:
// "201":
// "$ref": "#/responses/BatchFileResponse"
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
apiBatchOpts := web.GetForm(ctx).(*api.BatchChangeFileOptions)
fmt.Println(apiBatchOpts)
if ctx.Repo.Repository.IsEmpty {
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
}
if apiBatchOpts.Header.BranchName == "" {
apiBatchOpts.Header.BranchName = ctx.Repo.Repository.DefaultBranch
}
if apiBatchOpts.Header.Message == "" {
apiBatchOpts.Header.Message = time.Now().Format("RFC3339")
}
if apiBatchOpts.Header.Dates.Author.IsZero() {
apiBatchOpts.Header.Dates.Author = time.Now()
}
if apiBatchOpts.Header.Dates.Committer.IsZero() {
apiBatchOpts.Header.Dates.Committer = time.Now()
}
if batchFileResponse, err := createOrUpdateOrDeleteFiles(ctx, apiBatchOpts); err != nil {
handleCreateOrUpdateFileError(ctx, err)
} else {
ctx.JSON(http.StatusOK, batchFileResponse)
}
}
// UpdateFile handles API call for updating a file
func UpdateFile(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
@ -392,6 +457,59 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
}
func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *api.BatchChangeFileOptions) (*api.BatchFileResponse, error) {
if !canWriteFiles(ctx.Repo) {
return nil, models.ErrUserDoesNotHaveAccessToRepo{
UserID: ctx.User.ID,
RepoName: ctx.Repo.Repository.LowerName,
}
}
fileChan := make(chan repofiles.BatchSingleFileOption)
stopChan := make(chan bool)
errChan := make(chan error)
exchangeOption := &repofiles.ExchangeFileOption{
FileChan: fileChan,
StopChan: stopChan,
ErrChan: errChan,
}
go func() {
for _, f := range apiBatchOpts.Files {
if f.Encoding == "base64" {
content, err := base64.StdEncoding.DecodeString(f.Content)
exchangeOption.ErrChan <- err
f.Content = string(content)
}
exchangeOption.FileChan <- repofiles.BatchSingleFileOption{
Content: f.Content,
TreePath: f.FilePath,
ActionType: repofiles.ToFileActionType(f.ActionType),
}
}
exchangeOption.StopChan <- true
}()
opts := &repofiles.BatchUpdateFileOptions{
Message: apiBatchOpts.Header.Message,
OldBranch: apiBatchOpts.Header.BranchName,
NewBranch: apiBatchOpts.Header.NewBranchName,
Commiter: &repofiles.IdentityOptions{
Name: apiBatchOpts.Header.Committer.Name,
Email: apiBatchOpts.Header.Committer.Email,
},
Author: &repofiles.IdentityOptions{
Name: apiBatchOpts.Header.Author.Name,
Email: apiBatchOpts.Header.Author.Email,
},
Dates: &repofiles.CommitDateOptions{
Author: apiBatchOpts.Header.Dates.Author,
Committer: apiBatchOpts.Header.Dates.Committer,
},
Signoff: apiBatchOpts.Header.Signoff,
}
return repofiles.CreateOrUpdateOrDeleteRepofiles(ctx.Repo.Repository, ctx.User, opts, exchangeOption)
}
// Called from both CreateFile or UpdateFile to handle both
func createOrUpdateFile(ctx *context.APIContext, opts *repofiles.UpdateRepoFileOptions) (*api.FileResponse, error) {
if !canWriteFiles(ctx.Repo) {

View File

@ -119,6 +119,9 @@ type swaggerParameterBodies struct {
// in:body
CreateFileOptions api.CreateFileOptions
// in:body
BatchChangeFileOptions api.BatchChangeFileOptions
// in:body
UpdateFileOptions api.UpdateFileOptions

View File

@ -2424,6 +2424,56 @@
}
}
},
"/repos/{owner}/{repo}/blame": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get blame from a repository by sha and filepath***",
"operationId": "repoGetRefBlame",
"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": "repo commit sha or branch",
"name": "sha",
"in": "query",
"required": true
},
{
"type": "string",
"description": "filepath in repository",
"name": "filepath",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "success"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/branch_name_set": {
"get": {
"produces": [
@ -3419,6 +3469,59 @@
}
}
},
"/repos/{owner}/{repo}/contents/batch": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Change some files in a repository***",
"operationId": "repoBatchChangeFile",
"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
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/BatchChangeFileOptions"
}
}
],
"responses": {
"201": {
"$ref": "#/responses/BatchFileResponse"
},
"403": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/error"
}
}
}
},
"/repos/{owner}/{repo}/contents/{filepath}": {
"get": {
"produces": [
@ -13414,6 +13517,50 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"BatchChangeFileOptions": {
"description": "BatchCreateFileOptions options for creating more files",
"type": "object",
"properties": {
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"action_type": {
"type": "string",
"enum": [
"create",
"update",
"delete"
],
"x-go-name": "ActionType"
},
"content": {
"type": "string",
"x-go-name": "Content"
},
"encoding": {
"type": "string",
"enum": [
"text",
"base64"
],
"x-go-name": "Encoding"
},
"file_path": {
"type": "string",
"x-go-name": "FilePath"
}
}
},
"x-go-name": "Files"
},
"header": {
"$ref": "#/definitions/FileOptions"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"Branch": {
"description": "Branch represents a repository branch",
"type": "object",
@ -16064,6 +16211,42 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"FileOptions": {
"description": "FileOptions options for all file APIs",
"type": "object",
"properties": {
"author": {
"$ref": "#/definitions/Identity"
},
"branch": {
"description": "branch (optional) to base this file from. if not given, the default branch is used",
"type": "string",
"x-go-name": "BranchName"
},
"committer": {
"$ref": "#/definitions/Identity"
},
"dates": {
"$ref": "#/definitions/CommitDateOptions"
},
"message": {
"description": "message (optional) for the commit of this file. if not supplied, a default message will be used",
"type": "string",
"x-go-name": "Message"
},
"new_branch": {
"description": "new_branch (optional) will make a new branch from `branch` before creating the file",
"type": "string",
"x-go-name": "NewBranchName"
},
"signoff": {
"description": "Add a Signed-off-by trailer by the committer at the end of the commit log message.",
"type": "boolean",
"x-go-name": "Signoff"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"FileResponse": {
"description": "FileResponse contains information about a repo's file",
"type": "object",