Support EditorConfig for online browser/editor.

This commit is contained in:
KOUNOIKE Yuusuke
2018-04-21 10:33:29 +09:00
parent c1d6839c18
commit e350126794
8 changed files with 342 additions and 16 deletions

View File

@@ -68,7 +68,8 @@ libraryDependencies ++= Seq(
"com.wix" % "wix-embedded-mysql" % "3.0.0" % "test", "com.wix" % "wix-embedded-mysql" % "3.0.0" % "test",
"ru.yandex.qatools.embed" % "postgresql-embedded" % "2.6" % "test", "ru.yandex.qatools.embed" % "postgresql-embedded" % "2.6" % "test",
"net.i2p.crypto" % "eddsa" % "0.2.0", "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 // Compiler settings

View File

@@ -0,0 +1,134 @@
package editorconfig;
import org.ec4j.core.Resource;
import org.ec4j.core.ResourcePath;
import org.ec4j.core.model.Ec4jPath;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
public class JGitResource implements Resource {
private final Repository repo;
private final String revStr;
Ec4jPath path;
private static String removeInitialSlash(Ec4jPath path) {
return Ec4jPath.Ec4jPaths.root().relativize(path).toString();
}
public JGitResource(Git git, String revStr, String path){
if (!path.startsWith("/")){
path = "/" + path;
}
this.repo= git.getRepository();
this.path = Ec4jPath.Ec4jPaths.of(path);
this.revStr = revStr;
}
public JGitResource(Repository repo, String revStr, String path){
if (!path.startsWith("/")){
path = "/" + path;
}
this.repo = repo;
this.path = Ec4jPath.Ec4jPaths.of(path);
this.revStr = revStr;
}
public JGitResource(Repository repo, String revStr, Ec4jPath path){
this.repo = repo;
this.path = path;
this.revStr = revStr;
}
private RevTree getRevTree() throws IOException {
ObjectReader reader = repo.newObjectReader();
try {
RevWalk revWalk = new RevWalk(reader);
ObjectId id = repo.resolve(revStr);
RevCommit commit = revWalk.parseCommit(id);
return commit.getTree();
} finally {
reader.close();
}
}
@Override
public boolean exists() {
ObjectReader reader = repo.newObjectReader();
try {
TreeWalk treeWalk = TreeWalk.forPath(reader, removeInitialSlash(path), getRevTree());
if (treeWalk != null){
return true;
}
else {
return false;
}
} catch (IOException e) {
return false;
} finally {
reader.close();
}
}
@Override
public ResourcePath getParent() {
Ec4jPath parent = path.getParentPath();
return parent == null ? null : new JGitResourcePath(repo, revStr, path.getParentPath());
}
@Override
public Ec4jPath getPath() {
return path;
}
@Override
public RandomReader openRandomReader() throws IOException {
return Resources.StringRandomReader.ofReader(openReader());
}
@Override
public Reader openReader() throws IOException {
ObjectReader reader = repo.newObjectReader();
try {
TreeWalk treeWalk = TreeWalk.forPath(reader, removeInitialSlash(path), getRevTree());
return new InputStreamReader(reader.open(treeWalk.getObjectId(0)).openStream(), StandardCharsets.UTF_8);
} finally {
reader.close();
}
}
@Override
public boolean equals(Object obj){
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
JGitResource other = (JGitResource) obj;
if (!repo.equals(other.repo) || !revStr.equals(other.revStr) || !path.equals(other.path)){
return false;
}
return true;
}
@Override
public String toString(){
return "JGitResouce(Repo:" + repo.getDirectory() + ", revStr:" + revStr + ", path:" + path.toString() + ")";
}
}

View File

@@ -0,0 +1,84 @@
package editorconfig;
import org.ec4j.core.Resource;
import org.ec4j.core.ResourcePath;
import org.ec4j.core.model.Ec4jPath;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
public class JGitResourcePath implements ResourcePath {
private final Repository repo;
private final String revStr;
private final Ec4jPath path;
public JGitResourcePath(Repository repo, String revStr, Ec4jPath path){
this.repo= repo;
this.revStr = revStr;
this.path = path;
}
public static JGitResourcePath RootDirectory(Git git, String revStr){
return new JGitResourcePath(git.getRepository(), revStr, Ec4jPath.Ec4jPaths.of("/"));
}
@Override
public ResourcePath getParent() {
Ec4jPath parent = path.getParentPath();
return parent == null ? null : new JGitResourcePath(repo, revStr, parent);
}
@Override
public Ec4jPath getPath() {
return path;
}
@Override
public boolean hasParent() {
return path.getParentPath() != null;
}
@Override
public Resource relativize(Resource resource) {
if (resource instanceof JGitResource) {
JGitResource jgitResource = (JGitResource) resource;
return new JGitResource(repo, revStr, path.relativize(jgitResource.path).toString());
} else {
throw new IllegalArgumentException(
this.getClass().getName() + ".relativize(Resource resource) can handle only instances of "
+ JGitResource.class.getName());
}
}
@Override
public Resource resolve(String name) {
if(path == null){
return new JGitResource(repo, revStr, name);
}
else {
return new JGitResource(repo, revStr, path.resolve(name));
}
}
@Override
public boolean equals(Object obj){
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
JGitResourcePath other = (JGitResourcePath) obj;
if (!repo.equals(other.repo) || !revStr.equals(other.revStr) || !path.equals(other.path)){
return false;
}
return true;
}
@Override
public String toString(){
return "JGitResoucePath(Repo:" + repo.getDirectory() + ", revStr:" + revStr + ", path:" + path.toString() + ")";
}
}

View File

@@ -1,8 +1,8 @@
package gitbucket.core.controller package gitbucket.core.controller
import java.io.File import java.io.File
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.repo.html import gitbucket.core.repo.html
import gitbucket.core.helper import gitbucket.core.helper
@@ -19,6 +19,7 @@ import gitbucket.core.view
import gitbucket.core.view.helpers import gitbucket.core.view.helpers
import org.scalatra.forms._ import org.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.ec4j.core.model.PropertyType
import org.eclipse.jgit.api.{ArchiveCommand, Git} import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder} import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
@@ -314,8 +315,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
git => git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId => getPathObjectId(git, path, revCommit).map {
objectId =>
val paths = path.split("/") val paths = path.split("/")
val props = EditorConfigUtil.readProperties(git, branch, path)
html.editor( html.editor(
branch = branch, branch = branch,
repository = repository, repository = repository,
@@ -323,7 +327,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileName = Some(paths.last), fileName = Some(paths.last),
content = JGitUtil.getContentInfo(git, path, objectId), content = JGitUtil.getContentInfo(git, path, objectId),
protectedBranch = protectedBranch, protectedBranch = protectedBranch,
commit = revCommit.getName commit = revCommit.getName,
newLineMode = EditorConfigUtil.getNewLineMode(props),
useSoftTabs = EditorConfigUtil.getUseSoftTabs(props),
tabSize = EditorConfigUtil.getTabWidth(props)
) )
} getOrElse NotFound() } getOrElse NotFound()
} }
@@ -435,6 +442,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
// Download (This route is left for backword compatibility) // Download (This route is left for backword compatibility)
responseRawFile(git, objectId, path, repository) responseRawFile(git, objectId, path, repository)
} else { } else {
val props = EditorConfigUtil.readProperties(git, id, path)
val tabSize = EditorConfigUtil.getTabWidth(props)
html.blob( html.blob(
branch = id, branch = id,
repository = repository, repository = repository,
@@ -443,7 +452,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
latestCommit = new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)), latestCommit = new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount), hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
isBlame = request.paths(2) == "blame", isBlame = request.paths(2) == "blame",
isLfsFile = isLfsFile(git, objectId) isLfsFile = isLfsFile(git, objectId),
tabSize = tabSize
) )
} }
} getOrElse NotFound() } getOrElse NotFound()

