Fix Repository Contents API issues (#2802)

This commit is contained in:
Naoki Takezoe
2021-06-21 00:55:03 +09:00
committed by GitHub
parent 1004c83bc4
commit e0bab59846
6 changed files with 172 additions and 99 deletions

View File

@@ -28,7 +28,7 @@ object ApiContents {
"file", "file",
fileInfo.name, fileInfo.name,
fileInfo.path, fileInfo.path,
fileInfo.commitId, fileInfo.id.getName,
Some(Base64.getEncoder.encodeToString(arr)), Some(Base64.getEncoder.encodeToString(arr)),
Some("base64") Some("base64")
)(repositoryName) )(repositoryName)

View File

@@ -15,7 +15,6 @@ import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, WebHook} import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.WebHookService.{WebHookCreatePayload, WebHookPushPayload} import gitbucket.core.service.WebHookService.{WebHookCreatePayload, WebHookPushPayload}
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.helpers import gitbucket.core.view.helpers
import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveOutputStream} import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveOutputStream}
@@ -267,26 +266,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
branchName, branchName,
repository, repository,
logs logs
.map { .map { commit =>
commit => (
( commit.copy(verified = commit.commitSign.flatMap(GpgUtil.verifySign)),
CommitInfo( JGitUtil.getTagsOnCommit(git, commit.id),
id = commit.id, getCommitStatusWithSummary(repository.owner, repository.name, commit.id)
shortMessage = commit.shortMessage, )
fullMessage = commit.fullMessage,
parents = commit.parents,
authorTime = commit.authorTime,
authorName = commit.authorName,
authorEmailAddress = commit.authorEmailAddress,
commitTime = commit.commitTime,
committerName = commit.committerName,
committerEmailAddress = commit.committerEmailAddress,
commitSign = commit.commitSign,
verified = commit.commitSign.flatMap(GpgUtil.verifySign)
),
JGitUtil.getTagsOnCommit(git, commit.id),
getCommitStatusWithSummary(repository.owner, repository.name, commit.id)
)
} }
.splitWith { .splitWith {
case ((commit1, _, _), (commit2, _, _)) => case ((commit1, _, _), (commit2, _, _)) =>
@@ -356,11 +341,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val newFiles = files.map { file => val newFiles = files.map { file =>
file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}") file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
}.toSeq }
if (form.newBranch) { if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount) val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, files, newFiles, loginAccount) val objectId = _commit(newBranchName, newFiles, loginAccount)
val issueId = val issueId =
createIssueAndPullRequest( createIssueAndPullRequest(
repository, repository,
@@ -373,7 +358,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
) )
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else { } else {
_commit(form.branch, files, newFiles, loginAccount) _commit(form.branch, newFiles, loginAccount)
if (form.path.length == 0) { if (form.path.length == 0) {
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}") redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}")
} else { } else {
@@ -384,15 +369,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
def _commit( def _commit(
branchName: String, branchName: String,
files: Seq[CommitFile], //files: Seq[CommitFile],
newFiles: Seq[CommitFile], newFiles: Seq[CommitFile],
loginAccount: Account loginAccount: Account
): ObjectId = { ): ObjectId = {
commitFiles( commitFiles(
repository = repository, repository = repository,
branch = branchName, branch = branchName,
path = form.path, //path = form.path,
files = files.toIndexedSeq, //files = files.toIndexedSeq,
message = form.message.getOrElse("Add files via upload"), message = form.message.getOrElse("Add files via upload"),
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
@@ -509,7 +494,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit, commit = form.commit,
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
) )._1
} }
}) })
@@ -556,7 +541,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit, commit = form.commit,
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
) )._1
} }
}) })
@@ -599,7 +584,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit, commit = form.commit,
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
) )._1
} }
}) })

View File

@@ -1,9 +1,9 @@
package gitbucket.core.controller.api package gitbucket.core.controller.api
import gitbucket.core.api.{ApiContents, ApiError, CreateAFile, JsonFormat} import gitbucket.core.api.{ApiCommit, ApiContents, ApiError, CreateAFile, JsonFormat}
import gitbucket.core.controller.ControllerBase import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.{RepositoryCommitFileService, RepositoryService} import gitbucket.core.service.{RepositoryCommitFileService, RepositoryService}
import gitbucket.core.util.Directory.getRepositoryDir import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.JGitUtil.{FileInfo, getContentFromId, getFileList} import gitbucket.core.util.JGitUtil.{CommitInfo, FileInfo, getContentFromId, getFileList}
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.view.helpers.{isRenderable, renderMarkup} import gitbucket.core.view.helpers.{isRenderable, renderMarkup}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
@@ -35,7 +35,7 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
/** /**
* ii. Get contents * ii. Get contents
* https://developer.github.com/v3/repos/contents/#get-contents * https://docs.github.com/en/rest/reference/repos#get-repository-content
*/ */
get("/api/v3/repos/:owner/:repository/contents")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/contents")(referrersOnly { repository =>
getContents(repository, ".", params.getOrElse("ref", repository.repository.defaultBranch)) getContents(repository, ".", params.getOrElse("ref", repository.repository.defaultBranch))
@@ -43,34 +43,34 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
/** /**
* ii. Get contents * ii. Get contents
* https://developer.github.com/v3/repos/contents/#get-contents * https://docs.github.com/en/rest/reference/repos#get-repository-content
*/ */
get("/api/v3/repos/:owner/:repository/contents/*")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/contents/*")(referrersOnly { repository =>
getContents(repository, multiParams("splat").head, params.getOrElse("ref", repository.repository.defaultBranch)) getContents(repository, multiParams("splat").head, params.getOrElse("ref", repository.repository.defaultBranch))
}) })
private def getFileInfo(git: Git, revision: String, pathStr: String, ignoreCase: Boolean): Option[FileInfo] = {
val (dirName, fileName) = pathStr.lastIndexOf('/') match {
case -1 =>
(".", pathStr)
case n =>
(pathStr.take(n), pathStr.drop(n + 1))
}
if (ignoreCase) {
getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles)
.find(_.name.toLowerCase.equals(fileName.toLowerCase))
} else {
getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles)
.find(_.name.equals(fileName))
}
}
private def getContents( private def getContents(
repository: RepositoryService.RepositoryInfo, repository: RepositoryService.RepositoryInfo,
path: String, path: String,
refStr: String, refStr: String,
ignoreCase: Boolean = false ignoreCase: Boolean = false
) = { ) = {
def getFileInfo(git: Git, revision: String, pathStr: String, ignoreCase: Boolean): Option[FileInfo] = {
val (dirName, fileName) = pathStr.lastIndexOf('/') match {
case -1 =>
(".", pathStr)
case n =>
(pathStr.take(n), pathStr.drop(n + 1))
}
if (ignoreCase) {
getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles)
.find(_.name.toLowerCase.equals(fileName.toLowerCase))
} else {
getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles)
.find(_.name.equals(fileName))
}
}
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val fileList = getFileList(git, refStr, path, maxFiles = context.settings.repositoryViewer.maxFiles) val fileList = getFileList(git, refStr, path, maxFiles = context.settings.repositoryViewer.maxFiles)
if (fileList.isEmpty) { // file or NotFound if (fileList.isEmpty) { // file or NotFound
@@ -126,14 +126,13 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
} }
} }
} }
/*
/**
* iii. Create a file or iv. Update a file * iii. Create a file or iv. Update a file
* https://developer.github.com/v3/repos/contents/#create-a-file * https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents
* https://developer.github.com/v3/repos/contents/#update-a-file
* if sha is presented, update a file else create a file. * if sha is presented, update a file else create a file.
* requested #2112 * requested #2112
*/ */
put("/api/v3/repos/:owner/:repository/contents/*")(writableUsersOnly { repository => put("/api/v3/repos/:owner/:repository/contents/*")(writableUsersOnly { repository =>
context.withLoginAccount { context.withLoginAccount {
loginAccount => loginAccount =>
@@ -147,27 +146,53 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
} }
val paths = multiParams("splat").head.split("/") val paths = multiParams("splat").head.split("/")
val path = paths.take(paths.size - 1).toList.mkString("/") val path = paths.take(paths.size - 1).toList.mkString("/")
if (data.sha.isDefined && data.sha.get != commit) { Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) {
ApiError( git =>
"The blob SHA is not matched.", val fileInfo = getFileInfo(git, commit, path, false)
Some("https://developer.github.com/v3/repos/contents/#update-a-file")
) fileInfo match {
} else { case Some(f) if !data.sha.contains(f.id.getName) =>
val objectId = commitFile( ApiError(
repository, "The blob SHA is not matched.",
branch, Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents")
path, )
Some(paths.last), case _ =>
data.sha.map(_ => paths.last), val (commitId, blobId) = commitFile(
StringUtil.base64Decode(data.content), repository,
data.message, branch,
commit, path,
loginAccount, Some(paths.last),
data.committer.map(_.name).getOrElse(loginAccount.fullName), data.sha.map(_ => paths.last),
data.committer.map(_.email).getOrElse(loginAccount.mailAddress), StringUtil.base64Decode(data.content),
context.settings data.message,
) commit,
ApiContents("file", paths.last, path, objectId.name, None, None)(RepositoryName(repository)) loginAccount,
data.committer.map(_.name).getOrElse(loginAccount.fullName),
data.committer.map(_.email).getOrElse(loginAccount.mailAddress),
context.settings
)
blobId match {
case None =>
ApiError("Failed to commit a file.", None)
case Some(blobId) =>
Map(
"content" -> ApiContents(
"file",
paths.last,
path,
blobId.name,
Some(data.content),
Some("base64")
)(RepositoryName(repository)),
"commit" -> ApiCommit(
git,
RepositoryName(repository),
new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
)
)
}
}
} }
}) })
} }
@@ -175,13 +200,14 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
/* /*
* v. Delete a file * v. Delete a file
* https://developer.github.com/v3/repos/contents/#delete-a-file * https://docs.github.com/en/rest/reference/repos#delete-a-file
* should be implemented * should be implemented
*/ */
/* /*
* vi. Get archive link * vi. Download a repository archive (tar/zip)
* https://developer.github.com/v3/repos/contents/#get-archive-link * https://docs.github.com/en/rest/reference/repos#download-a-repository-archive-tar
* https://docs.github.com/en/rest/reference/repos#download-a-repository-archive-zip
*/ */
} }

View File

@@ -23,23 +23,29 @@ trait RepositoryCommitFileService {
with PullRequestService with PullRequestService
with WebHookPullRequestService with WebHookPullRequestService
with RepositoryService => with RepositoryService =>
import RepositoryCommitFileService._
/**
* Create multiple files by callback function.
* Returns commitId.
*/
def commitFiles( def commitFiles(
repository: RepositoryService.RepositoryInfo, repository: RepositoryService.RepositoryInfo,
files: Seq[CommitFile],
branch: String, branch: String,
path: String,
message: String, message: String,
loginAccount: Account, loginAccount: Account,
settings: SystemSettings settings: SystemSettings
)( )(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit
)(implicit s: Session, c: JsonFormat.Context) = { )(implicit s: Session, c: JsonFormat.Context): ObjectId = {
// prepend path to the filename _createFiles(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress, settings)(
_commitFile(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress, settings)(f) f
)._1
} }
/**
* Create a file from string content.
* Returns commitId + blobId.
*/
def commitFile( def commitFile(
repository: RepositoryService.RepositoryInfo, repository: RepositoryService.RepositoryInfo,
branch: String, branch: String,
@@ -52,7 +58,7 @@ trait RepositoryCommitFileService {
commit: String, commit: String,
loginAccount: Account, loginAccount: Account,
settings: SystemSettings settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = { )(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = {
commitFile( commitFile(
repository, repository,
branch, branch,
@@ -69,6 +75,10 @@ trait RepositoryCommitFileService {
) )
} }
/**
* Create a file from byte array content.
* Returns commitId + blobId.
*/
def commitFile( def commitFile(
repository: RepositoryService.RepositoryInfo, repository: RepositoryService.RepositoryInfo,
branch: String, branch: String,
@@ -82,7 +92,7 @@ trait RepositoryCommitFileService {
committerName: String, committerName: String,
committerMailAddress: String, committerMailAddress: String,
settings: SystemSettings settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = { )(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = {
val newPath = newFileName.map { newFileName => val newPath = newFileName.map { newFileName =>
if (path.length == 0) newFileName else s"${path}/${newFileName}" if (path.length == 0) newFileName else s"${path}/${newFileName}"
@@ -91,7 +101,7 @@ trait RepositoryCommitFileService {
if (path.length == 0) oldFileName else s"${path}/${oldFileName}" if (path.length == 0) oldFileName else s"${path}/${oldFileName}"
} }
_commitFile(repository, branch, message, pusherAccount, committerName, committerMailAddress, settings) { _createFiles(repository, branch, message, pusherAccount, committerName, committerMailAddress, settings) {
case (git, headTip, builder, inserter) => case (git, headTip, builder, inserter) =>
if (headTip.getName == commit) { if (headTip.getName == commit) {
val permission = JGitUtil val permission = JGitUtil
@@ -105,18 +115,23 @@ trait RepositoryCommitFileService {
} }
.flatten .flatten
.headOption .headOption
.map { bits =>
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath, permission.map { bits =>
FileMode.fromBits(bits) FileMode.fromBits(bits)
} getOrElse FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content))) }
.getOrElse(FileMode.REGULAR_FILE)
val objectId = newPath.map { newPath =>
val objectId = inserter.insert(Constants.OBJ_BLOB, content)
builder.add(JGitUtil.createDirCacheEntry(newPath, permission, objectId))
objectId
} }
builder.finish() builder.finish()
} objectId
} else None
} }
} }
private def _commitFile( private def _createFiles[R](
repository: RepositoryService.RepositoryInfo, repository: RepositoryService.RepositoryInfo,
branch: String, branch: String,
message: String, message: String,
@@ -125,8 +140,8 @@ trait RepositoryCommitFileService {
committerMailAddress: String, committerMailAddress: String,
settings: SystemSettings settings: SystemSettings
)( )(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
)(implicit s: Session, c: JsonFormat.Context): ObjectId = { )(implicit s: Session, c: JsonFormat.Context): (ObjectId, R) = {
LockUtil.lock(s"${repository.owner}/${repository.name}") { LockUtil.lock(s"${repository.owner}/${repository.name}") {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
@@ -135,7 +150,7 @@ trait RepositoryCommitFileService {
val headName = s"refs/heads/${branch}" val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName) val headTip = git.getRepository.resolve(headName)
f(git, headTip, builder, inserter) val result = f(git, headTip, builder, inserter)
val commitId = JGitUtil.createNewCommit( val commitId = JGitUtil.createNewCommit(
git, git,
@@ -228,7 +243,7 @@ trait RepositoryCommitFileService {
} }
} }
} }
commitId (commitId, result)
} }
} }
} }

