diff --git a/build.sbt b/build.sbt index 9aa2823..a31963b 100644 --- a/build.sbt +++ b/build.sbt @@ -68,7 +68,8 @@ "com.wix" % "wix-embedded-mysql" % "3.0.0" % "test", "ru.yandex.qatools.embed" % "postgresql-embedded" % "2.6" % "test", "net.i2p.crypto" % "eddsa" % "0.2.0", - "is.tagomor.woothee" % "woothee-java" % "1.7.0" + "is.tagomor.woothee" % "woothee-java" % "1.7.0", + "org.ec4j.core" % "ec4j-core" % "0.0.1" ) // Compiler settings diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index bcc2068..3a3480b 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -26,6 +26,7 @@ import org.apache.commons.compress.utils.IOUtils import org.scalatra.forms._ import org.apache.commons.io.FileUtils +import org.ec4j.core.model.PropertyType import org.eclipse.jgit.api.{ArchiveCommand, Git} import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder} @@ -330,17 +331,23 @@ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) - getPathObjectId(git, path, revCommit).map { objectId => - val paths = path.split("/") - html.editor( - branch = branch, - repository = repository, - pathList = paths.take(paths.size - 1).toList, - fileName = Some(paths.last), - content = JGitUtil.getContentInfo(git, path, objectId), - protectedBranch = protectedBranch, - commit = revCommit.getName - ) + getPathObjectId(git, path, revCommit).map { + objectId => + val paths = path.split("/") + val info = EditorConfigUtil.getEditorConfigInfo(git, branch, path) + + html.editor( + branch = branch, + repository = repository, + pathList = paths.take(paths.size - 1).toList, + fileName = Some(paths.last), + content = JGitUtil.getContentInfo(git, path, objectId), + protectedBranch = protectedBranch, + commit = revCommit.getName, + newLineMode = info.newLineMode, + useSoftTabs = info.useSoftTabs, + tabSize = info.tabSize + ) } getOrElse NotFound() } }) @@ -451,6 +458,7 @@ // Download (This route is left for backword compatibility) responseRawFile(git, objectId, path, repository) } else { + val info = EditorConfigUtil.getEditorConfigInfo(git, id, path) html.blob( branch = id, repository = repository, @@ -459,7 +467,8 @@ latestCommit = new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)), hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount), isBlame = request.paths(2) == "blame", - isLfsFile = isLfsFile(git, objectId) + isLfsFile = isLfsFile(git, objectId), + tabSize = info.tabSize ) } } getOrElse NotFound() diff --git a/src/main/scala/gitbucket/core/util/EditorConfigUtil.scala b/src/main/scala/gitbucket/core/util/EditorConfigUtil.scala new file mode 100644 index 0000000..e68efaf --- /dev/null +++ b/src/main/scala/gitbucket/core/util/EditorConfigUtil.scala @@ -0,0 +1,146 @@ +package gitbucket.core.util + +import java.io.{IOException, InputStreamReader, Reader} +import java.nio.charset.StandardCharsets + +import org.ec4j.core.Resource.Resources.StringRandomReader +import org.ec4j.core.model.PropertyType.{EndOfLineValue, IndentStyleValue} +import org.ec4j.core.model.{Ec4jPath, PropertyType, Version} +import org.ec4j.core.parser.ParseException +import org.ec4j.core._ +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.{ObjectReader, Repository} +import org.eclipse.jgit.revwalk.{RevTree, RevWalk} +import org.eclipse.jgit.treewalk.TreeWalk +import gitbucket.core.util.SyntaxSugars._ + +object EditorConfigUtil { + private class JGitResource(repo: Repository, revStr: String, path: Ec4jPath) extends Resource { + private def removeInitialSlash(path: Ec4jPath) = Ec4jPath.Ec4jPaths.root.relativize(path).toString + + def this(git: Git, revStr: String, path: String) = { + this(git.getRepository, revStr, Ec4jPath.Ec4jPaths.of(if (path.startsWith("/")) path else "/" + path)) + } + + def this(repo: Repository, revStr: String, path: String) = { + this(repo, revStr, Ec4jPath.Ec4jPaths.of(if (path.startsWith("/")) path else "/" + path)) + } + + private def getRevTree: RevTree = { + using(repo.newObjectReader()) { reader: ObjectReader => + val revWalk = new RevWalk(reader) + val id = repo.resolve(revStr) + val commit = revWalk.parseCommit(id) + commit.getTree + } + } + + override def exists(): Boolean = { + using(repo.newObjectReader()) { reader: ObjectReader => + try { + val treeWalk = Option(TreeWalk.forPath(reader, removeInitialSlash(path), getRevTree)) + treeWalk.isDefined + } catch { + case e: IOException => false + } + } + } + + override def getPath: Ec4jPath = { + path + } + + override def getParent: ResourcePath = { + Option(path.getParentPath).map { new JGitResourcePath(repo, revStr, _) }.getOrElse(null) + } + + override def openRandomReader(): Resource.RandomReader = { + StringRandomReader.ofReader(openReader()) + } + + override def openReader(): Reader = { + using(repo.newObjectReader) { reader: ObjectReader => + val treeWalk = TreeWalk.forPath(reader, removeInitialSlash(path), getRevTree) + new InputStreamReader(reader.open(treeWalk.getObjectId(0)).openStream, StandardCharsets.UTF_8) + } + } + } + + private class JGitResourcePath(repo: Repository, revStr: String, path: Ec4jPath) extends ResourcePath { + + override def getParent: ResourcePath = { + Option(path.getParentPath).map { new JGitResourcePath(repo, revStr, _) }.getOrElse(null) + } + + override def getPath: Ec4jPath = { + path + } + + override def hasParent: Boolean = { + Option(path.getParentPath).isDefined + } + + override def relativize(resource: Resource): Resource = { + resource match { + case r: JGitResource => + new JGitResource(repo, revStr, path.relativize(r.getPath).toString) + } + } + + override def resolve(name: String): Resource = { + Option(path) + .map { p => + new JGitResource(repo, revStr, p.resolve(name)) + } + .getOrElse { + new JGitResource(repo, revStr, name) + } + } + } + + private object JGitResourcePath { + def RootDirectory(git: Git, revStr: String) = + new JGitResourcePath(git.getRepository, revStr, Ec4jPath.Ec4jPaths.of("/")) + } + + private val TabSizeDefault: Int = 8 + private val NewLineModeDefault: String = "auto" + private val UseSoftTabsDefault = false + + case class EditorConfigInfo( + tabSize: Int, + newLineMode: String, + useSoftTabs: Boolean + ) + + def getEditorConfigInfo(git: Git, rev: String, path: String): EditorConfigInfo = { + try { + val resourcePropertiesService = ResourcePropertiesService + .builder() + .configFileName(EditorConfigConstants.EDITORCONFIG) + .rootDirectory(JGitResourcePath.RootDirectory(git, rev)) + .loader(EditorConfigLoader.of(Version.CURRENT)) + .keepUnset(true) + .build() + + val props = resourcePropertiesService.queryProperties(new JGitResource(git, rev, path)) + EditorConfigInfo( + tabSize = props.getValue[Integer](PropertyType.tab_width, TabSizeDefault, false), + newLineMode = props.getValue[EndOfLineValue](PropertyType.end_of_line, null, false) match { + case EndOfLineValue.cr => "cr" + case EndOfLineValue.lf => "lf" + case EndOfLineValue.crlf => "crlf" + case _ => "auto" + }, + props.getValue[IndentStyleValue](PropertyType.indent_style, null, false) match { + case IndentStyleValue.space => true + case IndentStyleValue.tab => false + case _ => false + } + ) + } catch { + case e: ParseException => EditorConfigInfo(TabSizeDefault, NewLineModeDefault, UseSoftTabsDefault) + case e: IOException => EditorConfigInfo(TabSizeDefault, NewLineModeDefault, UseSoftTabsDefault) + } + } +} diff --git a/src/main/twirl/gitbucket/core/repo/blob.scala.html b/src/main/twirl/gitbucket/core/repo/blob.scala.html index 0b9a427..9819143 100644 --- a/src/main/twirl/gitbucket/core/repo/blob.scala.html +++ b/src/main/twirl/gitbucket/core/repo/blob.scala.html @@ -5,10 +5,16 @@ latestCommit: gitbucket.core.util.JGitUtil.CommitInfo, hasWritePermission: Boolean, isBlame: Boolean, - isLfsFile: Boolean)(implicit context: gitbucket.core.controller.Context) + isLfsFile: Boolean, + tabSize: Int)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @gitbucket.core.html.main(s"${(repository.name :: pathList).mkString("/")} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) { @gitbucket.core.html.menu("files", repository){ +