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..22cb4e4 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiCommits.scala @@ -0,0 +1,156 @@ +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._ +import difflib.{Delta, DiffUtils} + +import scala.collection.JavaConverters._ + +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, + verification: Verification + ) + + case class Tree( + url: ApiPath, + tree: String + ) + + case class Verification( + verified: Boolean, + reason: String //, + // signature: String, + // payload: 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 + var changes = 0 + + diff.changeType match { + case ChangeType.ADD => { + additions = additions + diff.newContent.getOrElse("").replace("\r\n", "\n").split("\n").size + } + case ChangeType.MODIFY => { + val oldLines = diff.oldContent.getOrElse("").replace("\r\n", "\n").split("\n") + val newLines = diff.newContent.getOrElse("").replace("\r\n", "\n").split("\n") + val patch = DiffUtils.diff(oldLines.toList.asJava, newLines.toList.asJava) + patch.getDeltas.asScala.map { delta => + additions = additions + delta.getRevised.getLines.size + deletions = deletions + delta.getOriginal.getLines.size + } + } + case ChangeType.DELETE => { + deletions = deletions + diff.oldContent.getOrElse("").replace("\r\n", "\n").split("\n").size + } + } + + File( + filename = if(diff.changeType == ChangeType.DELETE){ diff.oldPath } else { diff.newPath }, + additions = additions, + deletions = deletions, + changes = changes, + 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 = "" // TODO + ) + } + + 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. + tree = commitInfo.id + ), + verification = Verification( // TODO + verified = false, + reason = "" //, +// signature = "", +// payload = "" + ) + ), + 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. + tree = 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..4613119 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -33,6 +33,12 @@ FieldSerializer[ApiComment]() + FieldSerializer[ApiContents]() + FieldSerializer[ApiLabel]() + + FieldSerializer[ApiCommits]() + + FieldSerializer[ApiCommits.Commit]() + + FieldSerializer[ApiCommits.Tree]() + + FieldSerializer[ApiCommits.Verification]() + + 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 42cffb3..04e60de 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -10,9 +10,11 @@ import gitbucket.core.util.JGitUtil._ import gitbucket.core.util._ import gitbucket.core.util.Implicits._ -import gitbucket.core.view.helpers.{renderMarkup, isRenderable} +import gitbucket.core.view.helpers.{isRenderable, renderMarkup} import org.eclipse.jgit.api.Git -import org.scalatra.{NoContent, UnprocessableEntity, Created} +import org.eclipse.jgit.revwalk.RevWalk +import org.scalatra.{Created, NoContent, UnprocessableEntity} + import scala.collection.JavaConverters._ class ApiController extends ApiControllerBase @@ -49,6 +51,7 @@ with LabelsService with MilestonesService with PullRequestService + with CommitsService with CommitStatusService with RepositoryCreationService with IssueCreationService @@ -627,6 +630,50 @@ }) getOrElse NotFound() }) + + 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, 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