diff --git a/integrations/api_repo_file_create_test.go b/integrations/api_repo_file_create_test.go index ba6c1a56afb4..8bee5d39f4c2 100644 --- a/integrations/api_repo_file_create_test.go +++ b/integrations/api_repo_file_create_test.go @@ -49,14 +49,14 @@ func getCreateFileOptions() api.CreateFileOptions { } } -func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileResponse { +func getExpectedFileResponseForCreate(repoFullName, commitID, treePath string) *api.FileResponse { sha := "a635aa942442ddfdba07468cf9661c08fbdf0ebf" encoding := "base64" content := "VGhpcyBpcyBuZXcgdGV4dA==" - selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master" - htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath - gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha - downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath + selfURL := setting.AppURL + "api/v1/repos/" + repoFullName + "/contents/" + treePath + "?ref=master" + htmlURL := setting.AppURL + repoFullName + "/src/branch/master/" + treePath + gitURL := setting.AppURL + "api/v1/repos/" + repoFullName + "/git/blobs/" + sha + downloadURL := setting.AppURL + repoFullName + "/raw/branch/master/" + treePath return &api.FileResponse{ Content: &api.ContentsResponse{ Name: filepath.Base(treePath), @@ -78,10 +78,10 @@ func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileRespon }, Commit: &api.FileCommitResponse{ CommitMeta: api.CommitMeta{ - URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID, + URL: setting.AppURL + "api/v1/repos/" + repoFullName + "/git/commits/" + commitID, SHA: commitID, }, - HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID, + HTMLURL: setting.AppURL + repoFullName + "/commit/" + commitID, Author: &api.CommitUser{ Identity: api.Identity{ Name: "Anne Doe", @@ -169,7 +169,7 @@ func TestAPICreateFile(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusCreated) gitRepo, _ := git.OpenRepository(repo1.RepoPath()) commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName) - expectedFileResponse := getExpectedFileResponseForCreate(commitID, treePath) + expectedFileResponse := getExpectedFileResponseForCreate("user2/repo1", commitID, treePath) var fileResponse api.FileResponse DecodeJSON(t, resp, &fileResponse) assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) @@ -276,5 +276,29 @@ func TestAPICreateFile(t *testing.T) { url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4) req = NewRequestWithJSON(t, "POST", url, &createFileOptions) session.MakeRequest(t, req, http.StatusForbidden) + + // Test creating a file in an empty repository + doAPICreateRepository(NewAPITestContext(t, "user2", "empty-repo"), true)(t) + createFileOptions = getCreateFileOptions() + fileID++ + treePath = fmt.Sprintf("new/file%d.txt", fileID) + url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, "empty-repo", treePath, token2) + req = NewRequestWithJSON(t, "POST", url, &createFileOptions) + resp = session.MakeRequest(t, req, http.StatusCreated) + emptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "empty-repo"}).(*repo_model.Repository) // public repo + gitRepo, _ := git.OpenRepository(emptyRepo.RepoPath()) + commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName) + expectedFileResponse := getExpectedFileResponseForCreate("user2/empty-repo", commitID, treePath) + DecodeJSON(t, resp, &fileResponse) + assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) + assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) + assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) + assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) + assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) + assert.EqualValues(t, expectedFileResponse.Commit.Author.Date, fileResponse.Commit.Author.Date) + assert.EqualValues(t, expectedFileResponse.Commit.Committer.Email, fileResponse.Commit.Committer.Email) + assert.EqualValues(t, expectedFileResponse.Commit.Committer.Name, fileResponse.Commit.Committer.Name) + assert.EqualValues(t, expectedFileResponse.Commit.Committer.Date, fileResponse.Commit.Committer.Date) + gitRepo.Close() }) } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 37781f79c96c..6b3f653c9528 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -233,9 +233,6 @@ func CreateFile(ctx *context.APIContext) { // "$ref": "#/responses/error" apiOpts := web.GetForm(ctx).(*api.CreateFileOptions) - if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty")) - } if apiOpts.BranchName == "" { apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go index dc932b39c2c2..0107d99e665a 100644 --- a/services/repository/files/cherry_pick.go +++ b/services/repository/files/cherry_pick.go @@ -97,9 +97,9 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod // Now commit the tree var commitHash string if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) + commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) } else { - commitHash, err = t.CommitTree(author, committer, treeHash, message, opts.Signoff) + commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff) } if err != nil { return nil, err diff --git a/services/repository/files/delete.go b/services/repository/files/delete.go index 95d05b520235..781a762d0ff7 100644 --- a/services/repository/files/delete.go +++ b/services/repository/files/delete.go @@ -179,9 +179,9 @@ func DeleteRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user // Now commit the tree var commitHash string if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) + commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) } else { - commitHash, err = t.CommitTree(author, committer, treeHash, message, opts.Signoff) + commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff) } if err != nil { return nil, err diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index 09a8b3ea0c27..904512f3f3c3 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -164,9 +164,9 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user // Now commit the tree var commitHash string if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) + commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) } else { - commitHash, err = t.CommitTree(author, committer, treeHash, message, opts.Signoff) + commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff) } if err != nil { return nil, err diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 2223e1c8fda3..c789ad4445da 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -77,6 +77,19 @@ func (t *TemporaryUploadRepository) Clone(branch string) error { return nil } +// Init the repository +func (t *TemporaryUploadRepository) Init() error { + if err := git.InitRepository(t.ctx, t.basePath, false); err != nil { + return err + } + gitRepo, err := git.OpenRepositoryCtx(t.ctx, t.basePath) + if err != nil { + return err + } + t.gitRepo = gitRepo + return nil +} + // SetDefaultIndex sets the git index to our HEAD func (t *TemporaryUploadRepository) SetDefaultIndex() error { if _, err := git.NewCommand(t.ctx, "read-tree", "HEAD").RunInDir(t.basePath); err != nil { @@ -209,12 +222,12 @@ func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, erro } // CommitTree creates a commit from a given tree for the user with provided message -func (t *TemporaryUploadRepository) CommitTree(author, committer *user_model.User, treeHash, message string, signoff bool) (string, error) { - return t.CommitTreeWithDate(author, committer, treeHash, message, signoff, time.Now(), time.Now()) +func (t *TemporaryUploadRepository) CommitTree(parent string, author, committer *user_model.User, treeHash, message string, signoff bool) (string, error) { + return t.CommitTreeWithDate(parent, author, committer, treeHash, message, signoff, time.Now(), time.Now()) } // CommitTreeWithDate creates a commit from a given tree for the user with provided message -func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *user_model.User, treeHash, message string, signoff bool, authorDate, committerDate time.Time) (string, error) { +func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, committer *user_model.User, treeHash, message string, signoff bool, authorDate, committerDate time.Time) (string, error) { authorSig := author.NewGitSig() committerSig := committer.NewGitSig() @@ -235,11 +248,23 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *user_m _, _ = messageBytes.WriteString(message) _, _ = messageBytes.WriteString("\n") - args := []string{"commit-tree", treeHash, "-p", "HEAD"} + var args []string + if parent != "" { + args = []string{"commit-tree", treeHash, "-p", parent} + } else { + args = []string{"commit-tree", treeHash} + } // Determine if we should sign if git.CheckGitVersionAtLeast("1.7.9") == nil { - sign, keyID, signer, _ := asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), author, t.basePath, "HEAD") + var sign bool + var keyID string + var signer *git.Signature + if parent != "" { + sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), author, t.basePath, parent) + } else { + sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), author) + } if sign { args = append(args, "-S"+keyID) if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 4b8653a7f7a8..2cb40aac47ac 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -141,7 +141,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do defer closer.Close() // oldBranch must exist for this operation - if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil { + if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil && !repo.IsEmpty { return nil, err } @@ -191,118 +191,130 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do log.Error("%v", err) } defer t.Close() + hasOldBranch := true 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 // Couldn't get a commit for the branch - } - - // Assigned LastCommitID in opts if it hasn't been set - if opts.LastCommitID == "" { - opts.LastCommitID = commit.ID.String() - } else { - lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID) - if err != nil { - return nil, fmt.Errorf("DeleteRepoFile: Invalid last commit ID: %v", err) + if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { + return nil, err + } + if err := t.Init(); err != nil { + return nil, err + } + hasOldBranch = false + opts.LastCommitID = "" + } + if hasOldBranch { + if err := t.SetDefaultIndex(); err != nil { + return nil, err } - opts.LastCommitID = lastCommitID.String() - } encoding := "UTF-8" bom := false executable := false - if !opts.IsNewFile { - fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) + if hasOldBranch { + // Get the commit of the original branch + commit, err := t.GetBranchCommit(opts.OldBranch) if err != nil { - return nil, err + return nil, err // Couldn't get a commit for the branch } - 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 != fromEntry.ID.String() { - return nil, models.ErrSHADoesNotMatch{ - Path: treePath, - GivenSHA: opts.SHA, - CurrentSHA: fromEntry.ID.String(), - } + + // Assigned LastCommitID in opts if it hasn't been set + 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) } - } 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 { - 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, + opts.LastCommitID = lastCommitID.String() + + } + + if !opts.IsNewFile { + fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) + 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 != fromEntry.ID.String() { + return nil, models.ErrSHADoesNotMatch{ + Path: treePath, + GivenSHA: opts.SHA, + CurrentSHA: fromEntry.ID.String(), } } - // The file wasn't modified, so we are good to delete it + } 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 { + 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 updating 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{} } - } else { - // When updating 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{} + encoding, bom = detectEncodingAndBOM(fromEntry, repo) + executable = fromEntry.IsExecutable() } - encoding, bom = detectEncodingAndBOM(fromEntry, repo) - executable = fromEntry.IsExecutable() - } - // For the path where this file will be created/updated, we need to make - // sure no parts of the path are existing files or links except for the last - // item in the path which is the file name, and that shouldn't exist IF it is - // a new file OR is being moved to a new path. - 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) { - // Means there is no item with that name, so we're good - break + // For the path where this file will be created/updated, we need to make + // sure no parts of the path are existing files or links except for the last + // item in the path which is the file name, and that shouldn't exist IF it is + // a new file OR is being moved to a new path. + 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) { + // Means there is no item with that name, so we're good + break + } + return nil, err } - return nil, err - } - if index < len(treePathParts)-1 { - if !entry.IsDir() { + 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 file exists where you’re trying to create a subdirectory [path: %s]", subTreePath), + Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath), Path: subTreePath, Name: part, - Type: git.EntryModeBlob, + 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 || opts.IsNewFile { + // The entry shouldn't exist if we are creating new file or moving to a new path + return nil, models.ErrRepoFileAlreadyExists{ + Path: treePath, } } - } 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 || opts.IsNewFile { - // The entry shouldn't exist if we are creating new file or moving to a new path - return nil, models.ErrRepoFileAlreadyExists{ - Path: treePath, - } - } + } } // Get the two paths (might be the same if not moving) from the index if they exist @@ -354,7 +366,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do opts.Content = content var lfsMetaObject *models.LFSMetaObject - if setting.LFS.StartServer { + if setting.LFS.StartServer && hasOldBranch { // Check there is no way this can return multiple infos filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ Attributes: []string{"filter"}, @@ -401,9 +413,9 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do // Now commit the tree var commitHash string if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) + commitHash, err = t.CommitTreeWithDate(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) } else { - commitHash, err = t.CommitTree(author, committer, treeHash, message, opts.Signoff) + commitHash, err = t.CommitTree(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff) } if err != nil { return nil, err @@ -436,7 +448,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do return nil, err } - commit, err = t.GetCommit(commitHash) + commit, err := t.GetCommit(commitHash) if err != nil { return nil, err } diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index 79fca3ead77c..ff0448fc8747 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -123,7 +123,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use committer := doer // Now commit the tree - commitHash, err := t.CommitTree(author, committer, treeHash, opts.Message, opts.Signoff) + commitHash, err := t.CommitTree(opts.LastCommitID, author, committer, treeHash, opts.Message, opts.Signoff) if err != nil { return err }