CloudIDE代码阅读功能部分api #58
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 you’re 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 you’re 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 you’re 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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -119,6 +119,9 @@ type swaggerParameterBodies struct {
|
|||
// in:body
|
||||
CreateFileOptions api.CreateFileOptions
|
||||
|
||||
// in:body
|
||||
BatchChangeFileOptions api.BatchChangeFileOptions
|
||||
|
||||
// in:body
|
||||
UpdateFileOptions api.UpdateFileOptions
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue