diff --git a/src/main/scala/gitbucket/core/api/ApiBranch.scala b/src/main/scala/gitbucket/core/api/ApiBranch.scala index 0a27850..faee455 100644 --- a/src/main/scala/gitbucket/core/api/ApiBranch.scala +++ b/src/main/scala/gitbucket/core/api/ApiBranch.scala @@ -8,7 +8,7 @@ */ case class ApiBranch( name: String, - // commit: ApiBranchCommit, + commit: ApiBranchCommit, protection: ApiBranchProtection)(repositoryName:RepositoryName) extends FieldSerializable { def _links = Map( "self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"), diff --git a/src/main/scala/gitbucket/core/api/ApiContents.scala b/src/main/scala/gitbucket/core/api/ApiContents.scala index 1582370..1d5753b 100644 --- a/src/main/scala/gitbucket/core/api/ApiContents.scala +++ b/src/main/scala/gitbucket/core/api/ApiContents.scala @@ -1,18 +1,27 @@ package gitbucket.core.api import gitbucket.core.util.JGitUtil.FileInfo +import gitbucket.core.util.RepositoryName import org.apache.commons.codec.binary.Base64 -case class ApiContents(`type`: String, name: String, content: Option[String], encoding: Option[String]) +case class ApiContents( + `type`: String, + name: String, + path: String, + sha: String, + content: Option[String], + encoding: Option[String])(repositoryName: RepositoryName){ + val download_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/raw/${sha}/${path}") +} object ApiContents{ - def apply(fileInfo: FileInfo, content: Option[Array[Byte]]): ApiContents = { + def apply(fileInfo: FileInfo, repositoryName: RepositoryName, content: Option[Array[Byte]]): ApiContents = { if(fileInfo.isDirectory) { - ApiContents("dir", fileInfo.name, None, None) + ApiContents("dir", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName) } else { content.map(arr => - ApiContents("file", fileInfo.name, Some(Base64.encodeBase64String(arr)), Some("base64")) - ).getOrElse(ApiContents("file", fileInfo.name, None, None)) + ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, Some(Base64.encodeBase64String(arr)), Some("base64"))(repositoryName) + ).getOrElse(ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName)) } } } diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index 611db3b..5a831ef 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -31,6 +31,7 @@ FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]() + + FieldSerializer[ApiContents]() + FieldSerializer[ApiLabel]() + ApiBranchProtection.enforcementLevelSerializer diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 794b451..872886e 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -84,9 +84,10 @@ /** * https://developer.github.com/v3/users/#get-a-single-user + * This API also returns group information (as GitHub). */ get("/api/v3/users/:userName") { - getAccountByUserName(params("userName")).filterNot(account => account.isGroupAccount).map { account => + getAccountByUserName(params("userName")).map { account => JsonFormat(ApiUser(account)) } getOrElse NotFound() } @@ -118,6 +119,20 @@ }) }) + /** + * https://developer.github.com/v3/repos/branches/#get-branch + */ + get ("/api/v3/repos/:owner/:repo/branches/:branch")(referrersOnly { repository => + //import gitbucket.core.api._ + (for{ + branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined + br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch) + } yield { + val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) + JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtection(protection))(RepositoryName(repository))) + }) getOrElse NotFound() + }) + /* * https://developer.github.com/v3/repos/contents/#get-contents */ @@ -169,11 +184,11 @@ ) } case _ => - Some(JsonFormat(ApiContents(f, content))) + Some(JsonFormat(ApiContents(f, RepositoryName(repository), content))) } }).getOrElse(NotFound()) } else { // directory - JsonFormat(fileList.map{f => ApiContents(f, None)}) + JsonFormat(fileList.map{f => ApiContents(f, RepositoryName(repository), None)}) } } }) @@ -274,13 +289,14 @@ (for{ branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) + br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch) } yield { if(protection.enabled){ enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts) } else { disableBranchProtection(repository.owner, repository.name, branch) } - JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) + JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), protection)(RepositoryName(repository))) }) getOrElse NotFound() }) @@ -611,5 +627,19 @@ private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + /** + * non-GitHub compatible API for Jenkins-Plugin + */ + get("/api/v3/repos/:owner/:repo/raw/*")(referrersOnly { repository => + val (id, path) = repository.splitPath(multiParams("splat").head) + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) + + getPathObjectId(git, path, revCommit).map { objectId => + responseRawFile(git, objectId, path, repository) + } getOrElse NotFound() + } + }) + } diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala index 8e42e37..21784c3 100644 --- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -1,26 +1,31 @@ package gitbucket.core.controller +import java.io.FileInputStream + import gitbucket.core.api.ApiError import gitbucket.core.model.Account -import gitbucket.core.service.{AccountService, SystemSettingsService} +import gitbucket.core.service.{AccountService, SystemSettingsService,RepositoryService} import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Directory._ import gitbucket.core.util.Implicits._ import gitbucket.core.util._ - +import gitbucket.core.util.JGitUtil._ import io.github.gitbucket.scalatra.forms._ import org.json4s._ import org.scalatra._ import org.scalatra.i18n._ import org.scalatra.json._ - -import javax.servlet.http.{HttpServletResponse, HttpServletRequest} -import javax.servlet.{FilterChain, ServletResponse, ServletRequest} +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import javax.servlet.{FilterChain, ServletRequest, ServletResponse} import scala.util.Try - import net.coobird.thumbnailator.Thumbnails +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.revwalk.RevCommit +import org.eclipse.jgit.treewalk._ +import org.apache.commons.io.IOUtils /** * Provides generic features for controller implementations. @@ -175,6 +180,49 @@ case _ => Some(parse(request.body)) }).filterNot(_ == JNothing).flatMap(j => Try(j.extract[A]).toOption) } + + protected def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = { + @scala.annotation.tailrec + def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { + case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) + case true => _getPathObjectId(path, walk) + case false => None + } + + using(new TreeWalk(git.getRepository)){ treeWalk => + treeWalk.addTree(revCommit.getTree) + treeWalk.setRecursive(true) + _getPathObjectId(path, treeWalk) + } + } + + protected def responseRawFile(git: Git, objectId: ObjectId, path: String, + repository: RepositoryService.RepositoryInfo): Unit = { + JGitUtil.getObjectLoaderFromId(git, objectId){ loader => + contentType = FileUtil.getMimeType(path) + + if(loader.isLarge){ + response.setContentLength(loader.getSize.toInt) + loader.copyTo(response.outputStream) + } else { + val bytes = loader.getCachedBytes + val text = new String(bytes, "UTF-8") + + val attrs = JGitUtil.getLfsObjects(text) + if(attrs.nonEmpty) { + response.setContentLength(attrs("size").toInt) + val oid = attrs("oid").split(":")(1) + + using(new FileInputStream(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))){ in => + IOUtils.copy(in, response.getOutputStream) + } + } else { + response.setContentLength(loader.getSize.toInt) + response.getOutputStream.write(bytes) + } + } + } + } } /** diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index b414dbd..71f4cf2 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -296,34 +296,6 @@ }.getOrElse(false) } - private def responseRawFile(git: Git, objectId: ObjectId, path: String, - repository: RepositoryService.RepositoryInfo): Unit = { - JGitUtil.getObjectLoaderFromId(git, objectId){ loader => - contentType = FileUtil.getMimeType(path) - - if(loader.isLarge){ - response.setContentLength(loader.getSize.toInt) - loader.copyTo(response.outputStream) - } else { - val bytes = loader.getCachedBytes - val text = new String(bytes, "UTF-8") - - val attrs = JGitUtil.getLfsObjects(text) - if(attrs.nonEmpty) { - response.setContentLength(attrs("size").toInt) - val oid = attrs("oid").split(":")(1) - - using(new FileInputStream(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))){ in => - IOUtils.copy(in, response.getOutputStream) - } - } else { - response.setContentLength(loader.getSize.toInt) - response.getOutputStream.write(bytes) - } - } - } - } - get("/:owner/:repository/blame/*"){ blobRoute.action() } @@ -696,21 +668,6 @@ } } - private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = { - @scala.annotation.tailrec - def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { - case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) - case true => _getPathObjectId(path, walk) - case false => None - } - - using(new TreeWalk(git.getRepository)){ treeWalk => - treeWalk.addTree(revCommit.getTree) - treeWalk.setRecursive(true) - _getPathObjectId(path, treeWalk) - } - } - private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = { val revision = name.stripSuffix(suffix) diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index e551374..009b043 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -50,6 +50,7 @@ * @param id the object id * @param isDirectory whether is it directory * @param name the file (or directory) name + * @param path the file (or directory) complete path * @param message the last commit message * @param commitId the last commit id * @param time the last modified time @@ -57,7 +58,7 @@ * @param mailAddress the committer's mail address * @param linkUrl the url of submodule */ - case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, message: String, commitId: String, + case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, path: String, message: String, commitId: String, time: Date, author: String, mailAddress: String, linkUrl: Option[String]) /** @@ -263,13 +264,13 @@ } } @tailrec - def simplifyPath(tuple: (ObjectId, FileMode, String, Option[String], RevCommit)): (ObjectId, FileMode, String, Option[String], RevCommit) = tuple match { - case (oid, FileMode.TREE, name, _, commit ) => + def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String], RevCommit)): (ObjectId, FileMode, String, String, Option[String], RevCommit) = tuple match { + case (oid, FileMode.TREE, name, path, _, commit ) => (using(new TreeWalk(git.getRepository)) { walk => walk.addTree(oid) // single tree child, or None if(walk.next() && walk.getFileMode(0) == FileMode.TREE){ - Some((walk.getObjectId(0), walk.getFileMode(0), name + "/" + walk.getNameString, None, commit)).filterNot(_ => walk.next()) + Some((walk.getObjectId(0), walk.getFileMode(0), name + "/" + walk.getNameString, path + "/" + walk.getNameString, None, commit)).filterNot(_ => walk.next()) } else { None } @@ -280,14 +281,14 @@ case _ => tuple } - def tupleAdd(tuple:(ObjectId, FileMode, String, Option[String]), rev:RevCommit) = tuple match { - case (oid, fmode, name, opt) => (oid, fmode, name, opt, rev) + def tupleAdd(tuple:(ObjectId, FileMode, String, String, Option[String]), rev:RevCommit) = tuple match { + case (oid, fmode, name, path, opt) => (oid, fmode, name, path, opt, rev) } @tailrec - def findLastCommits(result:List[(ObjectId, FileMode, String, Option[String], RevCommit)], - restList:List[((ObjectId, FileMode, String, Option[String]), Map[RevCommit, RevCommit])], - revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, Option[String], RevCommit)] ={ + def findLastCommits(result:List[(ObjectId, FileMode, String, String, Option[String], RevCommit)], + restList:List[((ObjectId, FileMode, String, String, Option[String]), Map[RevCommit, RevCommit])], + revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, String, Option[String], RevCommit)] ={ if(restList.isEmpty){ result } else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty @@ -327,13 +328,13 @@ } } - var fileList: List[(ObjectId, FileMode, String, Option[String])] = Nil + var fileList: List[(ObjectId, FileMode, String, String, Option[String])] = Nil useTreeWalk(revCommit){ treeWalk => while (treeWalk.next()) { val linkUrl = if (treeWalk.getFileMode(0) == FileMode.GITLINK) { getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url) } else None - fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, linkUrl) + fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, treeWalk.getPathString, linkUrl) } } revWalk.markStart(revCommit) @@ -342,11 +343,12 @@ val nextParentsMap = Option(lastCommit).map(_.getParents.map(_ -> lastCommit).toMap).getOrElse(Map()) findLastCommits(List.empty, fileList.map(a => a -> nextParentsMap), it) .map(simplifyPath) - .map { case (objectId, fileMode, name, linkUrl, commit) => + .map { case (objectId, fileMode, name, path, linkUrl, commit) => FileInfo( objectId, fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, name, + path, getSummaryMessage(commit.getFullMessage, commit.getShortMessage), commit.getName, commit.getAuthorIdent.getWhen,