diff --git a/src/main/scala/gitbucket/core/api/ApiCommits.scala b/src/main/scala/gitbucket/core/api/ApiCommits.scala new file mode 100644 index 0000000..dc3e2ad --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiCommits.scala @@ -0,0 +1,124 @@ +package gitbucket.core.api + +import gitbucket.core.model.Account +import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo} +import gitbucket.core.util.RepositoryName +import org.eclipse.jgit.diff.DiffEntry.ChangeType +import ApiCommits._ + +case class ApiCommits( + url: ApiPath, + sha: String, + html_url: ApiPath, + comment_url: ApiPath, + commit: Commit, + author: ApiUser, + committer: ApiUser, + parents: Seq[Tree], + stats: Stats, + files: Seq[File] +) + +object ApiCommits { + case class Commit( + url: ApiPath, + author: ApiPersonIdent, + committer: ApiPersonIdent, + message: String, + comment_count: Int, + tree: Tree + ) + + case class Tree( + url: ApiPath, + sha: String + ) + + case class Stats( + additions: Int, + deletions: Int, + total: Int + ) + + case class File( + filename: String, + additions: Int, + deletions: Int, + changes: Int, + status: String, + raw_url: ApiPath, + blob_url: ApiPath, + patch: String + ) + + + def apply(repositoryName: RepositoryName, commitInfo: CommitInfo, diffs: Seq[DiffInfo], author: Account, committer: Account, + commentCount: Int): ApiCommits = { + val files = diffs.map { diff => + var additions = 0 + var deletions = 0 + + diff.patch.getOrElse("").split("\n").foreach { line => + if(line.startsWith("+")) additions = additions + 1 + if(line.startsWith("-")) deletions = deletions + 1 + } + + File( + filename = if(diff.changeType == ChangeType.DELETE){ diff.oldPath } else { diff.newPath }, + additions = additions, + deletions = deletions, + changes = additions + deletions, + status = diff.changeType match { + case ChangeType.ADD => "added" + case ChangeType.MODIFY => "modified" + case ChangeType.DELETE => "deleted" + case ChangeType.RENAME => "renamed" + case ChangeType.COPY => "copied" + }, + raw_url = if(diff.changeType == ChangeType.DELETE){ + ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.parents.head}/${diff.oldPath}") + } else { + ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.id}/${diff.newPath}") + }, + blob_url = if(diff.changeType == ChangeType.DELETE){ + ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.parents.head}/${diff.oldPath}") + } else { + ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.id}/${diff.newPath}") + }, + patch = diff.patch.getOrElse("") + ) + } + + ApiCommits( + url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"), + sha = commitInfo.id, + html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"), + comment_url = ApiPath(""), + commit = Commit( + url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"), + author = ApiPersonIdent.author(commitInfo), + committer = ApiPersonIdent.committer(commitInfo), + message = commitInfo.shortMessage, + comment_count = commentCount, + tree = Tree( + url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${commitInfo.id}"), // TODO This endpoint has not been implemented yet. + sha = commitInfo.id + ) + ), + author = ApiUser(author), + committer = ApiUser(committer), + parents = commitInfo.parents.map { parent => + Tree( + url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${parent}"), // TODO This endpoint has not been implemented yet. + sha = parent + ) + }, + stats = Stats( + additions = files.map(_.additions).sum, + deletions = files.map(_.deletions).sum, + total = files.map(_.additions).sum + files.map(_.deletions).sum + ), + files = files + ) + } +} diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index 5a831ef..7164cf6 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -33,6 +33,11 @@ FieldSerializer[ApiComment]() + FieldSerializer[ApiContents]() + FieldSerializer[ApiLabel]() + + FieldSerializer[ApiCommits]() + + FieldSerializer[ApiCommits.Commit]() + + FieldSerializer[ApiCommits.Tree]() + + FieldSerializer[ApiCommits.Stats]() + + FieldSerializer[ApiCommits.File]() + ApiBranchProtection.enforcementLevelSerializer def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format => diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index a680508..6360847 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -12,6 +12,7 @@ import gitbucket.core.util._ import gitbucket.core.view.helpers.{isRenderable, renderMarkup} import org.eclipse.jgit.api.Git +import org.eclipse.jgit.revwalk.RevWalk import org.scalatra.{Created, NoContent, UnprocessableEntity} import scala.collection.JavaConverters._ @@ -50,6 +51,7 @@ with LabelsService with MilestonesService with PullRequestService + with CommitsService with CommitStatusService with RepositoryCreationService with IssueCreationService @@ -628,6 +630,52 @@ }) getOrElse NotFound() }) + /** + * https://developer.github.com/v3/repos/commits/#get-a-single-commit + */ + get("/api/v3/repos/:owner/:repo/commits/:sha")(referrersOnly { repository => + val owner = repository.owner + val name = repository.name + val sha = params("sha") + + using(Git.open(getRepositoryDir(owner, name))){ git => + val repo = git.getRepository + val objectId = repo.resolve(sha) + val commitInfo = using(new RevWalk(repo)){ revWalk => + new CommitInfo(revWalk.parseCommit(objectId)) + } + + JsonFormat(ApiCommits( + repositoryName = RepositoryName(repository), + commitInfo = commitInfo, + diffs = JGitUtil.getDiffs(git, commitInfo.parents.head, commitInfo.id, false, true), + author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress), + committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress), + commentCount = getCommitComment(repository.owner, repository.name, sha).size + )) + } + }) + + private def getAccount(userName: String, email: String): Account = { + getAccountByMailAddress(email).getOrElse { + Account( + userName = userName, + fullName = userName, + mailAddress = email, + password = "xxx", + isAdmin = false, + url = None, + registeredDate = new java.util.Date(), + updatedDate = new java.util.Date(), + lastLoginDate = None, + image = None, + isGroupAccount = false, + isRemoved = true, + description = None + ) + } + } + private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index df4af27..b13dde1 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -431,7 +431,7 @@ try { using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit => - JGitUtil.getDiffs(git, id) match { + JGitUtil.getDiffs(git, id, false) match { case (diffs, oldCommitId) => html.commit(id, new JGitUtil.CommitInfo(revCommit), JGitUtil.getBranchesOfCommit(git, revCommit.getName), diff --git a/src/main/scala/gitbucket/core/controller/WikiController.scala b/src/main/scala/gitbucket/core/controller/WikiController.scala index d3f949f..f034602 100644 --- a/src/main/scala/gitbucket/core/controller/WikiController.scala +++ b/src/main/scala/gitbucket/core/controller/WikiController.scala @@ -76,7 +76,7 @@ val Array(from, to) = params("commitId").split("\\.\\.\\.") using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => - html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository, + html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true, false).filter(_.newPath == pageName + ".md"), repository, isEditable(repository), flash.get("info")) } }) @@ -85,7 +85,7 @@ val Array(from, to) = params("commitId").split("\\.\\.\\.") using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => - html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository, + html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true, false), repository, isEditable(repository), flash.get("info")) } }) diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala index a368016..d8d419b 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -230,7 +230,7 @@ helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) } - val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) + val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true, false) (commits, diffs) } diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index fd22630..6d14d2c 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -347,7 +347,7 @@ diffs._1.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") => val action = if(diff.changeType == ChangeType.ADD) "created" else "edited" val fileName = diff.newPath - println(action + " - " + fileName + " - " + commit.id) + //println(action + " - " + fileName + " - " + commit.id) (action, fileName, commit.id) } } diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index 6e68bf5..802f9d1 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -1,5 +1,7 @@ package gitbucket.core.util +import java.io.ByteArrayOutputStream + import gitbucket.core.service.RepositoryService import org.eclipse.jgit.api.Git import Directory._ @@ -22,6 +24,7 @@ import org.cache2k.{Cache2kBuilder, CacheEntry} import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException} +import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter} import org.eclipse.jgit.dircache.DirCacheEntry import org.slf4j.LoggerFactory @@ -114,7 +117,8 @@ newObjectId: Option[String], oldMode: String, newMode: String, - tooLarge: Boolean + tooLarge: Boolean, + patch: Option[String] ) /** @@ -515,9 +519,10 @@ } /** - * Returns the tuple of diff of the given commit and the previous commit id. + * Returns the tuple of diff of the given commit and parent commit ids. + * DiffInfos returned from this method don't include the patch property. */ - def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = { + def getDiffs(git: Git, id: String, fetchContent: Boolean): (List[DiffInfo], Option[String]) = { @scala.annotation.tailrec def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] = i.hasNext match { @@ -538,7 +543,7 @@ } else { commits(1) } - (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName)) + (getDiffs(git, oldCommit.getName, id, fetchContent, false), Some(oldCommit.getName)) } else { // initial commit @@ -551,7 +556,7 @@ buffer.append((if(!fetchContent){ DiffInfo( changeType = ChangeType.ADD, - oldPath = null, + oldPath = "", newPath = treeWalk.getPathString, oldContent = None, newContent = None, @@ -561,12 +566,13 @@ newObjectId = Option(treeWalk.getObjectId(0)).map(_.name), oldMode = treeWalk.getFileMode(0).toString, newMode = treeWalk.getFileMode(0).toString, - tooLarge = false + tooLarge = false, + patch = None ) } else { DiffInfo( changeType = ChangeType.ADD, - oldPath = null, + oldPath = "", newPath = treeWalk.getPathString, oldContent = None, newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray), @@ -576,7 +582,8 @@ newObjectId = Option(treeWalk.getObjectId(0)).map(_.name), oldMode = treeWalk.getFileMode(0).toString, newMode = treeWalk.getFileMode(0).toString, - tooLarge = false + tooLarge = false, + patch = None ) })) } @@ -586,7 +593,7 @@ } } - def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = { + def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean, makePatch: Boolean): List[DiffInfo] = { val reader = git.getRepository.newObjectReader val oldTreeIter = new CanonicalTreeParser oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) @@ -612,7 +619,8 @@ newObjectId = Option(diff.getNewId).map(_.name), oldMode = diff.getOldMode.toString, newMode = diff.getNewMode.toString, - tooLarge = true + tooLarge = true, + patch = None ) } else { val oldIsImage = FileUtil.isImage(diff.getOldPath) @@ -630,7 +638,8 @@ newObjectId = Option(diff.getNewId).map(_.name), oldMode = diff.getOldMode.toString, newMode = diff.getNewMode.toString, - tooLarge = false + tooLarge = false, + patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) ) } else { DiffInfo( @@ -645,13 +654,23 @@ newObjectId = Option(diff.getNewId).map(_.name), oldMode = diff.getOldMode.toString, newMode = diff.getNewMode.toString, - tooLarge = false + tooLarge = false, + patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) ) } } }.toList } + private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = { + val out = new ByteArrayOutputStream() + using(new DiffFormatter(out)){ formatter => + formatter.setRepository(git.getRepository) + formatter.format(diff) + val patch = new String(out.toByteArray) + patch.split("\n").drop(4).mkString("\n") + } + } /** * Returns the list of branch names of the specified commit.