View File

@@ -0,0 +1,46 @@
package gitbucket.core.util
import java.nio.charset.StandardCharsets
import editorconfig.{JGitResource, JGitResourcePath}
import org.ec4j.core.model.PropertyType.{EndOfLineValue, IndentStyleValue}
import org.ec4j.core.model.{PropertyType, Version}
import org.ec4j.core.{EditorConfigConstants, EditorConfigLoader, ResourceProperties, ResourcePropertiesService}
import org.eclipse.jgit.api.Git
import collection.JavaConverters._
object EditorConfigUtil {
def readProperties(git: Git, rev: String, path: String): ResourceProperties = {
val resourcePropertiesService = ResourcePropertiesService
.builder()
.configFileName(EditorConfigConstants.EDITORCONFIG)
.rootDirectory(JGitResourcePath.RootDirectory(git, rev))
.loader(EditorConfigLoader.of(Version.CURRENT))
.keepUnset(true)
.build()
resourcePropertiesService.queryProperties(new JGitResource(git, rev, path))
}
def getTabWidth(props: ResourceProperties): Int = {
props.getValue[Integer](PropertyType.tab_width, 8, false)
}
def getNewLineMode(props: ResourceProperties): String = {
props.getValue[EndOfLineValue](PropertyType.end_of_line, null, false) match {
case EndOfLineValue.cr => "cr"
case EndOfLineValue.lf => "lf"
case EndOfLineValue.crlf => "crlf"
case _ => "auto"
}
}
def getUseSoftTabs(props: ResourceProperties): Boolean = {
props.getValue[IndentStyleValue](PropertyType.indent_style, IndentStyleValue.tab, false) match {
case IndentStyleValue.space => true
case IndentStyleValue.tab => false
case _ => false
}
}
}

