gitbucket.core.util.JGitUtil.scala Maven / Gradle / Ivy
The newest version!
package gitbucket.core.util
import java.io._
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
import Directory._
import StringUtil._
import scala.annotation.tailrec
import scala.jdk.CollectionConverters._
import scala.util.Using
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec
import java.util.Date
import java.util.concurrent.TimeUnit
import org.cache2k.{Cache, Cache2kBuilder}
import org.eclipse.jgit.api.errors._
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter, RawTextComparator}
import org.eclipse.jgit.dircache.DirCacheEntry
import org.eclipse.jgit.util.io.DisabledOutputStream
import org.slf4j.LoggerFactory
import scala.util.Using.Releasable
/**
* Provides complex JGit operations.
*/
object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
private implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
_.close()
private def isCacheEnabled(): Boolean =
!ConfigUtil.getConfigValue[Boolean]("gitbucket.disableCache").getOrElse(false)
/**
* The repository data.
*
* @param owner the user name of the repository owner
* @param name the repository name
* @param branchList the list of branch names
* @param tags the list of tags
*/
case class RepositoryInfo(owner: String, name: String, branchList: List[String], tags: List[TagInfo]) {
def this(owner: String, name: String) = this(owner, name, Nil, Nil)
}
/**
* The file data for the file list of the repository viewer.
*
* @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
* @param author the last committer name
* @param mailAddress the committer's mail address
* @param linkUrl the url of submodule
*/
case class FileInfo(
id: ObjectId,
isDirectory: Boolean,
name: String,
path: String,
message: String,
commitId: String,
time: Date,
author: String,
mailAddress: String,
linkUrl: Option[String]
)
/**
* The gpg commit sign data.
* @param signArmored signature for commit
* @param target string for verification target
*/
case class GpgSignInfo(signArmored: Array[Byte], target: Array[Byte])
/**
* The verified gpg sign data.
* @param signedUser
* @param signedKeyId
*/
case class GpgVerifyInfo(signedUser: String, signedKeyId: String)
private def getSignTarget(rev: RevCommit): Array[Byte] = {
val ascii = "ASCII"
val os = new ByteArrayOutputStream()
val w = new OutputStreamWriter(os, rev.getEncoding)
os.write("tree ".getBytes(ascii))
rev.getTree.copyTo(os)
os.write('\n')
rev.getParents.foreach { p =>
os.write("parent ".getBytes(ascii))
p.copyTo(os)
os.write('\n')
}
os.write("author ".getBytes(ascii))
w.write(rev.getAuthorIdent.toExternalString)
w.flush()
os.write('\n')
os.write("committer ".getBytes(ascii))
w.write(rev.getCommitterIdent.toExternalString)
w.flush()
os.write('\n')
if (rev.getEncoding.name != "UTF-8") {
os.write("encoding ".getBytes(ascii))
os.write(Constants.encodeASCII(rev.getEncoding.name))
os.write('\n')
}
os.write('\n')
if (!rev.getFullMessage.isEmpty) {
w.write(rev.getFullMessage)
w.flush()
}
os.toByteArray
}
/**
* The commit data.
*
* @param id the commit id
* @param shortMessage the short message
* @param fullMessage the full message
* @param parents the list of parent commit id
* @param authorTime the author time
* @param authorName the author name
* @param authorEmailAddress the mail address of the author
* @param commitTime the commit time
* @param committerName the committer name
* @param committerEmailAddress the mail address of the committer
*/
case class CommitInfo(
id: String,
shortMessage: String,
fullMessage: String,
parents: List[String],
authorTime: Date,
authorName: String,
authorEmailAddress: String,
commitTime: Date,
committerName: String,
committerEmailAddress: String,
commitSign: Option[GpgSignInfo],
verified: Option[GpgVerifyInfo]
) {
def this(rev: org.eclipse.jgit.revwalk.RevCommit) =
this(
rev.getName,
rev.getShortMessage,
rev.getFullMessage,
rev.getParents().map(_.name).toList,
rev.getAuthorIdent.getWhen,
rev.getAuthorIdent.getName,
rev.getAuthorIdent.getEmailAddress,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress,
Option(rev.getRawGpgSignature).map { s =>
GpgSignInfo(s, getSignTarget(rev))
},
None
)
val summary = getSummaryMessage(fullMessage, shortMessage)
val description = {
val i = fullMessage.trim.indexOf('\n')
if (i >= 0) {
Some(fullMessage.trim.substring(i).trim)
} else None
}
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
}
case class DiffInfo(
changeType: ChangeType,
oldPath: String,
newPath: String,
oldContent: Option[String],
newContent: Option[String],
oldIsImage: Boolean,
newIsImage: Boolean,
oldObjectId: Option[String],
newObjectId: Option[String],
oldMode: String,
newMode: String,
tooLarge: Boolean,
patch: Option[String]
)
/**
* The file content data for the file content view of the repository viewer.
*
* @param viewType "image", "large" or "other"
* @param size total size of object in bytes
* @param content the string content
* @param charset the character encoding
*/
case class ContentInfo(viewType: String, size: Option[Long], content: Option[String], charset: Option[String]) {
/**
* the line separator of this content ("LF" or "CRLF")
*/
val lineSeparator: String = if (content.exists(_.indexOf("\r\n") >= 0)) "CRLF" else "LF"
}
/**
* The tag data.
*
* @param name the tag name
* @param time the tagged date
* @param commitId the commit id
* @param message the message of the tagged commit
* @param objectId the tag object id
*/
case class TagInfo(name: String, time: Date, commitId: String, message: String, objectId: String)
/**
* The submodule data
*
* @param name the module name
* @param path the path in the repository
* @param repositoryUrl the repository url of this module
* @param viewerUrl the repository viewer url of this module
*/
case class SubmoduleInfo(name: String, path: String, repositoryUrl: String, viewerUrl: String)
case class BranchMergeInfo(ahead: Int, behind: Int, isMerged: Boolean)
case class BranchInfoSimple(
name: String,
committerName: String,
commitTime: Date,
committerEmailAddress: String,
commitId: String
)
case class BranchInfo(
name: String,
committerName: String,
commitTime: Date,
committerEmailAddress: String,
mergeInfo: Option[BranchMergeInfo],
commitId: String
)
case class BlameInfo(
id: String,
authorName: String,
authorEmailAddress: String,
authorTime: java.util.Date,
prev: Option[String],
prevPath: Option[String],
commitTime: java.util.Date,
message: String,
lines: Set[Int]
)
/**
* Returns RevCommit from the commit or tag id.
*
* @param git the Git object
* @param objectId the ObjectId of the commit or tag
* @return the RevCommit for the specified commit or tag
*/
def getRevCommitFromId(git: Git, objectId: ObjectId): RevCommit = {
val revWalk = new RevWalk(git.getRepository)
val revCommit = revWalk.parseAny(objectId) match {
case r: RevTag => revWalk.parseCommit(r.getObject)
case _ => revWalk.parseCommit(objectId)
}
revWalk.dispose
revCommit
}
private val cache: Cache[String, Int] = if (isCacheEnabled()) {
Cache2kBuilder
.of(classOf[String], classOf[Int])
.name("commit-count")
.expireAfterWrite(24, TimeUnit.HOURS)
.entryCapacity(10000)
.build()
} else null
private val objectCommitCache: Cache[ObjectId, RevCommit] = if (isCacheEnabled()) {
Cache2kBuilder
.of(classOf[ObjectId], classOf[RevCommit])
.name("object-commit")
.entryCapacity(10000)
.build()
} else null
def removeCache(git: Git): Unit = {
if (isCacheEnabled()) {
val dir = git.getRepository.getDirectory
val keyPrefix = dir.getAbsolutePath + "@"
cache.keys.forEach(key => {
if (key.startsWith(keyPrefix)) {
cache.remove(key)
}
})
}
}
/**
* Returns the number of commits in the specified branch or commit.
* If the specified branch has over 10000 commits, this method returns 100001.
*/
def getCommitCount(git: Git, branch: String, max: Int = 10001): Int = {
val dir = git.getRepository.getDirectory
if (isCacheEnabled()) {
val key = dir.getAbsolutePath + "@" + branch
val entry = cache.getEntry(key)
if (entry == null) {
val commitId = git.getRepository.resolve(branch)
val commitCount = git.log.add(commitId).call.iterator.asScala.take(max).size
cache.put(key, commitCount)
commitCount
} else {
entry.getValue
}
} else {
val commitId = git.getRepository.resolve(branch)
val commitCount = git.log.add(commitId).call.iterator.asScala.take(max).size
commitCount
}
}
/**
* Returns the repository information. It contains branch names and tag names.
*/
def getRepositoryInfo(owner: String, repository: String): RepositoryInfo = {
Using.resource(Git.open(getRepositoryDir(owner, repository))) { git =>
try {
RepositoryInfo(
owner,
repository,
// branches
git.branchList.call.asScala.map { ref =>
ref.getName.stripPrefix("refs/heads/")
}.toList,
// tags
git.tagList.call.asScala
.flatMap { ref =>
try {
val revCommit = getRevCommitFromId(git, ref.getObjectId)
Some(
TagInfo(
ref.getName.stripPrefix("refs/tags/"),
revCommit.getCommitterIdent.getWhen,
revCommit.getName,
revCommit.getShortMessage,
ref.getObjectId.getName
)
)
} catch {
case _: IncorrectObjectTypeException =>
None
}
}
.sortBy(_.time)
.toList
)
} catch {
// not initialized
case e: NoHeadException => RepositoryInfo(owner, repository, Nil, Nil)
}
}
}
/**
* Returns the file list of the specified path.
*
* @param git the Git object
* @param revision the branch name or commit id
* @param path the directory path (optional)
* @param baseUrl the base url of GitBucket instance. This parameter is used to generate links of submodules (optional)
* @param commitCount the number of commit of this repository (optional). If this number is greater than threshold, the commit info is cached in memory.
* @param maxFiles don't fetch commit info if the number of files in the directory is bigger than this number.
* @return The list of files in the specified directory. If the number of files are greater than threshold, the returned file list won't include the commit info.
*/
def getFileList(
git: Git,
revision: String,
path: String = ".",
baseUrl: Option[String] = None,
commitCount: Int = 0,
maxFiles: Int = 5
): List[FileInfo] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val objectId = git.getRepository.resolve(revision)
if (objectId == null) return Nil
val revCommit = revWalk.parseCommit(objectId)
def useTreeWalk(rev: RevCommit)(f: TreeWalk => Any): Unit =
if (path == ".") {
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(rev.getTree)
Using.resource(treeWalk)(f)
} else {
val treeWalk = TreeWalk.forPath(git.getRepository, path, rev.getTree)
if (treeWalk != null) {
treeWalk.enterSubtree
Using.resource(treeWalk)(f)
}
}
@tailrec
def simplifyPath(
tuple: (ObjectId, FileMode, String, String, Option[String], Option[RevCommit])
): (ObjectId, FileMode, String, String, Option[String], Option[RevCommit]) =
tuple match {
case (oid, FileMode.TREE, name, path, _, commit) =>
(Using.resource(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,
path + "/" + walk.getNameString,
None,
commit
)
).filterNot(_ => walk.next())
} else {
None
}
}) match {
case Some(child) => simplifyPath(child)
case _ => tuple
}
case _ => tuple
}
def appendLastCommits(
fileList: List[(ObjectId, FileMode, String, String, Option[String])]
): List[(ObjectId, FileMode, String, String, Option[String], Option[RevCommit])] = {
fileList.map { case (id, mode, name, path, opt) =>
if (maxFiles > 0 && fileList.size >= maxFiles) {
// Don't attempt to get the last commit if the number of files is very large.
(id, mode, name, path, opt, None)
} else if (commitCount < 10000) {
(id, mode, name, path, opt, Some(getCommit(path)))
} else if (isCacheEnabled()) {
// Use in-memory cache if the commit count is too big.
val cached = objectCommitCache.getEntry(id)
if (cached == null) {
val commit = getCommit(path)
objectCommitCache.put(id, commit)
(id, mode, name, path, opt, Some(commit))
} else {
(id, mode, name, path, opt, Some(cached.getValue))
}
} else {
val commit = getCommit(path)
(id, mode, name, path, opt, Some(commit))
}
}
}
def getCommit(path: String): RevCommit = {
git
.log()
.addPath(path)
.add(revCommit)
.setMaxCount(1)
.call()
.iterator()
.next()
}
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, baseUrl).find(_.path == treeWalk.getPathString).map(_.viewerUrl)
} else None
fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(
0
), treeWalk.getNameString, treeWalk.getPathString, linkUrl)
}
}
appendLastCommits(fileList)
.map(simplifyPath)
.map { case (objectId, fileMode, name, path, linkUrl, commit) =>
FileInfo(
objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name,
path,
getSummaryMessage(
commit.map(_.getFullMessage).getOrElse(""),
commit.map(_.getShortMessage).getOrElse("")
),
commit.map(_.getName).getOrElse(""),
commit.map(_.getAuthorIdent.getWhen).orNull,
commit.map(_.getAuthorIdent.getName).getOrElse(""),
commit.map(_.getAuthorIdent.getEmailAddress).getOrElse(""),
linkUrl
)
}
.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true, false) => true
case (false, true) => false
case _ => file1.name.compareTo(file2.name) < 0
}
}
}
}
/**
* Returns the first line of the commit message.
*/
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
val i = fullMessage.trim.indexOf('\n')
val firstLine = if (i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage
if (firstLine.length > shortMessage.length) shortMessage else firstLine
}
/**
* get all file list by revision. only file.
*/
def getTreeId(git: Git, revision: String): Option[String] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val objectId = git.getRepository.resolve(revision)
if (objectId == null) return None
val revCommit = revWalk.parseCommit(objectId)
Some(revCommit.getTree.name)
}
}
/**
* get all file list by tree object id.
*/
def getAllFileListByTreeId(git: Git, treeId: String): List[String] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val objectId = git.getRepository.resolve(treeId + "^{tree}")
if (objectId == null) return Nil
Using.resource(new TreeWalk(git.getRepository)) { treeWalk =>
treeWalk.addTree(objectId)
treeWalk.setRecursive(true)
var ret: List[String] = Nil
if (treeWalk != null) {
while (treeWalk.next()) {
ret +:= treeWalk.getPathString
}
}
ret.reverse
}
}
}
/**
* Returns the commit list of the specified branch.
*
* @param git the Git object
* @param revision the branch name or commit id
* @param page the page number (1-)
* @param limit the number of commit info per page. 0 (default) means unlimited.
* @param path filters by this path. default is no filter.
* @return a tuple of the commit list and whether has next, or the error message
*/
def getCommitLog(
git: Git,
revision: String,
page: Int = 1,
limit: Int = 0,
path: String = ""
): Either[String, (List[CommitInfo], Boolean)] = {
val fixedPage = if (page <= 0) 1 else page
@scala.annotation.tailrec
def getCommitLog(
i: java.util.Iterator[RevCommit],
count: Int,
logs: List[CommitInfo]
): (List[CommitInfo], Boolean) =
i.hasNext match {
case true if (limit <= 0 || logs.size < limit) => {
val commit = i.next
getCommitLog(
i,
count + 1,
if (limit <= 0 || (fixedPage - 1) * limit <= count) logs :+ new CommitInfo(commit) else logs
)
}
case _ => (logs, i.hasNext)
}
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val objectId = git.getRepository.resolve(revision)
if (objectId == null) {
Left(s"${revision} can't be resolved.")
} else {
revWalk.markStart(revWalk.parseCommit(objectId))
if (path.nonEmpty) {
revWalk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path), TreeFilter.ANY_DIFF))
}
Right(getCommitLog(revWalk.iterator, 0, Nil))
}
}
}
def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false)(
endCondition: RevCommit => Boolean
): List[CommitInfo] = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
i.hasNext match {
case true => {
val revCommit = i.next
if (endCondition(revCommit)) {
if (includesLastCommit) logs :+ new CommitInfo(revCommit) else logs
} else {
getCommitLog(i, logs :+ new CommitInfo(revCommit))
}
}
case false => logs
}
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
getCommitLog(revWalk.iterator, Nil).reverse
}
}
/**
* Returns the commit list between two revisions.
*
* @param git the Git object
* @param from the from revision
* @param to the to revision
* @return the commit list
*/
// TODO swap parameters 'from' and 'to'!?
def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] =
getCommitLogs(git, to)(_.getName == from)
/**
* Returns the latest RevCommit of the specified path.
*
* @param git the Git object
* @param path the path
* @param revision the branch name or commit id
* @return the latest commit
*/
def getLatestCommitFromPath(git: Git, path: String, revision: String): Option[RevCommit] =
getLatestCommitFromPaths(git, List(path), revision).get(path)
/**
* Returns the list of latest RevCommit of the specified paths.
*
* @param git the Git object
* @param paths the list of paths
* @param revision the branch name or commit id
* @return the list of latest commit
*/
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val start = getRevCommitFromId(git, git.getRepository.resolve(revision))
paths.flatMap { path =>
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
if (commit == null) {
None
} else {
Some((path, commit))
}
}.toMap
}
def getPatch(git: Git, from: Option[String], to: String): String = {
val out = new ByteArrayOutputStream()
val df = new DiffFormatter(out)
df.setRepository(git.getRepository)
df.setDiffComparator(RawTextComparator.DEFAULT)
df.setDetectRenames(true)
getDiffEntries(git, from, to)
.foreach { entry =>
df.format(entry)
}
new String(out.toByteArray, "UTF-8")
}
private def getDiffEntries(git: Git, from: Option[String], to: String): Seq[DiffEntry] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
df.setRepository(git.getRepository)
val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
(from match {
case None =>
toCommit.getParentCount match {
case 0 =>
df.scan(
new EmptyTreeIterator(),
new CanonicalTreeParser(null, git.getRepository.newObjectReader(), toCommit.getTree)
).asScala
case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
}
case Some(from) =>
val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
df.scan(fromCommit.getTree, toCommit.getTree).asScala
}).toSeq
}
}
def getParentCommitId(git: Git, id: String): Option[String] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val commit = revWalk.parseCommit(git.getRepository.resolve(id))
commit.getParentCount match {
case 0 => None
case _ => Some(commit.getParent(0).getName)
}
}
}
def getDiff(git: Git, from: Option[String], to: String, path: String): Option[DiffInfo] = {
getDiffEntries(git, from, to).find(_.getNewPath == path).map { diff =>
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
val includeContent = oldIsImage || newIsImage
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false,
patch = None
)
}
}
def getDiffs(
git: Git,
from: Option[String],
to: String,
fetchContent: Boolean,
makePatch: Boolean,
maxFiles: Int = 100,
maxLines: Int = 1000
): List[DiffInfo] = {
val diffs = getDiffEntries(git, from, to)
diffs.map { diff =>
if (maxFiles > 0 && diffs.size > maxFiles) { // Don't show diff if there are more than maxFiles
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = None,
newContent = None,
oldIsImage = false,
newIsImage = false,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = true,
patch = None
)
} else {
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
val patch =
if (oldIsImage || newIsImage) None else Some(makePatchFromDiffEntry(git, diff)) // TODO use DiffFormatter
val tooLarge = maxLines > 0 &&
patch.exists(_.count(_ == '\n') > maxLines) // Don't show diff if the file has more than maxLines of diff
val includeContent = tooLarge || !fetchContent || oldIsImage || newIsImage
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = tooLarge,
patch = if (makePatch) patch else None
)
}
}.toList
}
private def getTextContent(git: Git, objectId: ObjectId): Option[String] = {
JGitUtil
.getContentFromId(git, objectId, false)
.filter(FileUtil.isText)
.map(convertFromByteArray)
}
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
val out = new ByteArrayOutputStream()
Using.resource(new DiffFormatter(out)) { formatter =>
formatter.setRepository(git.getRepository)
formatter.format(diff)
val patch = new String(out.toByteArray) // TODO charset???
patch.split("\n").drop(4).mkString("\n")
}
}
/**
* Returns the list of branch names of the specified commit.
*/
def getBranchesOfCommit(git: Git, commitId: String): List[String] =
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val commit = revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))
git.getRepository.getRefDatabase
.getRefsByPrefix(Constants.R_HEADS)
.asScala
.filter { e =>
(revWalk.isMergedInto(
commit,
revWalk.parseCommit(e.getObjectId)
))
}
.map { e =>
e.getName.substring(Constants.R_HEADS.length)
}
.toList
.sorted
}
/**
* Returns the list of tags which pointed on the specified commit.
*/
def getTagsOnCommit(git: Git, commitId: String): List[String] = {
git.getRepository.getAllRefsByPeeledObjectId.asScala
.get(git.getRepository.resolve(commitId + "^0"))
.map {
_.asScala
.collect {
case x if x.getName.startsWith(Constants.R_TAGS) =>
x.getName.substring(Constants.R_TAGS.length)
}
.toList
.sorted
}
.getOrElse {
List.empty
}
}
/**
* Returns the list of tags which contains the specified commit.
*/
def getTagsOfCommit(git: Git, commitId: String): List[String] =
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val commit = revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))
git.getRepository.getRefDatabase
.getRefsByPrefix(Constants.R_TAGS)
.asScala
.filter { e =>
(revWalk.isMergedInto(
commit,
revWalk.parseCommit(e.getObjectId)
))
}
.map { e =>
e.getName.substring(Constants.R_TAGS.length)
}
.toList
.sorted
.reverse
}
def initRepository(dir: java.io.File, defaultBranch: String): Unit =
Using.resource(new RepositoryBuilder().setGitDir(dir).setBare().setInitialBranch(defaultBranch).build) {
repository =>
repository.create(true)
setReceivePack(repository)
}
def cloneRepository(from: java.io.File, to: java.io.File): Unit =
Using.resource(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call) { git =>
setReceivePack(git.getRepository)
}
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
val config = repository.getConfig
config.setBoolean("http", null, "receivepack", true)
config.save
}
def getDefaultBranch(
git: Git,
repository: RepositoryService.RepositoryInfo,
revstr: String = ""
): Option[(ObjectId, String)] = {
Seq(
Some(if (revstr.isEmpty) repository.repository.defaultBranch else revstr),
repository.branchList.headOption
).flatMap {
case Some(rev) => Some((git.getRepository.resolve(rev), rev))
case None => None
}.find(_._1 != null)
}
def createTag(git: Git, name: String, message: Option[String], commitId: String) = {
try {
val objectId: ObjectId = git.getRepository.resolve(commitId)
Using.resource(new RevWalk(git.getRepository)) { walk =>
val tagCommand = git.tag().setName(name).setObjectId(walk.parseCommit(objectId))
message.foreach { message =>
tagCommand.setMessage(message)
}
tagCommand.call()
}
Right("Tag added.")
} catch {
case _: ConcurrentRefUpdateException => Left("Sorry, some error occurs.")
case _: InvalidTagNameException => Left("Sorry, that name is invalid.")
case _: NoHeadException => Left("Sorry, this repo doesn't have HEAD reference")
case _: GitAPIException => Left("Sorry, some Git operation error occurs.")
}
}
def createBranch(git: Git, fromBranch: String, newBranch: String) = {
try {
git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call()
Right("Branch created.")
} catch {
case _: RefAlreadyExistsException => Left("Sorry, that branch already exists.")
// JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists.
case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.")
}
}
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
val entry = new DirCacheEntry(path)
entry.setFileMode(mode)
entry.setObjectId(objectId)
entry
}
def createNewCommit(
git: Git,
inserter: ObjectInserter,
headId: AnyObjectId,
treeId: AnyObjectId,
ref: String,
fullName: String,
mailAddress: String,
message: String
): ObjectId = {
val newCommit = new CommitBuilder()
newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
newCommit.setMessage(message)
if (headId != null) {
newCommit.setParentIds(List(headId).asJava)
}
newCommit.setTreeId(treeId)
val newHeadId = inserter.insert(newCommit)
inserter.flush()
inserter.close()
val refUpdate = git.getRepository.updateRef(ref)
refUpdate.setNewObjectId(newHeadId)
refUpdate.update()
removeCache(git)
newHeadId
}
/**
* Read submodule information from .gitmodules
*/
def getSubmodules(git: Git, tree: RevTree, baseUrl: Option[String]): List[SubmoduleInfo] = {
val repository = git.getRepository
getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
(try {
val config = new BlobBasedConfig(repository.getConfig(), bytes)
config.getSubsections("submodule").asScala.map { module =>
val path = config.getString("submodule", module, "path")
val url = config.getString("submodule", module, "url")
SubmoduleInfo(module, path, url, StringUtil.getRepositoryViewerUrl(url, baseUrl))
}
} catch {
case e: ConfigInvalidException => {
logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e)
Nil
}
}).toList
} getOrElse Nil
}
/**
* Get object content of the given path as byte array from the Git repository.
*
* @param git the Git object
* @param revTree the rev tree
* @param path the path
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = {
@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.resource(new TreeWalk(git.getRepository)) { treeWalk =>
treeWalk.addTree(revTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
} flatMap { objectId =>
getContentFromId(git, objectId, fetchLargeFile)
}
}
def getLfsObjects(text: String): Map[String, String] = {
if (text.startsWith("version https://git-lfs.github.com/spec/v1")) {
// LFS objects
text
.split("\n")
.map { line =>
val dim = line.split(" ")
dim(0) -> dim(1)
}
.toMap
} else {
Map.empty
}
}
def getContentSize(loader: ObjectLoader): Long = {
if (loader.isLarge) {
loader.getSize
} else {
val bytes = loader.getCachedBytes
val text = new String(bytes, "UTF-8")
val attr = getLfsObjects(text)
attr.get("size") match {
case Some(size) => size.toLong
case None => loader.getSize
}
}
}
def isLfsPointer(loader: ObjectLoader): Boolean = {
!loader.isLarge && new String(loader.getBytes(), "UTF-8").startsWith("version https://git-lfs.github.com/spec/v1")
}
def getContentInfo(git: Git, path: String, objectId: ObjectId, safeMode: Boolean): ContentInfo = {
// Viewer
Using.resource(git.getRepository.getObjectDatabase) { db =>
val loader = db.open(objectId)
val isLfs = isLfsPointer(loader)
val large = FileUtil.isLarge(loader.getSize)
val viewer = if (FileUtil.isImage(path, safeMode)) "image" else if (large) "large" else "other"
val bytes = if (viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
val size = Some(getContentSize(loader))
if (viewer == "other") {
if (!isLfs && bytes.isDefined && FileUtil.isText(bytes.get)) {
// text
ContentInfo(
"text",
size,
Some(StringUtil.convertFromByteArray(bytes.get)),
Some(StringUtil.detectEncoding(bytes.get))
)
} else {
// binary
ContentInfo("binary", size, None, None)
}
} else {
// image or large
ContentInfo(viewer, size, None, None)
}
}
}
/**
* Get object content of the given object id as byte array from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] =
try {
Using.resource(git.getRepository.getObjectDatabase) { db =>
val loader = db.open(id)
if (loader.isLarge || (fetchLargeFile == false && FileUtil.isLarge(loader.getSize))) {
None
} else {
Some(loader.getBytes)
}
}
} catch {
case _: MissingObjectException => None
}
/**
* Get objectLoader of the given object id from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param f the function process ObjectLoader
* @return None if object does not exist
*/
def getObjectLoaderFromId[A](git: Git, id: ObjectId)(f: ObjectLoader => A): Option[A] =
try {
Using.resource(git.getRepository.getObjectDatabase) { db =>
Some(f(db.open(id)))
}
} catch {
case _: MissingObjectException => None
}
/**
* Returns all commit id in the specified repository.
*/
def getAllCommitIds(git: Git): Seq[String] =
if (isEmpty(git)) {
Nil
} else {
val existIds = new scala.collection.mutable.ListBuffer[String]()
val i = git.log.all.call.iterator
while (i.hasNext) {
existIds += i.next.name
}
existIds.toSeq
}
def processTree[T](git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => T): Seq[T] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
Using.resource(new TreeWalk(git.getRepository)) { treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(id))
treeWalk.setRecursive(true)
val result = new collection.mutable.ListBuffer[T]()
while (treeWalk.next) {
result += f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser]))
}
result.toSeq
}
}
}
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
def getForkedCommitId(
oldGit: Git,
newGit: Git,
userName: String,
repositoryName: String,
branch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String
): String = {
val existIds = getAllCommitIds(oldGit)
getCommitLogs(newGit, requestBranch, true) { commit =>
existIds.contains(commit.name) && getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
}
/**
* Fetch pull request contents into refs/pull/${issueId}/head and return (commitIdTo, commitIdFrom)
*/
// TODO should take Git instead of owner and username for testability
def updatePullRequest(
userName: String,
repositoryName: String,
branch: String,
issueId: Int,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String
): (String, String) =
Using.resources(
Git.open(Directory.getRepositoryDir(userName, repositoryName)),
Git.open(Directory.getRepositoryDir(requestUserName, requestRepositoryName))
) { (oldGit, newGit) =>
oldGit.fetch
.setRemote(Directory.getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head").setForceUpdate(true))
.call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${issueId}/head").getName
val commitIdFrom = getForkedCommitId(
oldGit,
newGit,
userName,
repositoryName,
branch,
requestUserName,
requestRepositoryName,
requestBranch
)
(commitIdTo, commitIdFrom)
}
/**
* Returns the last modified commit of specified path
*
* @param git the Git object
* @param startCommit the search base commit id
* @param path the path of target file or directory
* @return the last modified commit of specified path
*/
def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = {
git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
}
def getBranchesNoMergeInfo(git: Git, defaultBranch: String): Seq[BranchInfoSimple] = {
val repo = git.getRepository
val defaultObject = repo.resolve(defaultBranch)
git.branchList.call.asScala.map { ref =>
val walk = new RevWalk(repo)
try {
val branchName = ref.getName.stripPrefix("refs/heads/")
val branchCommit = walk.parseCommit(ref.getObjectId)
val when = branchCommit.getCommitterIdent.getWhen
val committer = branchCommit.getCommitterIdent.getName
val committerEmail = branchCommit.getCommitterIdent.getEmailAddress
BranchInfoSimple(branchName, committer, when, committerEmail, ref.getObjectId.name)
} finally {
walk.dispose()
}
}.toSeq
}
def getBranches(git: Git, defaultBranch: String, origin: Boolean): Seq[BranchInfo] = {
val repo = git.getRepository
val defaultObject = repo.resolve(defaultBranch)
git.branchList.call.asScala.map { ref =>
val walk = new RevWalk(repo)
try {
val defaultCommit = walk.parseCommit(defaultObject)
val branchName = ref.getName.stripPrefix("refs/heads/")
val branchCommit = walk.parseCommit(ref.getObjectId)
val when = branchCommit.getCommitterIdent.getWhen
val committer = branchCommit.getCommitterIdent.getName
val committerEmail = branchCommit.getCommitterIdent.getEmailAddress
val mergeInfo = if (origin && branchName == defaultBranch) {
None
} else {
walk.reset()
walk.setRevFilter(RevFilter.MERGE_BASE)
walk.markStart(branchCommit)
walk.markStart(defaultCommit)
val mergeBase = walk.next()
walk.reset()
walk.setRevFilter(RevFilter.ALL)
Some(
BranchMergeInfo(
ahead = RevWalkUtils.count(walk, branchCommit, mergeBase),
behind = RevWalkUtils.count(walk, defaultCommit, mergeBase),
isMerged = walk.isMergedInto(branchCommit, defaultCommit)
)
)
}
BranchInfo(branchName, committer, when, committerEmail, mergeInfo, ref.getObjectId.name)
} finally {
walk.dispose()
}
}.toSeq
}
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
Option(git.getRepository.resolve(id))
.map { commitId =>
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository)
blamer.setStartCommit(commitId)
blamer.setFilePath(path)
val blame = blamer.call()
var blameMap = Map[String, JGitUtil.BlameInfo]()
var idLine = List[(String, Int)]()
0.until(blame.getResultContents().size()).foreach { i =>
val c = blame.getSourceCommit(i)
if (!blameMap.contains(c.name)) {
blameMap += c.name -> JGitUtil.BlameInfo(
c.name,
c.getAuthorIdent.getName,
c.getAuthorIdent.getEmailAddress,
c.getAuthorIdent.getWhen,
Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next)
.map(_.name),
if (blame.getSourcePath(i) == path) { None }
else { Some(blame.getSourcePath(i)) },
c.getCommitterIdent.getWhen,
c.getShortMessage,
Set.empty
)
}
idLine :+= (c.name, i)
}
val limeMap = idLine.groupBy(_._1).view.mapValues(_.map(_._2).toSet)
blameMap.values.map { b =>
b.copy(lines = limeMap(b.id))
}
}
.getOrElse(Seq.empty)
}
/**
* Returns sha1
*
* @param owner repository owner
* @param name repository name
* @param revstr A git object references expression
* @return sha1
*/
def getShaByRef(owner: String, name: String, revstr: String): Option[String] = {
Using.resource(Git.open(getRepositoryDir(owner, name))) { git =>
Option(git.getRepository.resolve(revstr)).map(ObjectId.toString(_))
}
}
private def openFile[T](git: Git, repository: RepositoryService.RepositoryInfo, treeWalk: TreeWalk)(
f: InputStream => T
): T = {
val attrs = treeWalk.getAttributes
val loader = git.getRepository.open(treeWalk.getObjectId(0))
if (attrs.containsKey("filter") && attrs.get("filter").getValue == "lfs") {
val lfsAttrs = getLfsAttributes(loader)
if (lfsAttrs.nonEmpty) {
val oid = lfsAttrs("oid").split(":")(1)
Using.resource(new FileInputStream(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))) { in =>
f(in)
}
} else {
throw new NoSuchElementException("LFS attribute is empty.")
}
} else {
Using.resource(loader.openStream()) { in =>
f(in)
}
}
}
def openFile[T](git: Git, repository: RepositoryService.RepositoryInfo, tree: RevTree, path: String)(
f: InputStream => T
): T = {
Using.resource(TreeWalk.forPath(git.getRepository, path, tree)) { treeWalk =>
openFile(git, repository, treeWalk)(f)
}
}
private def getLfsAttributes(loader: ObjectLoader): Map[String, String] = {
val bytes = loader.getCachedBytes
val text = new String(bytes, "UTF-8")
JGitUtil.getLfsObjects(text)
}
}