mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 04:17:25 +02:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1da7173f27 | ||
|
|
1cb1e68a01 | ||
|
|
b59c8a5512 | ||
|
|
fe63ad0976 | ||
|
|
941cb7b851 | ||
|
|
d1cf0d9fd7 | ||
|
|
64c2bb4d6b | ||
|
|
24c9f5c17e | ||
|
|
d368e4e80d | ||
|
|
5c0ff84fc4 | ||
|
|
502a21b6b6 | ||
|
|
0e9bf59c0f | ||
|
|
108f9fccdd | ||
|
|
ac884bd7c3 | ||
|
|
a4cb5c991c | ||
|
|
68f1f55f37 | ||
|
|
1dc779d5e8 | ||
|
|
f781c7a08c | ||
|
|
a8511a9f39 | ||
|
|
47714eec45 | ||
|
|
c46e9b2f4d | ||
|
|
26d579f13f | ||
|
|
6556d26742 | ||
|
|
608dce2205 | ||
|
|
f86e50c723 | ||
|
|
b60fe33886 | ||
|
|
5210a143fd | ||
|
|
6b11c1a180 | ||
|
|
b3669f6d66 | ||
|
|
bbff75e037 | ||
|
|
7e10618ceb | ||
|
|
7f4def6b83 | ||
|
|
5790d246c8 | ||
|
|
19dee09c86 | ||
|
|
dfe2889912 | ||
|
|
223ba791fe | ||
|
|
0d49bbe7ac | ||
|
|
8381e8122a | ||
|
|
f38924c7fe | ||
|
|
43152c9341 | ||
|
|
cf84e8b7cc | ||
|
|
2b42e73530 | ||
|
|
60030959f2 | ||
|
|
7174523ac5 | ||
|
|
f573fef9eb | ||
|
|
b4250d8254 | ||
|
|
ac4d4de3c1 | ||
|
|
05e6d008fa | ||
|
|
dd4abb2073 | ||
|
|
612aba1365 | ||
|
|
94dce09570 | ||
|
|
cc241c5a7b | ||
|
|
13cf9d01f0 | ||
|
|
47453fec3f | ||
|
|
641d506559 | ||
|
|
3dec2b8159 | ||
|
|
a0bd969140 | ||
|
|
b30d42a37b | ||
|
|
a03acc68e7 | ||
|
|
05296473d3 | ||
|
|
2118f8c764 | ||
|
|
e366af98b5 | ||
|
|
81e2ac44c3 | ||
|
|
07bb326c06 | ||
|
|
bcc2c8cc2d | ||
|
|
2e0e17f1aa | ||
|
|
c517b44e82 | ||
|
|
f311339786 | ||
|
|
34853d0322 | ||
|
|
9c60b69c88 | ||
|
|
4f10bccf84 | ||
|
|
c7eaebf597 | ||
|
|
60e1052d33 | ||
|
|
7e77c102b0 | ||
|
|
a452c582ab | ||
|
|
0d3adb074d | ||
|
|
8ec4b52dda | ||
|
|
9265c68383 | ||
|
|
4bd2d78ecb | ||
|
|
e7aa766d0a | ||
|
|
7d8300b3ce | ||
|
|
af8a1234ed | ||
|
|
bd0ecd0a9d | ||
|
|
35c8f02f90 | ||
|
|
f160952817 | ||
|
|
9e5a302ab1 | ||
|
|
a1dc19fa26 | ||
|
|
e79ded934f | ||
|
|
ef3e7d9286 | ||
|
|
68b25ddbb5 | ||
|
|
f96040eade | ||
|
|
599a808054 | ||
|
|
382c5c55ec | ||
|
|
afb2306904 | ||
|
|
2642da3be3 | ||
|
|
dcbf283c9d | ||
|
|
f38fa0132c | ||
|
|
569053f7e0 | ||
|
|
037a97ff3d |
24
README.md
24
README.md
@@ -41,13 +41,37 @@ or you can start GitBucket by ```java -jar gitbucket.war``` without servlet cont
|
||||
|
||||
- --port=[NUMBER]
|
||||
- --prefix=[CONTEXTPATH]
|
||||
- --host=[HOSTNAME]
|
||||
- --https=true
|
||||
- --gitbucket.home=[DATA_DIR]
|
||||
|
||||
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
|
||||
|
||||
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
|
||||
|
||||
### Mac OS X
|
||||
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
|
||||
|
||||
Run the following commands in `Terminal` to
|
||||
|
||||
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
|
||||
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 1.9 - 28 Dec 2013
|
||||
- Display GITBUCKET_HOME on the system settings page
|
||||
- Fix some bugs
|
||||
|
||||
### 1.8 - 30 Nov 2013
|
||||
- Add user and group deletion
|
||||
- Improve pull request performance
|
||||
- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
|
||||
- LDAP StartTLS support
|
||||
- Enable hard wrapping in Markdown
|
||||
- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
|
||||
- Fix some bugs
|
||||
|
||||
### 1.7 - 26 Oct 2013
|
||||
- Support working on Java6 in embedded Jetty mode
|
||||
- Add ```--host``` option to bind specified host name in embedded Jetty mode
|
||||
|
||||
20
contrib/macosx/gitbucket.plist
Normal file
20
contrib/macosx/gitbucket.plist
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>gitbucket</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/java</string>
|
||||
<string>-Dmail.smtp.starttls.enable=true</string>
|
||||
<string>-jar</string>
|
||||
<string>gitbucket.war</string>
|
||||
<string>--host=127.0.0.1</string>
|
||||
<string>--port=8080</string>
|
||||
<string>--https=true</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: gitbucket
|
||||
Summary: Github clone written with Scala.
|
||||
Version: 1.6
|
||||
Summary: GitHub clone written with Scala.
|
||||
Version: 1.7
|
||||
Release: 1%{?dist}
|
||||
License: Apache
|
||||
URL: https://github.com/takezoe/gitbucket
|
||||
@@ -15,7 +15,7 @@ Requires: java >= 1.7
|
||||
|
||||
%description
|
||||
|
||||
GitBucket is the easily installable Github clone written with Scala.
|
||||
GitBucket is the easily installable GitHub clone written with Scala.
|
||||
|
||||
|
||||
%install
|
||||
@@ -40,5 +40,8 @@ touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
||||
|
||||
|
||||
%changelog
|
||||
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
||||
- Version bump to v1.7.
|
||||
|
||||
* Thu Oct 17 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
||||
- First build.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import sbt._
|
||||
import Keys._
|
||||
import org.scalatra.sbt._
|
||||
import org.scalatra.sbt.PluginKeys._
|
||||
import sbt.ScalaVersion
|
||||
import twirl.sbt.TwirlPlugin._
|
||||
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
||||
|
||||
@@ -25,14 +23,14 @@ object MyBuild extends Build {
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||
),
|
||||
scalacOptions := Seq("-deprecation"),
|
||||
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.2.5",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.0.2",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
|
||||
"commons-io" % "commons-io" % "2.4",
|
||||
"org.pegdown" % "pegdown" % "1.4.1",
|
||||
"org.apache.commons" % "commons-compress" % "1.5",
|
||||
@@ -43,7 +41,7 @@ object MyBuild extends Build {
|
||||
"com.h2database" % "h2" % "1.3.173",
|
||||
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")),
|
||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
||||
"junit" % "junit" % "4.11" % "test"
|
||||
),
|
||||
EclipseKeys.withSource := true,
|
||||
|
||||
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
||||
set SCRIPT_DIR=%~dp0
|
||||
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
|
||||
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
|
||||
|
||||
2
sbt.sh
2
sbt.sh
@@ -1 +1 @@
|
||||
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
|
||||
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
|
||||
|
||||
@@ -27,6 +27,8 @@ public class JettyLauncher {
|
||||
contextPath = dim[1];
|
||||
} else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
|
||||
forceHttps = true;
|
||||
} else if(dim[0].equals("--gitbucket.home")){
|
||||
System.setProperty("gitbucket.home", dim[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
src/main/java/util/PatchUtil.java
Normal file
93
src/main/java/util/PatchUtil.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package util;
|
||||
|
||||
import org.eclipse.jgit.api.errors.PatchApplyException;
|
||||
import org.eclipse.jgit.diff.RawText;
|
||||
import org.eclipse.jgit.internal.JGitText;
|
||||
import org.eclipse.jgit.patch.FileHeader;
|
||||
import org.eclipse.jgit.patch.HunkHeader;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class helps to apply patch. Most of these code came from {@link org.eclipse.jgit.api.ApplyCommand}.
|
||||
*/
|
||||
public class PatchUtil {
|
||||
|
||||
public static String apply(String source, String patch, FileHeader fh)
|
||||
throws IOException, PatchApplyException {
|
||||
RawText rt = new RawText(source.getBytes("UTF-8"));
|
||||
List<String> oldLines = new ArrayList<String>(rt.size());
|
||||
for (int i = 0; i < rt.size(); i++)
|
||||
oldLines.add(rt.getString(i));
|
||||
List<String> newLines = new ArrayList<String>(oldLines);
|
||||
for (HunkHeader hh : fh.getHunks()) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(patch.getBytes("UTF-8"), hh.getStartOffset(), hh.getEndOffset() - hh.getStartOffset());
|
||||
RawText hrt = new RawText(out.toByteArray());
|
||||
List<String> hunkLines = new ArrayList<String>(hrt.size());
|
||||
for (int i = 0; i < hrt.size(); i++)
|
||||
hunkLines.add(hrt.getString(i));
|
||||
int pos = 0;
|
||||
for (int j = 1; j < hunkLines.size(); j++) {
|
||||
String hunkLine = hunkLines.get(j);
|
||||
switch (hunkLine.charAt(0)) {
|
||||
case ' ':
|
||||
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
|
||||
hunkLine.substring(1))) {
|
||||
throw new PatchApplyException(MessageFormat.format(
|
||||
JGitText.get().patchApplyException, hh));
|
||||
}
|
||||
pos++;
|
||||
break;
|
||||
case '-':
|
||||
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
|
||||
hunkLine.substring(1))) {
|
||||
throw new PatchApplyException(MessageFormat.format(
|
||||
JGitText.get().patchApplyException, hh));
|
||||
}
|
||||
newLines.remove(hh.getNewStartLine() - 1 + pos);
|
||||
break;
|
||||
case '+':
|
||||
newLines.add(hh.getNewStartLine() - 1 + pos,
|
||||
hunkLine.substring(1));
|
||||
pos++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isNoNewlineAtEndOfFile(fh))
|
||||
newLines.add(""); //$NON-NLS-1$
|
||||
if (!rt.isMissingNewlineAtEnd())
|
||||
oldLines.add(""); //$NON-NLS-1$
|
||||
if (!isChanged(oldLines, newLines))
|
||||
return null; // don't touch the file
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String l : newLines) {
|
||||
// don't bother handling line endings - if it was windows, the \r is
|
||||
// still there!
|
||||
sb.append(l).append('\n');
|
||||
}
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isChanged(List<String> ol, List<String> nl) {
|
||||
if (ol.size() != nl.size())
|
||||
return true;
|
||||
for (int i = 0; i < ol.size(); i++)
|
||||
if (!ol.get(i).equals(nl.get(i)))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isNoNewlineAtEndOfFile(FileHeader fh) {
|
||||
HunkHeader lastHunk = fh.getHunks().get(fh.getHunks().size() - 1);
|
||||
RawText lhrt = new RawText(lastHunk.getBuffer());
|
||||
return lhrt.getString(lhrt.size() - 1).equals(
|
||||
"\\ No newline at end of file"); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
1
src/main/resources/update/1_8.sql
Normal file
1
src/main/resources/update/1_8.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE ACCOUNT ADD COLUMN REMOVED BOOLEAN DEFAULT FALSE;
|
||||
@@ -1,9 +1,19 @@
|
||||
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
|
||||
import app._
|
||||
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||
import org.scalatra._
|
||||
import javax.servlet._
|
||||
import java.util.EnumSet
|
||||
|
||||
class ScalatraBootstrap extends LifeCycle {
|
||||
override def init(context: ServletContext) {
|
||||
// Register TransactionFilter and BasicAuthenticationFilter at first
|
||||
context.addFilter("transactionFilter", new TransactionFilter)
|
||||
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
||||
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
|
||||
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
|
||||
|
||||
// Register controllers
|
||||
context.mount(new IndexController, "/")
|
||||
context.mount(new SearchController, "/")
|
||||
context.mount(new FileUploadController, "/upload")
|
||||
@@ -20,7 +30,9 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
context.mount(new IssuesController, "/*")
|
||||
context.mount(new PullRequestsController, "/*")
|
||||
context.mount(new RepositorySettingsController, "/*")
|
||||
context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*")
|
||||
|
||||
// Create GITBUCKET_HOME directory if it does not exist
|
||||
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
||||
if(!dir.exists){
|
||||
dir.mkdirs()
|
||||
|
||||
@@ -6,6 +6,7 @@ import util.StringUtil._
|
||||
import util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.FlashMapSupport
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
class AccountController extends AccountControllerBase
|
||||
with SystemSettingsService with AccountService with RepositoryService with ActivityService
|
||||
@@ -97,6 +98,27 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:userName/_delete")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
|
||||
getAccountByUserName(userName, true).foreach { account =>
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
deleteRepository(userName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
}
|
||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
removeUserRelatedData(userName)
|
||||
|
||||
updateAccount(account.copy(isRemoved = true))
|
||||
}
|
||||
|
||||
session.invalidate
|
||||
redirect("/")
|
||||
})
|
||||
|
||||
get("/register"){
|
||||
if(loadSystemSettings().allowAccountRegistration){
|
||||
if(context.loginAccount.isDefined){
|
||||
|
||||
@@ -3,7 +3,7 @@ package app
|
||||
import _root_.util.Directory._
|
||||
import _root_.util.Implicits._
|
||||
import _root_.util.ControlUtil._
|
||||
import _root_.util.{FileUtil, Validations, Keys}
|
||||
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
|
||||
import org.scalatra._
|
||||
import org.scalatra.json._
|
||||
import org.json4s._
|
||||
@@ -15,12 +15,13 @@ import service.AccountService
|
||||
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
|
||||
import java.text.SimpleDateFormat
|
||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||
import org.scalatra.i18n._
|
||||
|
||||
/**
|
||||
* Provides generic features for controller implementations.
|
||||
*/
|
||||
abstract class ControllerBase extends ScalatraFilter
|
||||
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
|
||||
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations {
|
||||
|
||||
implicit val jsonFormats = DefaultFormats
|
||||
|
||||
@@ -37,7 +38,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||
if(account == null){
|
||||
// Redirect to login form
|
||||
httpResponse.sendRedirect(context + "/signin?" + path)
|
||||
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
|
||||
} else if(account.isAdmin){
|
||||
// H2 Console (administrators only)
|
||||
chain.doFilter(request, response)
|
||||
@@ -71,7 +72,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
action
|
||||
}
|
||||
|
||||
override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
|
||||
override def ajaxGet[T](path : String, form : ValueType[T])(action : T => Any) : Route =
|
||||
super.ajaxGet(path, form){ form =>
|
||||
request.setAttribute(Keys.Request.Ajax, "true")
|
||||
action(form)
|
||||
@@ -83,7 +84,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
action
|
||||
}
|
||||
|
||||
override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
|
||||
override def ajaxPost[T](path : String, form : ValueType[T])(action : T => Any) : Route =
|
||||
super.ajaxPost(path, form){ form =>
|
||||
request.setAttribute(Keys.Request.Ajax, "true")
|
||||
action(form)
|
||||
@@ -106,7 +107,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
if(request.getMethod.toUpperCase == "POST"){
|
||||
org.scalatra.Unauthorized(redirect("/signin"))
|
||||
} else {
|
||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL))
|
||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(currentURL)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,13 +170,13 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
|
||||
}
|
||||
|
||||
protected def uniqueUserName: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
getAccountByUserName(value).map { _ => "User already exists." }
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getAccountByUserName(value, true).map { _ => "User already exists." }
|
||||
}
|
||||
|
||||
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
getAccountByMailAddress(value)
|
||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||
getAccountByMailAddress(value, true)
|
||||
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
||||
.map { _ => "Mail address is already registered." }
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import util._
|
||||
import service._
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
class CreateRepositoryController extends CreateRepositoryControllerBase
|
||||
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
||||
@@ -73,28 +73,26 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
JGitUtil.initRepository(gitdir)
|
||||
|
||||
if(form.createReadme){
|
||||
FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir =>
|
||||
// Clone the repository
|
||||
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
|
||||
using(Git.open(gitdir)){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
val content = if(form.description.nonEmpty){
|
||||
form.name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
form.description.get
|
||||
} else {
|
||||
form.name + "\n" +
|
||||
"===============\n"
|
||||
}
|
||||
|
||||
// Create README.md
|
||||
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
|
||||
if(form.description.nonEmpty){
|
||||
form.name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
form.description.get
|
||||
} else {
|
||||
form.name + "\n" +
|
||||
"===============\n"
|
||||
}, "UTF-8")
|
||||
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
|
||||
val git = Git.open(tmpdir)
|
||||
git.add.addFilepattern("README.md").call
|
||||
git.commit
|
||||
.setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
.setMessage("Initial commit").call
|
||||
git.push.call
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +174,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
private def existsAccount: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||
}
|
||||
|
||||
@@ -184,7 +182,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
* Duplicate check for the repository name.
|
||||
*/
|
||||
private def unique: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||
params.get("owner").flatMap { userName =>
|
||||
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ trait IndexControllerBase extends ControllerBase {
|
||||
get("/_user/proposals")(usersOnly {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package app
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import service._
|
||||
import util.CollaboratorsAuthenticator
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
class LabelsController extends LabelsControllerBase
|
||||
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
|
||||
@@ -51,7 +52,7 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
* Constraint for the identifier such as user name, repository name or page name.
|
||||
*/
|
||||
private def labelName: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(!value.matches("^[^,]+$")){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
|
||||
@@ -4,29 +4,32 @@ import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticat
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import util.FileUtil._
|
||||
import service._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import org.apache.commons.io.FileUtils
|
||||
import scala.collection.JavaConverters._
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
|
||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||
import service.IssuesService._
|
||||
import service.PullRequestService._
|
||||
import util.JGitUtil.DiffInfo
|
||||
import service.RepositoryService.RepositoryTreeNode
|
||||
import util.JGitUtil.CommitInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.eclipse.jgit.merge.MergeStrategy
|
||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import service.WebHookService.WebHookPayload
|
||||
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService with WebHookService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
|
||||
trait PullRequestsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService
|
||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService with WebHookService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||
|
||||
val pullRequestForm = mapping(
|
||||
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
||||
"content" -> trim(label("Content", optional(text()))),
|
||||
@@ -91,80 +94,96 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
pulls.html.mergeguide(
|
||||
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch),
|
||||
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
|
||||
pullreq,
|
||||
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
params("id").toIntOpt.flatMap { issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
LockUtil.lock(s"${owner}/${name}/merge"){
|
||||
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
|
||||
val remote = getRepositoryDir(owner, name)
|
||||
withTmpDir(new java.io.File(getTemporaryDir(owner, name), s"merge-${issueId}")){ tmpdir =>
|
||||
using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call){ git =>
|
||||
using(Git.open(getRepositoryDir(owner, name))) { git =>
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(owner, name, issueId, true)
|
||||
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(owner, name, issueId, true)
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// fetch pull request to temporary working repository
|
||||
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
|
||||
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(owner, name).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
|
||||
|
||||
// merge pull request
|
||||
git.checkout.setName(pullreq.branch).call
|
||||
|
||||
val result = git.merge
|
||||
.include(git.getRepository.resolve(pullRequestBranchName))
|
||||
.setFastForward(FastForwardMode.NO_FF)
|
||||
.setCommit(false)
|
||||
.call
|
||||
|
||||
if(result.getConflicts != null){
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
// merge commit
|
||||
git.getRepository.writeMergeCommitMsg(
|
||||
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
|
||||
+ form.message)
|
||||
|
||||
git.commit
|
||||
.setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
.call
|
||||
|
||||
// push
|
||||
git.push.call
|
||||
|
||||
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
commits.flatten.foreach { commit =>
|
||||
if(!existsCommitId(owner, name, commit.id)){
|
||||
insertCommitId(owner, name, commit.id)
|
||||
}
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
// merge
|
||||
val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
|
||||
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
val conflicted = try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
if (conflicted) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
// creates merge commit
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(merger.getResultTreeId)
|
||||
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
|
||||
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
mergeCommit.setAuthor(personIdent)
|
||||
mergeCommit.setCommitter(personIdent)
|
||||
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n\n" +
|
||||
form.message)
|
||||
|
||||
// insertObject and got mergeCommit Object Id
|
||||
val inserter = git.getRepository.newObjectInserter
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
|
||||
// update refs
|
||||
val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(false)
|
||||
refUpdate.setRefLogIdent(personIdent)
|
||||
refUpdate.setRefLogMessage("merged", true)
|
||||
refUpdate.update()
|
||||
|
||||
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
commits.flatten.foreach { commit =>
|
||||
if(!existsCommitId(owner, name, commit.id)){
|
||||
insertCommitId(owner, name, commit.id)
|
||||
}
|
||||
}
|
||||
|
||||
// call web hook
|
||||
val webHookURLs = getWebHookURLs(owner, name)
|
||||
if(webHookURLs.nonEmpty){
|
||||
val payload = WebHookPayload(
|
||||
git,
|
||||
loginAccount,
|
||||
mergeBaseRefName,
|
||||
repository,
|
||||
commits.flatten.toList,
|
||||
getAccountByUserName(owner).get)
|
||||
|
||||
callWebHook(owner, name, webHookURLs, payload)
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,23 +334,52 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
*/
|
||||
private def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
||||
// TODO Are there more quick way?
|
||||
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
|
||||
val remote = getRepositoryDir(userName, repositoryName)
|
||||
withTmpDir(new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")){ tmpdir =>
|
||||
using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call){ git =>
|
||||
|
||||
git.checkout.setName(branch).call
|
||||
|
||||
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${branch}"
|
||||
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
||||
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||
try {
|
||||
// fetch objects from origin repository branch
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
|
||||
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
||||
.setRefSpecs(refSpec)
|
||||
.call
|
||||
|
||||
val result = git.merge
|
||||
.include(git.getRepository.resolve("FETCH_HEAD"))
|
||||
.setCommit(false).call
|
||||
// merge conflict check
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
|
||||
val mergeTip = git.getRepository.resolve(tmpRefName)
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
} finally {
|
||||
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
||||
refUpdate.setForceUpdate(true)
|
||||
refUpdate.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.getConflicts != null
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
|
||||
*/
|
||||
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String,
|
||||
issueId: Int): Boolean = {
|
||||
LockUtil.lock(s"${userName}/${repositoryName}/merge") {
|
||||
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||
// merge
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import util.{UsersAuthenticator, OwnerAuthenticator}
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.FlashMapSupport
|
||||
import org.scalatra.i18n.Messages
|
||||
import service.WebHookService.WebHookPayload
|
||||
import util.JGitUtil.CommitInfo
|
||||
import util.ControlUtil._
|
||||
@@ -139,13 +140,15 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
|
||||
val webHookURLs = getWebHookURLs(repository.owner, repository.name)
|
||||
if(webHookURLs.nonEmpty){
|
||||
val owner = getAccountByUserName(repository.owner).get
|
||||
callWebHook(repository.owner, repository.name, webHookURLs,
|
||||
WebHookPayload(
|
||||
git,
|
||||
owner,
|
||||
"refs/heads/" + repository.repository.defaultBranch,
|
||||
repository,
|
||||
commits.toList,
|
||||
getAccountByUserName(repository.owner).get))
|
||||
owner))
|
||||
}
|
||||
|
||||
flash += "info" -> "Test payload deployed!"
|
||||
@@ -177,7 +180,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
* Provides duplication check for web hook url.
|
||||
*/
|
||||
private def webHook: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
|
||||
}
|
||||
|
||||
@@ -185,7 +188,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
* Provides Constraint to validate the collaborator name.
|
||||
*/
|
||||
private def collaborator: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getAccountByUserName(value) match {
|
||||
case None => Some("User does not exist.")
|
||||
case Some(x) if(x.isGroupAccount)
|
||||
|
||||
@@ -232,6 +232,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
(id, path.substring(id.length).replaceFirst("^/", ""))
|
||||
}
|
||||
|
||||
|
||||
private val readmeFiles = Seq("readme.md", "readme.markdown")
|
||||
|
||||
/**
|
||||
* Provides HTML of the file list.
|
||||
*
|
||||
@@ -251,8 +254,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
// process README.md
|
||||
val readme = files.find(_.name == "README.md").map { file =>
|
||||
// process README.md or README.markdown
|
||||
val readme = files.find { file =>
|
||||
readmeFiles.contains(file.name.toLowerCase)
|
||||
}.map { file =>
|
||||
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
|
||||
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
|
||||
redirect("/")
|
||||
} else {
|
||||
redirect(urlEncode(redirectUrl).replaceAll("%2F", "/"))
|
||||
redirect(redirectUrl)
|
||||
}
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
|
||||
@@ -33,7 +33,9 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
"bindPassword" -> trim(label("Bind Password", optional(text()))),
|
||||
"baseDN" -> trim(label("Base DN", text(required))),
|
||||
"userNameAttribute" -> trim(label("User name attribute", text(required))),
|
||||
"mailAttribute" -> trim(label("Mail address attribute", text(required)))
|
||||
"mailAttribute" -> trim(label("Mail address attribute", text(required))),
|
||||
"tls" -> trim(label("Enable TLS", optional(boolean()))),
|
||||
"keystore" -> trim(label("Keystore", optional(text())))
|
||||
)(Ldap.apply))
|
||||
)(SystemSettings.apply)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import util.AdminAuthenticator
|
||||
import util.StringUtil._
|
||||
import util.ControlUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.Directory._
|
||||
|
||||
class UserManagementController extends UserManagementControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
@@ -17,57 +19,61 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
url: Option[String], fileId: Option[String])
|
||||
|
||||
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||
mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String])
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String], clearImage: Boolean)
|
||||
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
val newUserForm = mapping(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text())))
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||
)(NewUserForm.apply)
|
||||
|
||||
val editUserForm = mapping(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
|
||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
||||
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
||||
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
)(EditUserForm.apply)
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" , optional(text())))
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" ,optional(text())))
|
||||
)(NewGroupForm.apply)
|
||||
|
||||
val editGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
val users = getAllUsers()
|
||||
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||
val users = getAllUsers(includeRemoved)
|
||||
|
||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName)
|
||||
}.toMap
|
||||
admin.users.html.list(users, members)
|
||||
admin.users.html.list(users, members, includeRemoved)
|
||||
})
|
||||
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
@@ -82,18 +88,32 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||
val userName = params("userName")
|
||||
admin.users.html.user(getAccountByUserName(userName))
|
||||
admin.users.html.user(getAccountByUserName(userName, true))
|
||||
})
|
||||
|
||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { account =>
|
||||
updateAccount(getAccountByUserName(userName).get.copy(
|
||||
getAccountByUserName(userName, true).map { account =>
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
deleteRepository(userName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
}
|
||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
removeUserRelatedData(userName)
|
||||
}
|
||||
|
||||
updateAccount(account.copy(
|
||||
password = form.password.map(sha1).getOrElse(account.password),
|
||||
fullName = form.fullName,
|
||||
mailAddress = form.mailAddress,
|
||||
isAdmin = form.isAdmin,
|
||||
url = form.url))
|
||||
url = form.url,
|
||||
isRemoved = form.isRemoved))
|
||||
|
||||
updateImage(userName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
@@ -114,20 +134,34 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
|
||||
admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
}
|
||||
})
|
||||
|
||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
|
||||
getAccountByUserName(groupName).map { account =>
|
||||
updateGroup(groupName, form.url)
|
||||
updateGroupMembers(form.groupName, memberNames)
|
||||
getAccountByUserName(groupName, true).map { account =>
|
||||
updateGroup(groupName, form.url, form.isRemoved)
|
||||
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
memberNames.foreach { userName =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
if(form.isRemoved){
|
||||
// Remove from GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, Nil)
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
deleteRepository(groupName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
||||
}
|
||||
} else {
|
||||
// Update GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, memberNames)
|
||||
// Update COLLABORATOR for group repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
memberNames.foreach { userName =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ import util.ControlUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.scalatra.FlashMapSupport
|
||||
import service.WikiService.WikiPageInfo
|
||||
import org.scalatra.i18n.Messages
|
||||
import scala.Some
|
||||
import java.util.ResourceBundle
|
||||
|
||||
class WikiController extends WikiControllerBase
|
||||
with WikiService with RepositoryService with AccountService with ActivityService
|
||||
@@ -66,7 +67,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository,
|
||||
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||
}
|
||||
})
|
||||
@@ -96,7 +97,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/}")
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/")
|
||||
} else {
|
||||
flash += "info" -> "This patch was not able to be reversed."
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
|
||||
@@ -170,12 +171,12 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
})
|
||||
|
||||
private def unique: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
|
||||
}
|
||||
|
||||
private def pagename: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(value.exists("\\/:*?\"<>|".contains(_))){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
@@ -186,17 +187,17 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
}
|
||||
|
||||
private def conflictForNew: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] = {
|
||||
optionIf(targetWikiPage.nonEmpty){
|
||||
Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.")
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
targetWikiPage.map { _ =>
|
||||
"Someone has created the wiki since you started. Please reload this page and re-apply your changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def conflictForEdit: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] = {
|
||||
optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(true)){
|
||||
Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.")
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
targetWikiPage.filter(_.id != params("id")).map{ _ =>
|
||||
"Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ object Accounts extends Table[Account]("ACCOUNT") {
|
||||
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
||||
def image = column[String]("IMAGE")
|
||||
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
||||
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
|
||||
def removed = column[Boolean]("REMOVED")
|
||||
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount ~ removed <> (Account, Account.unapply _)
|
||||
}
|
||||
|
||||
case class Account(
|
||||
@@ -28,5 +29,6 @@ case class Account(
|
||||
updatedDate: java.util.Date,
|
||||
lastLoginDate: Option[java.util.Date],
|
||||
image: Option[String],
|
||||
isGroupAccount: Boolean
|
||||
isGroupAccount: Boolean,
|
||||
isRemoved: Boolean
|
||||
)
|
||||
|
||||
@@ -51,13 +51,18 @@ trait AccountService {
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByUserName(userName: String): Option[Account] =
|
||||
Query(Accounts) filter(_.userName is userName.bind) firstOption
|
||||
def getAccountByUserName(userName: String, includeRemoved: Boolean = false): Option[Account] =
|
||||
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
|
||||
|
||||
def getAccountByMailAddress(mailAddress: String): Option[Account] =
|
||||
Query(Accounts) filter(_.mailAddress is mailAddress.bind) firstOption
|
||||
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
|
||||
Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
|
||||
|
||||
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
|
||||
def getAllUsers(includeRemoved: Boolean = true): List[Account] =
|
||||
if(includeRemoved){
|
||||
Query(Accounts) sortBy(_.userName) list
|
||||
} else {
|
||||
Query(Accounts) filter (_.removed is false.bind) sortBy(_.userName) list
|
||||
}
|
||||
|
||||
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
|
||||
Accounts insert Account(
|
||||
@@ -71,12 +76,13 @@ trait AccountService {
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = false)
|
||||
isGroupAccount = false,
|
||||
isRemoved = false)
|
||||
|
||||
def updateAccount(account: Account): Unit =
|
||||
Accounts
|
||||
.filter { a => a.userName is account.userName.bind }
|
||||
.map { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? }
|
||||
.map { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? ~ a.removed }
|
||||
.update (
|
||||
account.password,
|
||||
account.fullName,
|
||||
@@ -85,7 +91,8 @@ trait AccountService {
|
||||
account.url,
|
||||
account.registeredDate,
|
||||
currentDate,
|
||||
account.lastLoginDate)
|
||||
account.lastLoginDate,
|
||||
account.isRemoved)
|
||||
|
||||
def updateAvatarImage(userName: String, image: Option[String]): Unit =
|
||||
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
|
||||
@@ -105,10 +112,11 @@ trait AccountService {
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = true)
|
||||
isGroupAccount = true,
|
||||
isRemoved = false)
|
||||
|
||||
def updateGroup(groupName: String, url: Option[String]): Unit =
|
||||
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
|
||||
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
|
||||
Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
|
||||
|
||||
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
|
||||
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
|
||||
@@ -131,6 +139,12 @@ trait AccountService {
|
||||
.map(_.groupName)
|
||||
.list
|
||||
|
||||
def removeUserRelatedData(userName: String): Unit = {
|
||||
Query(GroupMembers).filter(_.userName is userName.bind).delete
|
||||
Query(Collaborators).filter(_.collaboratorName is userName.bind).delete
|
||||
Query(Repositories).filter(_.userName is userName.bind).delete
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object AccountService extends AccountService
|
||||
object AccountService extends AccountService
|
||||
@@ -15,6 +15,9 @@ trait PullRequestService { self: IssuesService =>
|
||||
}
|
||||
}
|
||||
|
||||
def updateCommitIdTo(owner: String, repository: String, issueId: Int, commitIdTo: String): Unit =
|
||||
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdTo).update(commitIdTo)
|
||||
|
||||
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
|
||||
Query(PullRequests)
|
||||
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
@@ -43,6 +46,18 @@ trait PullRequestService { self: IssuesService =>
|
||||
commitIdFrom,
|
||||
commitIdTo))
|
||||
|
||||
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean): List[PullRequest] =
|
||||
Query(PullRequests)
|
||||
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
.filter { case (t1, t2) =>
|
||||
(t1.requestUserName is userName.bind) &&
|
||||
(t1.requestRepositoryName is repositoryName.bind) &&
|
||||
(t1.requestBranch is branch.bind) &&
|
||||
(t2.closed is closed.bind)
|
||||
}
|
||||
.map { case (t1, t2) => t1 }
|
||||
.list
|
||||
|
||||
}
|
||||
|
||||
object PullRequestService {
|
||||
|
||||
@@ -46,8 +46,8 @@ trait RepositoryService { self: AccountService =>
|
||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||
@@ -120,7 +120,7 @@ trait RepositoryService { self: AccountService =>
|
||||
case Some(x) if(x.isAdmin) => Query(Repositories)
|
||||
// for Normal Users
|
||||
case Some(x) if(!x.isAdmin) =>
|
||||
Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
|
||||
Query(Repositories) filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) ||
|
||||
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
|
||||
}
|
||||
// for Guests
|
||||
|
||||
@@ -32,6 +32,8 @@ trait SystemSettingsService {
|
||||
props.setProperty(LdapBaseDN, ldap.baseDN)
|
||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
|
||||
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
||||
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
||||
}
|
||||
}
|
||||
props.store(new java.io.FileOutputStream(GitBucketConf), null)
|
||||
@@ -69,7 +71,9 @@ trait SystemSettingsService {
|
||||
getOptionValue(props, LdapBindPassword, None),
|
||||
getValue(props, LdapBaseDN, ""),
|
||||
getValue(props, LdapUserNameAttribute, ""),
|
||||
getValue(props, LdapMailAddressAttribute, "")))
|
||||
getValue(props, LdapMailAddressAttribute, ""),
|
||||
getOptionValue[Boolean](props, LdapTls, None),
|
||||
getOptionValue(props, LdapKeystore, None)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -97,7 +101,9 @@ object SystemSettingsService {
|
||||
bindPassword: Option[String],
|
||||
baseDN: String,
|
||||
userNameAttribute: String,
|
||||
mailAttribute: String)
|
||||
mailAttribute: String,
|
||||
tls: Option[Boolean],
|
||||
keystore: Option[String])
|
||||
|
||||
case class Smtp(
|
||||
host: String,
|
||||
@@ -129,6 +135,8 @@ object SystemSettingsService {
|
||||
private val LdapBaseDN = "ldap.baseDN"
|
||||
private val LdapUserNameAttribute = "ldap.username_attribute"
|
||||
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
||||
private val LdapTls = "ldap.tls"
|
||||
private val LdapKeystore = "ldap.keystore"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
|
||||
defining(props.getProperty(key)){ value =>
|
||||
|
||||
@@ -74,14 +74,16 @@ trait WebHookService {
|
||||
object WebHookService {
|
||||
|
||||
case class WebHookPayload(
|
||||
pusher: WebHookUser,
|
||||
ref: String,
|
||||
commits: List[WebHookCommit],
|
||||
repository: WebHookRepository)
|
||||
|
||||
object WebHookPayload {
|
||||
def apply(git: Git, refName: String, repositoryInfo: RepositoryInfo,
|
||||
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
|
||||
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
|
||||
WebHookPayload(
|
||||
WebHookUser(pusher.fullName, pusher.mailAddress),
|
||||
refName,
|
||||
commits.map { commit =>
|
||||
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package service
|
||||
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.{StringUtil, Directory, JGitUtil, LockUtil}
|
||||
import util.ControlUtil._
|
||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
||||
import org.eclipse.jgit.diff.DiffFormatter
|
||||
import org.eclipse.jgit.api.errors.PatchApplyException
|
||||
import util.{PatchUtil, Directory, JGitUtil, LockUtil}
|
||||
import _root_.util.ControlUtil._
|
||||
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry}
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
|
||||
import java.io.ByteArrayInputStream
|
||||
import org.eclipse.jgit.patch._
|
||||
import org.eclipse.jgit.api.errors.PatchFormatException
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
|
||||
object WikiService {
|
||||
|
||||
@@ -42,13 +48,8 @@ trait WikiService {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
|
||||
if(!dir.exists){
|
||||
try {
|
||||
JGitUtil.initRepository(dir)
|
||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
|
||||
} finally {
|
||||
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
|
||||
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
|
||||
}
|
||||
JGitUtil.initRepository(dir)
|
||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,11 +59,11 @@ trait WikiService {
|
||||
*/
|
||||
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
optionIf(!JGitUtil.isEmpty(git)){
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
||||
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId)
|
||||
}
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ trait WikiService {
|
||||
*/
|
||||
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
optionIf(!JGitUtil.isEmpty(git)){
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
val index = path.lastIndexOf('/')
|
||||
val parentPath = if(index < 0) "." else path.substring(0, index)
|
||||
val fileName = if(index < 0) path else path.substring(index + 1)
|
||||
@@ -79,7 +80,7 @@ trait WikiService {
|
||||
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
|
||||
git.getRepository.open(file.id).getBytes
|
||||
}
|
||||
}
|
||||
} else None
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,12 +100,13 @@ trait WikiService {
|
||||
*/
|
||||
def revertWikiPage(owner: String, repository: String, from: String, to: String,
|
||||
committer: model.Account, pageName: Option[String]): Boolean = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
|
||||
// clone working copy
|
||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
||||
|
||||
using(Git.open(workDir)){ git =>
|
||||
case class RevertInfo(operation: String, filePath: String, source: String)
|
||||
|
||||
try {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
|
||||
val reader = git.getRepository.newObjectReader
|
||||
val oldTreeIter = new CanonicalTreeParser
|
||||
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
|
||||
@@ -112,7 +114,6 @@ trait WikiService {
|
||||
val newTreeIter = new CanonicalTreeParser
|
||||
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
|
||||
pageName match {
|
||||
case Some(x) => diff.getNewPath == x + ".md"
|
||||
@@ -127,72 +128,135 @@ trait WikiService {
|
||||
new String(out.toByteArray, "UTF-8")
|
||||
}
|
||||
|
||||
try {
|
||||
git.apply.setPatch(new java.io.ByteArrayInputStream(patch.getBytes("UTF-8"))).call
|
||||
git.add.addFilepattern(".").call
|
||||
git.commit.setCommitter(committer.fullName, committer.mailAddress).setMessage(pageName match {
|
||||
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
|
||||
case None => s"Revert ${from} ... ${to}"
|
||||
}).call
|
||||
git.push.call
|
||||
true
|
||||
} catch {
|
||||
case ex: PatchApplyException => false
|
||||
val p = new Patch()
|
||||
p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
|
||||
if(!p.getErrors.isEmpty){
|
||||
throw new PatchFormatException(p.getErrors())
|
||||
}
|
||||
val revertInfo = (p.getFiles.asScala.map { fh =>
|
||||
fh.getChangeType match {
|
||||
case DiffEntry.ChangeType.MODIFY => {
|
||||
val source = getWikiPage(owner, repository, fh.getNewPath.replaceFirst("\\.md$", "")).map(_.content).getOrElse("")
|
||||
val applied = PatchUtil.apply(source, patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else Nil
|
||||
}
|
||||
case DiffEntry.ChangeType.ADD => {
|
||||
val applied = PatchUtil.apply("", patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else Nil
|
||||
}
|
||||
case DiffEntry.ChangeType.DELETE => {
|
||||
Seq(RevertInfo("DELETE", fh.getNewPath, ""))
|
||||
}
|
||||
case DiffEntry.ChangeType.RENAME => {
|
||||
val applied = PatchUtil.apply("", patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else {
|
||||
Seq(RevertInfo("DELETE", fh.getOldPath, ""))
|
||||
}
|
||||
}
|
||||
case _ => Nil
|
||||
}
|
||||
}).flatten
|
||||
|
||||
if(revertInfo.nonEmpty){
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||
val index = treeWalk.addTree(revWalk.parseTree(headId))
|
||||
treeWalk.setRecursive(true)
|
||||
while(treeWalk.next){
|
||||
val path = treeWalk.getPathString
|
||||
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
|
||||
if(revertInfo.find(x => x.filePath == path).isEmpty){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revertInfo.filter(_.operation == "ADD").foreach { x =>
|
||||
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
|
||||
}
|
||||
builder.finish()
|
||||
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||
pageName match {
|
||||
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
|
||||
case None => s"Revert ${from} ... ${to}"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save the wiki page.
|
||||
*/
|
||||
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
||||
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
|
||||
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
|
||||
// clone working copy
|
||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
var created = true
|
||||
var updated = false
|
||||
var removed = false
|
||||
|
||||
// write as file
|
||||
using(Git.open(workDir)){ git =>
|
||||
defining(new File(workDir, newPageName + ".md")){ file =>
|
||||
// new page
|
||||
val created = !file.exists
|
||||
|
||||
// created or updated
|
||||
val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
|
||||
FileUtils.writeStringToFile(file, content, "UTF-8")
|
||||
git.add.addFilepattern(file.getName).call
|
||||
}
|
||||
|
||||
// delete file
|
||||
val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){
|
||||
git.rm.addFilepattern(currentPageName + ".md").call
|
||||
}
|
||||
|
||||
// commit and push
|
||||
optionIf(added || deleted){
|
||||
defining(git.commit.setCommitter(committer.fullName, committer.mailAddress)
|
||||
.setMessage(if(message.trim.length == 0){
|
||||
if(deleted){
|
||||
s"Rename ${currentPageName} to ${newPageName}"
|
||||
} else if(created){
|
||||
s"Created ${newPageName}"
|
||||
} else {
|
||||
s"Updated ${newPageName}"
|
||||
}
|
||||
} else {
|
||||
message
|
||||
}).call){ commit =>
|
||||
git.push.call
|
||||
Some(commit.getName)
|
||||
if(headId != null){
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||
val index = treeWalk.addTree(revWalk.parseTree(headId))
|
||||
treeWalk.setRecursive(true)
|
||||
while(treeWalk.next){
|
||||
val path = treeWalk.getPathString
|
||||
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
|
||||
if(path == currentPageName + ".md" && currentPageName != newPageName){
|
||||
removed = true
|
||||
} else if(path != newPageName + ".md"){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
} else {
|
||||
created = false
|
||||
updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(created || updated || removed){
|
||||
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||
if(message.trim.length == 0) {
|
||||
if(removed){
|
||||
s"Rename ${currentPageName} to ${newPageName}"
|
||||
} else if(created){
|
||||
s"Created ${newPageName}"
|
||||
} else {
|
||||
s"Updated ${newPageName}"
|
||||
}
|
||||
} else {
|
||||
message
|
||||
})
|
||||
|
||||
Some(newHeadId)
|
||||
} else None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,36 +266,35 @@ trait WikiService {
|
||||
*/
|
||||
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
||||
committer: String, mailAddress: String, message: String): Unit = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
|
||||
// clone working copy
|
||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
var removed = false
|
||||
|
||||
// delete file
|
||||
new File(workDir, pageName + ".md").delete
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||
val index = treeWalk.addTree(revWalk.parseTree(headId))
|
||||
treeWalk.setRecursive(true)
|
||||
while(treeWalk.next){
|
||||
val path = treeWalk.getPathString
|
||||
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
|
||||
if(path != pageName + ".md"){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
} else {
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using(Git.open(workDir)){ git =>
|
||||
git.rm.addFilepattern(pageName + ".md").call
|
||||
|
||||
// commit and push
|
||||
git.commit.setCommitter(committer, mailAddress).setMessage(message).call
|
||||
git.push.call
|
||||
if(removed){
|
||||
builder.finish()
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
|
||||
if(!workDir.exists){
|
||||
Git.cloneRepository
|
||||
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
|
||||
.setDirectory(workDir)
|
||||
.call
|
||||
.getRepository
|
||||
.close
|
||||
} else using(Git.open(workDir)){ git =>
|
||||
git.pull.call
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ object AutoUpdate {
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
Version(1, 9),
|
||||
Version(1, 8),
|
||||
Version(1, 7),
|
||||
Version(1, 6),
|
||||
Version(1, 5),
|
||||
@@ -86,7 +88,7 @@ object AutoUpdate {
|
||||
/**
|
||||
* The version file (GITBUCKET_HOME/version).
|
||||
*/
|
||||
val versionFile = new File(GitBucketHome, "version")
|
||||
lazy val versionFile = new File(GitBucketHome, "version")
|
||||
|
||||
/**
|
||||
* Returns the current version from the version file.
|
||||
@@ -101,11 +103,8 @@ object AutoUpdate {
|
||||
}
|
||||
case _ => Version(0, 0)
|
||||
}
|
||||
} else {
|
||||
Version(0, 0)
|
||||
}
|
||||
|
||||
}
|
||||
} else Version(0, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -117,8 +116,12 @@ class AutoUpdateListener extends ServletContextListener {
|
||||
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
||||
|
||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||
val datadir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||
if(datadir != null){
|
||||
System.setProperty("gitbucket.home", datadir)
|
||||
}
|
||||
org.h2.Driver.load()
|
||||
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
|
||||
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};AUTO_SERVER=TRUE")
|
||||
|
||||
logger.debug("Start schema update")
|
||||
defining(getConnection(event.getServletContext)){ conn =>
|
||||
|
||||
@@ -43,7 +43,7 @@ class GitRepositoryServlet extends GitServlet {
|
||||
|
||||
def getServletContext(): ServletContext = config.getServletContext
|
||||
def getServletName(): String = config.getServletName
|
||||
});
|
||||
})
|
||||
|
||||
super.init(config)
|
||||
}
|
||||
@@ -78,7 +78,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
class CommitLogHook(owner: String, repository: String, userName: String, baseURL: String) extends PostReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with WebHookService {
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||
|
||||
@@ -97,17 +97,17 @@ class CommitLogHook(owner: String, repository: String, userName: String, baseURL
|
||||
val newCommits = if(commits.size > 1000){
|
||||
val existIds = getAllCommitIds(owner, repository)
|
||||
commits.flatMap { commit =>
|
||||
optionIf(!existIds.contains(commit.id)){
|
||||
if(!existIds.contains(commit.id)){
|
||||
createIssueComment(commit)
|
||||
Some(commit)
|
||||
}
|
||||
} else None
|
||||
}
|
||||
} else {
|
||||
commits.flatMap { commit =>
|
||||
optionIf(!existsCommitId(owner, repository, commit.id)){
|
||||
if(!existsCommitId(owner, repository, commit.id)){
|
||||
createIssueComment(commit)
|
||||
Some(commit)
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +130,22 @@ class CommitLogHook(owner: String, repository: String, userName: String, baseURL
|
||||
}
|
||||
}
|
||||
|
||||
if(refName(1) == "heads"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE |
|
||||
ReceiveCommand.Type.UPDATE |
|
||||
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
|
||||
updatePullRequests(branchName)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
// call web hook
|
||||
val webHookURLs = getWebHookURLs(owner, repository)
|
||||
if(webHookURLs.nonEmpty){
|
||||
val payload = WebHookPayload(
|
||||
git,
|
||||
getAccountByUserName(userName).get,
|
||||
command.getRefName,
|
||||
getRepository(owner, repository, baseURL).get,
|
||||
newCommits,
|
||||
@@ -157,4 +168,21 @@ class CommitLogHook(owner: String, repository: String, userName: String, baseURL
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
|
||||
*/
|
||||
private def updatePullRequests(branch: String) =
|
||||
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
|
||||
if(getRepository(pullreq.userName, pullreq.repositoryName, baseURL).isDefined){
|
||||
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
|
||||
git.fetch
|
||||
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
|
||||
.call
|
||||
|
||||
val commitIdTo = git.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
|
||||
updateCommitIdTo(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package util
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.treewalk.TreeWalk
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import scala.language.reflectiveCalls
|
||||
|
||||
/**
|
||||
* Provides control facilities.
|
||||
@@ -23,12 +25,12 @@ object ControlUtil {
|
||||
}
|
||||
|
||||
def using[T](git: Git)(f: Git => T): T =
|
||||
try f(git) finally git.getRepository.close
|
||||
try f(git) finally git.getRepository.close()
|
||||
|
||||
def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
|
||||
try f(git1, git2) finally {
|
||||
git1.getRepository.close
|
||||
git2.getRepository.close
|
||||
git1.getRepository.close()
|
||||
git2.getRepository.close()
|
||||
}
|
||||
|
||||
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
|
||||
@@ -37,12 +39,15 @@ object ControlUtil {
|
||||
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
|
||||
try f(treeWalk) finally treeWalk.release()
|
||||
|
||||
def executeIf(condition: => Boolean)(action: => Unit): Boolean =
|
||||
if(condition){
|
||||
action
|
||||
true
|
||||
} else false
|
||||
|
||||
def optionIf[T](condition: => Boolean)(action: => Option[T]): Option[T] =
|
||||
if(condition) action else None
|
||||
// def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
|
||||
// try {
|
||||
// f(ref)
|
||||
// } finally {
|
||||
// val refUpdate = git.getRepository.updateRef(ref.getDestination)
|
||||
// refUpdate.setForceUpdate(true)
|
||||
// refUpdate.delete()
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -8,9 +8,15 @@ import util.ControlUtil._
|
||||
*/
|
||||
object Directory {
|
||||
|
||||
val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
|
||||
case Some(env) => new File(env)
|
||||
case None => new File(System.getProperty("user.home"), "gitbucket")
|
||||
val GitBucketHome = (System.getProperty("gitbucket.home") match {
|
||||
// -Dgitbucket.home=<path>
|
||||
case path if(path != null) => new File(path)
|
||||
case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
|
||||
// environment variable GITBUCKET_HOME
|
||||
case Some(env) => new File(env)
|
||||
// default is HOME/gitbucket
|
||||
case None => new File(System.getProperty("user.home"), "gitbucket")
|
||||
}
|
||||
}).getAbsolutePath
|
||||
|
||||
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
|
||||
@@ -56,25 +62,10 @@ object Directory {
|
||||
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
|
||||
new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
|
||||
|
||||
/**
|
||||
* Temporary directory which is used in the repository creation.
|
||||
*
|
||||
* GitBucket generates initial repository contents in this directory and push them.
|
||||
* This directory is removed after the repository creation.
|
||||
*/
|
||||
def getInitRepositoryDir(owner: String, repository: String): File =
|
||||
new File(getTemporaryDir(owner, repository), "init")
|
||||
|
||||
/**
|
||||
* Substance directory of the wiki repository.
|
||||
*/
|
||||
def getWikiRepositoryDir(owner: String, repository: String): File =
|
||||
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
|
||||
|
||||
/**
|
||||
* Wiki working directory which is cloned from the wiki repository.
|
||||
*/
|
||||
def getWikiWorkDir(owner: String, repository: String): File =
|
||||
new File(getTemporaryDir(owner, repository), "wiki")
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.eclipse.jgit.errors.MissingObjectException
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.errors.NoHeadException
|
||||
import service.RepositoryService
|
||||
import org.eclipse.jgit.dircache.DirCacheEntry
|
||||
|
||||
/**
|
||||
* Provides complex JGit operations.
|
||||
@@ -78,9 +79,9 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
|
||||
optionIf(i >= 0){
|
||||
if(i >= 0){
|
||||
Some(fullMessage.trim.substring(i).trim)
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,4 +465,33 @@ object JGitUtil {
|
||||
}.find(_._1 != null)
|
||||
}
|
||||
|
||||
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,
|
||||
fullName: String, mailAddress: String, message: String): String = {
|
||||
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.release()
|
||||
|
||||
val refUpdate = git.getRepository.updateRef(Constants.HEAD)
|
||||
refUpdate.setNewObjectId(newHeadId)
|
||||
refUpdate.update()
|
||||
|
||||
newHeadId.getName
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package util
|
||||
import util.ControlUtil._
|
||||
import service.SystemSettingsService
|
||||
import com.novell.ldap._
|
||||
import java.security.Security
|
||||
import org.slf4j.LoggerFactory
|
||||
import service.SystemSettingsService.Ldap
|
||||
import scala.annotation.tailrec
|
||||
|
||||
@@ -11,7 +13,8 @@ import scala.annotation.tailrec
|
||||
*/
|
||||
object LDAPUtil {
|
||||
|
||||
private val LDAP_VERSION: Int = 3
|
||||
private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
|
||||
private val logger = LoggerFactory.getLogger(getClass().getName())
|
||||
|
||||
/**
|
||||
* Try authentication by LDAP using given configuration.
|
||||
@@ -22,7 +25,9 @@ object LDAPUtil {
|
||||
ldapSettings.host,
|
||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
ldapSettings.bindDN.getOrElse(""),
|
||||
ldapSettings.bindPassword.getOrElse("")
|
||||
ldapSettings.bindPassword.getOrElse(""),
|
||||
ldapSettings.tls.getOrElse(false),
|
||||
ldapSettings.keystore.getOrElse("")
|
||||
) match {
|
||||
case Some(conn) => {
|
||||
withConnection(conn) { conn =>
|
||||
@@ -41,7 +46,9 @@ object LDAPUtil {
|
||||
ldapSettings.host,
|
||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
userDN,
|
||||
password
|
||||
password,
|
||||
ldapSettings.tls.getOrElse(false),
|
||||
ldapSettings.keystore.getOrElse("")
|
||||
) match {
|
||||
case Some(conn) => {
|
||||
withConnection(conn) { conn =>
|
||||
@@ -55,15 +62,41 @@ object LDAPUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = {
|
||||
val conn: LDAPConnection = new LDAPConnection
|
||||
private def bind(host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String): Option[LDAPConnection] = {
|
||||
if (tls) {
|
||||
// Dynamically set Sun as the security provider
|
||||
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider())
|
||||
|
||||
if (keystore.compareTo("") != 0) {
|
||||
// Dynamically set the property that JSSE uses to identify
|
||||
// the keystore that holds trusted root certificates
|
||||
System.setProperty("javax.net.ssl.trustStore", keystore)
|
||||
}
|
||||
}
|
||||
|
||||
val conn: LDAPConnection = new LDAPConnection(new LDAPJSSEStartTLSFactory())
|
||||
try {
|
||||
// Connect to the server
|
||||
conn.connect(host, port)
|
||||
|
||||
if (tls) {
|
||||
// Secure the connection
|
||||
conn.startTLS()
|
||||
}
|
||||
|
||||
// Bind to the server
|
||||
conn.bind(LDAP_VERSION, dn, password.getBytes)
|
||||
|
||||
Some(conn)
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
if (conn.isConnected) conn.disconnect()
|
||||
// Provide more information if something goes wrong
|
||||
logger.info("" + e)
|
||||
|
||||
if (conn.isConnected) {
|
||||
conn.disconnect()
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -97,8 +130,8 @@ object LDAPUtil {
|
||||
|
||||
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
|
||||
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
|
||||
optionIf (results.hasMore) {
|
||||
if(results.hasMore) {
|
||||
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
.foreach { case (address, name) =>
|
||||
email.setFrom(address, name)
|
||||
}
|
||||
email.setCharset("UTF-8")
|
||||
email.setSubject(subject)
|
||||
email.setHtmlMsg(msg)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
trait Validations {
|
||||
|
||||
@@ -8,7 +9,7 @@ trait Validations {
|
||||
* Constraint for the identifier such as user name, repository name or page name.
|
||||
*/
|
||||
def identifier: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(!value.matches("^[a-zA-Z0-9\\-_.]+$")){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
@@ -25,7 +26,7 @@ trait Validations {
|
||||
*/
|
||||
def date(constraints: Constraint*): SingleValueType[java.util.Date] =
|
||||
new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
|
||||
def convert(value: String): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
|
||||
def convert(value: String, messages: Messages): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
// by user name
|
||||
getAccountByUserName(userName).map { account =>
|
||||
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/${account.userName}/_avatar"""
|
||||
}
|
||||
@@ -28,13 +28,13 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
// by mail address
|
||||
getAccountByMailAddress(mailAddress).map { account =>
|
||||
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/${account.userName}/_avatar"""
|
||||
}
|
||||
} getOrElse {
|
||||
if(getSystemSettings().gravatar){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/_unknown/_avatar"""
|
||||
}
|
||||
|
||||
@@ -14,22 +14,21 @@ trait LinkConverter { self: RequestCache =>
|
||||
// escape HTML tags
|
||||
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
|
||||
// convert issue id to link
|
||||
.replaceBy(("(^|\\W)" + issueIdPrefix + "(\\d+)(\\W|$)").r){ m =>
|
||||
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
||||
getIssue(repository.owner, repository.name, m.group(2)) match {
|
||||
case Some(issue) if(issue.isPullRequest)
|
||||
=> Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""")
|
||||
case Some(_) => Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""")
|
||||
case None => Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""")
|
||||
|
||||
=> Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>""")
|
||||
case Some(_) => Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>""")
|
||||
case None => Some(s"""#${m.group(2)}""")
|
||||
}
|
||||
}
|
||||
// convert @username to link
|
||||
.replaceBy("(^|\\W)@([a-zA-Z0-9\\-_]+)(\\W|$)".r){ m =>
|
||||
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m =>
|
||||
getAccountByUserName(m.group(2)).map { _ =>
|
||||
s"""${m.group(1)}<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>${m.group(3)}"""
|
||||
s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
|
||||
}
|
||||
}
|
||||
// convert commit id to link
|
||||
.replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", s"""$$1<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>$$3""")
|
||||
.replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ object Markdown {
|
||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
|
||||
// escape issue id
|
||||
val source = if(enableRefsLink){
|
||||
markdown.replaceAll("(^|\\W)#([0-9]+)(\\W|$)", "$1issue:$2$3")
|
||||
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
||||
} else markdown
|
||||
|
||||
val rootNode = new PegDownProcessor(
|
||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES
|
||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
|
||||
).parseMarkdown(source.toCharArray)
|
||||
|
||||
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
|
||||
@@ -121,4 +121,4 @@ class GitBucketHtmlSerializer(
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,16 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
|
||||
|
||||
/**
|
||||
* Returns <img> which displays the avatar icon.
|
||||
* Looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
* Returns <img> which displays the avatar icon for the given user name.
|
||||
* This method looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
*/
|
||||
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
|
||||
getAvatarImageHtml(userName, size, "", tooltip)
|
||||
|
||||
/**
|
||||
* Returns <img> which displays the avatar icon for the given mail address.
|
||||
* This method looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
*/
|
||||
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
|
||||
getAvatarImageHtml(commit.committer, size, commit.mailAddress)
|
||||
|
||||
@@ -57,7 +61,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
value
|
||||
}
|
||||
|
||||
import scala.util.matching.Regex
|
||||
import scala.util.matching.Regex._
|
||||
implicit class RegexReplaceString(s: String) {
|
||||
def replaceAll(pattern: String, replacer: (Match) => String): String = {
|
||||
@@ -65,6 +68,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert link notations in the activity message.
|
||||
*/
|
||||
def activityMessage(message: String)(implicit context: app.Context): Html =
|
||||
Html(message
|
||||
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
|
||||
@@ -72,7 +78,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
|
||||
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||
.replaceAll("\\[user:([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1">$$1</a>""")
|
||||
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -101,14 +107,32 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
def assets(implicit context: app.Context): String = s"${context.path}/assets"
|
||||
|
||||
/**
|
||||
* Generates the link to the account page.
|
||||
* Generates the text link to the account page.
|
||||
* If user does not exist or disabled, this method returns user name as text without link.
|
||||
*/
|
||||
def user(userName: String, mailAddress: String, styleClass: String = "")(implicit context: app.Context): Html = {
|
||||
getAccountByMailAddress(mailAddress).map { account =>
|
||||
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${userName}</a>""")
|
||||
} getOrElse Html(userName)
|
||||
}
|
||||
def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: app.Context): Html =
|
||||
userWithContent(userName, mailAddress, styleClass)(Html(userName))
|
||||
|
||||
/**
|
||||
* Generates the avatar link to the account page.
|
||||
* If user does not exist or disabled, this method returns avatar image without link.
|
||||
*/
|
||||
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html =
|
||||
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip))
|
||||
|
||||
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: app.Context): Html =
|
||||
(if(mailAddress.isEmpty){
|
||||
getAccountByUserName(userName)
|
||||
} else {
|
||||
getAccountByMailAddress(mailAddress)
|
||||
}).map { account =>
|
||||
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${content}</a>""")
|
||||
} getOrElse content
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given Date is past date.
|
||||
*/
|
||||
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
</div>
|
||||
<fieldset class="margin">
|
||||
@if(account.isDefined){
|
||||
<div class="pull-right">
|
||||
<a href="@path/@account.get.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-success" value="Save"/>
|
||||
<a href="@url(account.get.userName)" class="btn">Cancel</a>
|
||||
} else {
|
||||
@@ -64,3 +67,10 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#delete').click(function(){
|
||||
return confirm('Once you delete your account, there is no going back.\nAre you sure?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,5 +1,6 @@
|
||||
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import util.Directory._
|
||||
@import view.helpers._
|
||||
@html.main("System Settings"){
|
||||
@menu("system"){
|
||||
@@ -8,9 +9,15 @@
|
||||
<div class="box">
|
||||
<div class="box-header">System Settings</div>
|
||||
<div class="box-content">
|
||||
<!--====================================================================-->
|
||||
<!-- GITBUCKET_HOME -->
|
||||
<!--====================================================================-->
|
||||
<label class="strong">GITBUCKET_HOME</label>
|
||||
@GitBucketHome
|
||||
<!--====================================================================-->
|
||||
<!-- Account registration -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Account registration</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
@@ -94,6 +101,20 @@
|
||||
<span id="error-ldap_mailAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="ldap.tls"@if(settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/> Enable TLS
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapBindDN">Keystore</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapKeystore" name="ldap.keystore" value="@settings.ldap.map(_.keystore)"/>
|
||||
<span id="error-ldap_keystore" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Notification email -->
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
<span id="error-groupName" class="error"></span>
|
||||
</div>
|
||||
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
@if(account.isDefined){
|
||||
<label for="removed">
|
||||
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
|
||||
Disable
|
||||
</label>
|
||||
}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label class="strong">URL (Optional)</label>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
@(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context)
|
||||
@(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Manage Users"){
|
||||
@admin.html.menu("users"){
|
||||
<div style="text-align: right; margin-bottom: 4px;">
|
||||
<div class="pull-right" style="margin-bottom: 4px;">
|
||||
<a href="@path/admin/users/_newuser" class="btn">New User</a>
|
||||
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
|
||||
</div>
|
||||
<label for="includeRemoved">
|
||||
<input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/>
|
||||
Include removed users
|
||||
</label>
|
||||
<table class="table table-bordered table-hover">
|
||||
@users.map { account =>
|
||||
<tr>
|
||||
<td>
|
||||
<td @if(account.isRemoved){style="background-color: #dddddd;"}>
|
||||
<div class="pull-right">
|
||||
@if(account.isGroupAccount){
|
||||
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
|
||||
@@ -57,4 +61,11 @@
|
||||
}
|
||||
</table>
|
||||
}
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#includeRemoved').click(function(){
|
||||
location.href = '@path/admin/users?includeRemoved=' + this.checked;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -11,6 +11,12 @@
|
||||
<span id="error-userName" class="error"></span>
|
||||
</div>
|
||||
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
@if(account.isDefined){
|
||||
<label for="removed">
|
||||
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
|
||||
Disable
|
||||
</label>
|
||||
}
|
||||
</fieldset>
|
||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||
<fieldset>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
||||
<div class="box-header-small">
|
||||
<i class="icon-comment"></i>
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented
|
||||
@user(comment.commentedUserName, styleClass="username strong") commented
|
||||
<span class="pull-right">
|
||||
@datetime(comment.registeredDate)
|
||||
@if(comment.action != "commit" && comment.action != "merge" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
@@ -36,11 +36,11 @@
|
||||
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<span class="label label-info">Merged</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code>
|
||||
@user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
|
||||
@if(pullreq.get.requestUserName == repository.owner){
|
||||
<span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.branch)</span>
|
||||
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
|
||||
} else {
|
||||
<span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span>
|
||||
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
|
||||
}
|
||||
@datetime(comment.registeredDate)
|
||||
</div>
|
||||
@@ -50,9 +50,9 @@
|
||||
<span class="label label-important">Closed</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
@if(issue.isPullRequest){
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the pull request @datetime(comment.registeredDate)
|
||||
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate)
|
||||
} else {
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
|
||||
@user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label label-success">Reopened</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
|
||||
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<span id="error-edit-title" class="error"></span>
|
||||
<input type="text" style="width: 680px;" id="edit-title" value="@title"/>
|
||||
<textarea style="width: 680px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
|
||||
<input type="button" class="btn btn-small" value="Update Issue"/>
|
||||
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-small">Update Issue</button>
|
||||
<button type="button" class="btn btn-small btn-danger pull-right">Cancel</button>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#edit-content').elastic();
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
|
||||
}
|
||||
<div class="small muted">
|
||||
<a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> opened this issue @datetime(issue.registeredDate)
|
||||
@user(issue.openedUserName, styleClass="username strong") opened this issue @datetime(issue.registeredDate)
|
||||
</div>
|
||||
<h4 id="issueTitle">@issue.title</h4>
|
||||
</div>
|
||||
<div class="issue-info">
|
||||
<span id="label-assigned">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@avatar(userName, 20) <a href="@url(userName)" class="username strong">@userName</a> is assigned
|
||||
@avatar(userName, 20) @user(userName, styleClass="username strong") is assigned
|
||||
}.getOrElse("No one is assigned")
|
||||
</span>
|
||||
@if(hasWritePermission){
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="issue-participants">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
<span class="strong">@participants.size</span> @plural(participants.size, "participant")
|
||||
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
|
||||
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
#@issue.issueId
|
||||
</span>
|
||||
<div class="small muted" style="margin-left: 20px;">
|
||||
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)
|
||||
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||
@if(commentCount > 0){
|
||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="small muted" style="margin-left: 20px;">
|
||||
@avatar(issue.openedUserName, 20) by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)
|
||||
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||
@if(commentCount > 0){
|
||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
}
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
@if(issue.closed) {
|
||||
@comments.find(_.action == "merge").map{ comment =>
|
||||
<span class="label label-info">Merged</span>
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> merged @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code>
|
||||
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
at @datetime(comment.registeredDate)
|
||||
}.getOrElse {
|
||||
<span class="label label-important">Closed</span>
|
||||
<a href="@url(issue.openedUserName)" class="username strong">@issue.openedUserName</a> wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code>
|
||||
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
}
|
||||
} else {
|
||||
<span class="label label-success">Open</span>
|
||||
<a href="@url(issue.openedUserName)" class="username strong">@issue.openedUserName</a> wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code>
|
||||
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
}
|
||||
</div>
|
||||
<ul class="nav nav-tabs" id="pullreq-tab">
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
@readme.map { content =>
|
||||
<div id="readme" class="box">
|
||||
<div class="box-header">README.md</div>
|
||||
<div class="box-content">@markdown(content, repository, false, false)</div>
|
||||
<div class="box-content markdown-body">@markdown(content, repository, false, false)</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
</ul>
|
||||
@if(!isGroupRepository){
|
||||
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true">
|
||||
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true" autocomplete="off">
|
||||
<div>
|
||||
<span class="error" id="error-userName"></span>
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"${if(pageName == "") "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.main(s"${if(pageName.isEmpty) "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.header("wiki", repository)
|
||||
@tab("", repository)
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName == ""){New Page} else {@pageName}</h1>
|
||||
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<div class="btn-group">
|
||||
@if(pageName != ""){
|
||||
@if(page.isDefined){
|
||||
<a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
|
||||
<a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a>
|
||||
<a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<form action="@url(repository)/wiki/@if(pageName == ""){_new} else {_edit}" method="POST" validate="true">
|
||||
<form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true">
|
||||
<span id="error-pageName" class="error"></span>
|
||||
<input type="text" name="pageName" value="@pageName" style="width: 900px; font-weight: bold;" placeholder="Input a page name."/>
|
||||
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 900px; height: 400px;", "")
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
@commits.map { commit =>
|
||||
<tr>
|
||||
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
|
||||
<td>@avatar(commit.committer, 20) <a href="@url(commit.committer)">@commit.committer</a></td>
|
||||
<td>@avatar(commit, 20) @user(commit.committer, commit.mailAddress)</td>
|
||||
<td width="80%">
|
||||
<span class="muted">@datetime(commit.time):</span> @commit.shortMessage
|
||||
</td>
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
<listener-class>servlet.SessionCleanupListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Automatic migration -->
|
||||
<!-- ===================================================================== -->
|
||||
<listener>
|
||||
<listener-class>servlet.AutoUpdateListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Scalatra configuration -->
|
||||
<!-- ===================================================================== -->
|
||||
@@ -18,6 +25,9 @@
|
||||
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- HTTP interface for Git repositories -->
|
||||
<!-- ===================================================================== -->
|
||||
<servlet>
|
||||
<servlet-name>GitRepositoryServlet</servlet-name>
|
||||
<servlet-class>servlet.GitRepositoryServlet</servlet-class>
|
||||
@@ -27,34 +37,10 @@
|
||||
<servlet-name>GitRepositoryServlet</servlet-name>
|
||||
<url-pattern>/git/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<filter>
|
||||
<filter-name>TransactionFilter</filter-name>
|
||||
<filter-class>servlet.TransactionFilter</filter-class>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>TransactionFilter</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<filter>
|
||||
<filter-name>BasicAuthenticationFilter</filter-name>
|
||||
<filter-class>servlet.BasicAuthenticationFilter</filter-class>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>BasicAuthenticationFilter</filter-name>
|
||||
<url-pattern>/git/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- H2 database configuration -->
|
||||
<!-- ===================================================================== -->
|
||||
<listener>
|
||||
<listener-class>servlet.AutoUpdateListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<context-param>
|
||||
<param-name>db.user</param-name>
|
||||
<param-value>sa</param-value>
|
||||
@@ -101,4 +87,14 @@
|
||||
<session-timeout>1440</session-timeout>
|
||||
</session-config>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Optional configurations -->
|
||||
<!-- ===================================================================== -->
|
||||
<!--
|
||||
<context-param>
|
||||
<param-name>gitbucket.home</param-name>
|
||||
<param-value>PATH_TO_DATADIR</param-value>
|
||||
</context-param>
|
||||
-->
|
||||
|
||||
</web-app>
|
||||
@@ -1,39 +0,0 @@
|
||||
$(function(){
|
||||
$.each($('form[validate=true]'), function(i, form){
|
||||
$(form).submit(validate);
|
||||
});
|
||||
$.each($('input[formaction]'), function(i, input){
|
||||
$(input).click(function(){
|
||||
var form = $(input).parents('form')
|
||||
$(form).attr('action', $(input).attr('formaction'))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function validate(e){
|
||||
var form = $(e.target);
|
||||
|
||||
if(form.data('validated') == true){
|
||||
return true;
|
||||
}
|
||||
|
||||
$.post(form.attr('action') + '/validate', $(e.target).serialize(), function(data){
|
||||
// clear all error messages
|
||||
$('.error').text('');
|
||||
|
||||
if($.isEmptyObject(data)){
|
||||
form.data('validated', true);
|
||||
form.submit();
|
||||
} else {
|
||||
form.data('validated', false);
|
||||
displayErrors(data);
|
||||
}
|
||||
}, 'json');
|
||||
return false;
|
||||
}
|
||||
|
||||
function displayErrors(data){
|
||||
$.each(data, function(key, value){
|
||||
$('#error-' + key.split('.').join('_')).text(value);
|
||||
});
|
||||
}
|
||||
@@ -9,8 +9,8 @@ class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
|
||||
val RootMailAddress = "root@localhost"
|
||||
|
||||
"getAllUsers" in { withTestDB{
|
||||
AccountService.getAllUsers must be like{
|
||||
case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false)) => ok
|
||||
AccountService.getAllUsers() must be like{
|
||||
case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -75,3 +75,4 @@ class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ import java.io.File
|
||||
trait ServiceSpecBase {
|
||||
|
||||
def withTestDB[A](action: => A): A = {
|
||||
util.FileUtil.withTmpDir(new File(FileUtils.getTempDirectory(), Random.alphanumeric.take(10).mkString)){
|
||||
dir =>
|
||||
util.FileUtil.withTmpDir(new File(FileUtils.getTempDirectory(), Random.alphanumeric.take(10).mkString)){ dir =>
|
||||
val (url, user, pass) = (s"jdbc:h2:${dir}", "sa", "sa")
|
||||
org.h2.Driver.load()
|
||||
using(DriverManager.getConnection(url, user, pass)){ conn =>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
package util
|
||||
|
||||
import org.specs2.mutable._
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
class ValidationsSpec extends Specification with Validations {
|
||||
|
||||
"identifier" should {
|
||||
"validate id string " in {
|
||||
identifier.validate("id", "aa_ZZ-00.01") mustEqual None
|
||||
identifier.validate("id", "_aaaa") mustEqual Some("id starts with invalid character.")
|
||||
identifier.validate("id", "-aaaa") mustEqual Some("id starts with invalid character.")
|
||||
identifier.validate("id", "aa_ZZ#01") mustEqual Some("id contains invalid character.")
|
||||
identifier.validate("id", "aa_ZZ-00.01", null) mustEqual None
|
||||
identifier.validate("id", "_aaaa", null) mustEqual Some("id starts with invalid character.")
|
||||
identifier.validate("id", "-aaaa", null) mustEqual Some("id starts with invalid character.")
|
||||
identifier.validate("id", "aa_ZZ#01", null) mustEqual Some("id contains invalid character.")
|
||||
}
|
||||
}
|
||||
|
||||
"color" should {
|
||||
"validate color string " in {
|
||||
color.validate("color", "#88aaff") mustEqual None
|
||||
color.validate("color", "#gghhii") mustEqual Some("color must be '#[0-9a-fA-F]{6}'.")
|
||||
val messages = Messages()
|
||||
color.validate("color", "#88aaff", messages) mustEqual None
|
||||
color.validate("color", "#gghhii", messages) mustEqual Some("color must be '#[0-9a-fA-F]{6}'.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +28,7 @@ class ValidationsSpec extends Specification with Validations {
|
||||
// date().validate("date", "2013-10-5" , Map[String, String]()) mustEqual List(("date", "date must be '\\d{4}-\\d{2}-\\d{2}'."))
|
||||
// }
|
||||
"convert date string " in {
|
||||
val result = date().convert("2013-10-05")
|
||||
val result = date().convert("2013-10-05", null)
|
||||
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(result) mustEqual "2013-10-05 00:00:00"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class AvatarImageProviderSpec extends Specification {
|
||||
val provider = new AvatarImageProviderImpl(Some(createAccount(None)), createSystemSettings(true))
|
||||
|
||||
provider.toHtml("user", 20).toString mustEqual
|
||||
"<img src=\"http://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e?s=20\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
|
||||
"<img src=\"https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e?s=20\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
|
||||
}
|
||||
|
||||
"show uploaded image even if gravatar integration is enabled" in {
|
||||
@@ -38,7 +38,7 @@ class AvatarImageProviderSpec extends Specification {
|
||||
val provider = new AvatarImageProviderImpl(None, createSystemSettings(true))
|
||||
|
||||
provider.toHtml("user", 20, "hoge@hoge.com").toString mustEqual
|
||||
"<img src=\"http://www.gravatar.com/avatar/4712f9b0e63f56ad952ad387eaa23b9c?s=20\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
|
||||
"<img src=\"https://www.gravatar.com/avatar/4712f9b0e63f56ad952ad387eaa23b9c?s=20\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
|
||||
}
|
||||
|
||||
"show unknown image for unknown user if gravatar integration is enabled" in {
|
||||
@@ -75,7 +75,8 @@ class AvatarImageProviderSpec extends Specification {
|
||||
updatedDate = new Date(),
|
||||
lastLoginDate = None,
|
||||
image = image,
|
||||
isGroupAccount = false)
|
||||
isGroupAccount = false,
|
||||
isRemoved = false)
|
||||
|
||||
private def createSystemSettings(useGravatar: Boolean) =
|
||||
SystemSettings(
|
||||
|
||||
Reference in New Issue
Block a user