View File

@@ -5,10 +5,16 @@
latestCommit: gitbucket.core.util.JGitUtil.CommitInfo, latestCommit: gitbucket.core.util.JGitUtil.CommitInfo,
hasWritePermission: Boolean, hasWritePermission: Boolean,
isBlame: 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 @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.main(s"${(repository.name :: pathList).mkString("/")} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) {
@gitbucket.core.html.menu("files", repository){ @gitbucket.core.html.menu("files", repository){
<style>
.prettyprint {
tab-size: @tabSize
}
</style>
<div class="head"> <div class="head">
<div class="pull-right hide-if-blame"><div class="btn-group"> <div class="pull-right hide-if-blame"><div class="btn-group">
<a href="@helpers.url(repository)/blob/@helpers.encodeRefName((latestCommit.id :: pathList).mkString("/"))" data-hotkey="y" style="display: none;">Transfer to URL with SHA</a> <a href="@helpers.url(repository)/blob/@helpers.encodeRefName((latestCommit.id :: pathList).mkString("/"))" data-hotkey="y" style="display: none;">Transfer to URL with SHA</a>

View File

@@ -4,7 +4,11 @@
fileName: Option[String], fileName: Option[String],
content: gitbucket.core.util.JGitUtil.ContentInfo, content: gitbucket.core.util.JGitUtil.ContentInfo,
protectedBranch: Boolean, protectedBranch: Boolean,
commit: String)(implicit context: gitbucket.core.controller.Context) commit: String,
newLineMode: String = "auto",
useSoftTabs: Boolean = false,
tabSize: Int = 8
)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@gitbucket.core.html.main(if(fileName.isEmpty) "New File" else s"Editing ${fileName.get} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) { @gitbucket.core.html.main(if(fileName.isEmpty) "New File" else s"Editing ${fileName.get} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) {
@gitbucket.core.html.menu("files", repository){ @gitbucket.core.html.menu("files", repository){
@@ -94,6 +98,9 @@ $(function(){
@if(protectedBranch){ @if(protectedBranch){
editor.setReadOnly(true); editor.setReadOnly(true);
} }
editor.getSession().setOption("tabSize", @tabSize);
editor.getSession().setOption("newLineMode", "@newLineMode");
editor.getSession().setOption("useSoftTabs", @useSoftTabs);
editor.on('change', function(){ editor.on('change', function(){
updateCommitButtonStatus(); updateCommitButtonStatus();

View File

@@ -0,0 +1,38 @@
package gitbucket.core.util
import org.scalatest.FunSuite
import GitSpecUtil._
class EditorConfigUtilSpec extends FunSuite {
val simpleConfig =
"""[*.txt]
|indent_style = tab
|indent_size = 4""".stripMargin
test("no EditorConfig file") {
withTestRepository { git =>
createFile(git, "master", "README.md", "body", message = "commit1")
val props = EditorConfigUtil.readProperties(git, "master", "test.txt")
assert(EditorConfigUtil.getTabWidth(props) == 8)
assert(EditorConfigUtil.getUseSoftTabs(props) == false)
assert(EditorConfigUtil.getNewLineMode(props) == "auto")
val subdirProps = EditorConfigUtil.readProperties(git, "master", "dir1/dir2/dir3/dir4/test.txt")
assert(EditorConfigUtil.getTabWidth(subdirProps) == 8)
assert(EditorConfigUtil.getUseSoftTabs(subdirProps) == false)
assert(EditorConfigUtil.getNewLineMode(subdirProps) == "auto")
}
}
test("simple EditorConfig") {
withTestRepository { git =>
createFile(git, "master", ".editorconfig", simpleConfig, message = "commit1")
val props = EditorConfigUtil.readProperties(git, "master", "test.txt")
assert(EditorConfigUtil.getTabWidth(props) == 4)
val subdirProps = EditorConfigUtil.readProperties(git, "master", "dir1/dir2/dir3/dir4/test.txt")
assert(EditorConfigUtil.getTabWidth(subdirProps) == 4)
}
}
}