View File

@@ -20,7 +20,6 @@ import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
import org.apache.http.client.utils.URLEncodedUtils import org.apache.http.client.utils.URLEncodedUtils
import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{HttpClientUtil, RepositoryName, StringUtil} import gitbucket.core.util.{HttpClientUtil, RepositoryName, StringUtil}
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
import org.apache.http.NameValuePair import org.apache.http.NameValuePair

View File

@@ -1,11 +1,15 @@
package gitbucket.core.api package gitbucket.core.api
import gitbucket.core.TestingGitBucketServer import gitbucket.core.TestingGitBucketServer
import org.apache.commons.io.IOUtils
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import scala.util.Using import scala.util.Using
import org.kohsuke.github.{GHCommitState, GitHub} import org.kohsuke.github.{GHCommitState, GitHub}
/**
* Need to run `sbt package` before running this test.
*/
class ApiIntegrationTest extends AnyFunSuite { class ApiIntegrationTest extends AnyFunSuite {
test("create repository") { test("create repository") {
@@ -133,4 +137,48 @@ class ApiIntegrationTest extends AnyFunSuite {
} }
} }
test("create and update contents") {
Using.resource(new TestingGitBucketServer(19999)) { server =>
val github = server.client("root", "root")
val repo = github.createRepository("create_contents_test").autoInit(true).create()
val createResult =
repo
.createContent()
.branch("master")
.content("create")
.message("Create content")
.path("README.md")
.commit();
assert(createResult.getContent.isFile == true)
assert(IOUtils.toString(createResult.getContent.read(), "UTF-8") == "create")
val content1 = repo.getFileContent("README.md")
assert(content1.isFile == true)
assert(IOUtils.toString(content1.read(), "UTF-8") == "create")
assert(content1.getSha == createResult.getContent.getSha)
val updateResult =
repo
.createContent()
.branch("master")
.content("update")
.message("Update content")
.path("README.md")
.sha(content1.getSha)
.commit();
assert(updateResult.getContent.isFile == true)
assert(IOUtils.toString(updateResult.getContent.read(), "UTF-8") == "update")
val content2 = repo.getFileContent("README.md")
assert(content2.isFile == true)
assert(IOUtils.toString(content2.read(), "UTF-8") == "update")
assert(content2.getSha == updateResult.getContent.getSha)
assert(content1.getSha != content2.getSha)
}
}
} }