Merge branch 'master' into feature/plugin-hotdeploy

This commit is contained in:
Naoki Takezoe
2017-05-02 14:21:53 +09:00
42 changed files with 296 additions and 163 deletions

View File

@@ -1,6 +1,7 @@
# Guideline for Issues
- At first, See [FAQ](https://github.com/gitbucket/gitbucket/wiki/FAQ) and check issues whether there is a same question or request in the past.
- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- We can also support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- Write an issue in English. At least, write subject in English.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.

View File

@@ -1,31 +1,35 @@
GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.png)](https://gitter.im/gitbucket/gitbucket) [![Build Status](https://travis-ci.org/gitbucket/gitbucket.svg?branch=master)](https://travis-ci.org/gitbucket/gitbucket)
=========
GitBucket is a Git platform powered by Scala offering:
GitBucket is a Git web platform powered by Scala offering:
- Easy installation
- Intuitive UI
- High extensibility by plugins
- API compatibility with GitHub
You can try an [online demo](https://gitbucket.herokuapp.com/) *(ID: root / Pass: root)* of GitBucket, and also get the latest information at [GitBucket News](https://gitbucket.github.io/gitbucket-news/).
Features
--------
The current version of GitBucket provides a basic features below:
The current version of GitBucket provides many features such as:
- Public / Private Git repository (http and ssh access)
- Public / Private Git repositories (with http/https and ssh access)
- GitLFS support
- Repository viewer includes online file editor
- Issues, Pull request and Wiki for repositories
- Activity timeline and email notification
- Repository viewer including an online file editor
- Issues, Pull Requests and Wiki for repositories
- Activity timeline and email notifications
- Account and group management with LDAP integration
- Plug-in system
- a Plug-in system
If you want to try the development version of GitBucket, see the [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md).
Installation
--------
GitBucket requires **Java8**. You have to install it if it is not already installed.
GitBucket requires **Java8**. You have to install it, if it is not already installed.
1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`.
2. Go to `http://[hostname]:8080/` and log in with **root** / **root**.
2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**.
You can specify following options:
@@ -45,24 +49,37 @@ To upgrade GitBucket, replace `gitbucket.war` with the new version, after stoppi
Plugins
--------
GitBucket has a plug-in system to allow extensions to GitBucket. We provide some official plug-ins:
GitBucket has a plug-in system that allows extra functionality. Officially the following plug-ins are provided:
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
- [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
- [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin)
You can find more plugins made by the community at [GitBucket community plugins](http://gitbucket-plugins.github.io/).
Support
--------
- If you have any questions about GitBucket, send it to the [gitter room](https://gitter.im/gitbucket/gitbucket) before opening an issue.
- Make sure check whether there is the same question or request in the past.
- When raise a new issue, write at least the subject in **English**.
- We can also provide support in Japanese at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- The first priority of GitBucket is easy installation and API compatibility with GitHub, so we might reject if your request is against it.
- If you have any questions about GitBucket, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- We can also provide support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- Write an issue in English. At least, write subject in English.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
Release Notes
-------------
### 4.12 - 30 Apr 2017
- [Gist plug-in](https://github.com/gitbucket/gitbucket-gist-plugin) provides JavaScript to embed snippet
- Dropdown menu filter in the branch comparing page
- Caution for the embedded H2 database
### 4.11 - 1 Apr 2017
- Deploy keys support
- Auto generate avatar images
- Collaborators of the private forked repository are copied from the original repository
- Cache avatar images in the browser
- New extension point to receive events about repository
### 4.10 - 25 Feb 2017
- Update to Scala 2.12, Scalatra 2.5 and Slick 3.2
- Display file size in the file viewer

View File

@@ -10,7 +10,7 @@ sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.12.1"
scalaVersion := "2.12.2"
// dependency settings
resolvers ++= Seq(
@@ -21,8 +21,8 @@ resolvers ++= Seq(
"amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/"
)
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.6.1.201703071140-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.6.1.201703071140-r",
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.7.0.201704051617-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.7.0.201704051617-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.5.0",
@@ -60,7 +60,7 @@ libraryDependencies ++= Seq(
)
// Compiler settings
scalacOptions := Seq("-deprecation", "-language:postfixOps")
scalacOptions := Seq("-deprecation", "-language:postfixOps", "-opt:l:method")
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"

View File

@@ -1,18 +1,24 @@
How to run from the source tree
========
Install [sbt](http://www.scala-sbt.org/index.html) at first.
```
$ brew install sbt
```
Run for Development
--------
If you want to test GitBucket, input following command at the root directory of the source tree.
If you want to test GitBucket, type the following command in the root directory of the source tree.
```
$ sbt ~jetty:start
```
Then access to `http://localhost:8080/` by your browser. The default administrator account is `root` and password is `root`.
Then access `http://localhost:8080/` in your browser. The default administrator account is `root` and password is `root`.
Source code modification is detected and reloaded automatically. You can modify logging configuration by editing `src/main/resources/logback-dev.xml`.
Source code modifications are detected and a reloaded happens automatically. You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`.
Build war file
--------
@@ -23,9 +29,9 @@ To build war file, run the following command:
$ sbt package
```
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
`gitbucket_2.12-x.x.x.war` is generated into `target/scala-2.12`.
To build executable war file, run
To build an executable war file, run
```
$ sbt executable
@@ -35,8 +41,8 @@ at the top of the source tree. It generates executable `gitbucket.war` into `tar
Run tests spec
---------
To run the full serie of tests, run the following command:
To run the full series of tests, run the following command:
```
sbt test
$ sbt test
```

View File

@@ -1,7 +1,7 @@
Notification Email
========
GitBucket sends email to target users by enabling the notification email by an administrator.
GitBucket can send email notification to users if this feature is enabled by an administrator.
The timing of the notification are as follows:
@@ -20,4 +20,4 @@ Notified users are as follows:
* collaborators
* participants
However, the operation in person is excluded from the target.
However, the person performing the operation is excluded from the notification.

View File

@@ -34,8 +34,6 @@ object GitBucketCoreModule extends Module("gitbucket-core",
Generate release files
--------
Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https://maven.apache.org/).
### Make release war file
Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`.
@@ -52,4 +50,12 @@ For plug-in development, we have to publish the GitBucket jar file to the Maven
$ sbt publish-signed
```
Then operate release sequence at https://oss.sonatype.org/.
Then logged-in https://oss.sonatype.org/ and delete following files from the staging repository:
- gitbucket_2.12-x.x.x.war
- gitbucket_2.12-x.x.x.war.asc
- gitbucket_2.12-x.x.x.war.asc.md5
- gitbucket_2.12-x.x.x.war.asc.sha1
- gitbucket_2.12-x.x.x.war.md5
At last, close and release the repository.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
set SCRIPT_DIR=%~dp0
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.12.jar" %*

2
sbt.sh
View File

@@ -1,2 +0,0 @@
#!/bin/sh
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.12.jar "$@"

View File

@@ -1,4 +1,9 @@
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
@@ -8,6 +13,8 @@ import java.security.ProtectionDomain;
public class JettyLauncher {
public static void main(String[] args) throws Exception {
System.setProperty("java.awt.headless", "true");
String host = null;
int port = 8080;
InetSocketAddress address = null;
@@ -19,19 +26,25 @@ public class JettyLauncher {
if(arg.startsWith("--") && arg.contains("=")) {
String[] dim = arg.split("=");
if(dim.length >= 2) {
if(dim[0].equals("--host")) {
host = dim[1];
} else if(dim[0].equals("--port")) {
port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")) {
contextPath = dim[1];
if(!contextPath.startsWith("/")){
contextPath = "/" + contextPath;
}
} else if(dim[0].equals("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]);
} else if(dim[0].equals("--temp_dir")){
tmpDirPath = dim[1];
switch (dim[0]) {
case "--host":
host = dim[1];
break;
case "--port":
port = Integer.parseInt(dim[1]);
break;
case "--prefix":
contextPath = dim[1];
if (!contextPath.startsWith("/")) {
contextPath = "/" + contextPath;
}
break;
case "--gitbucket.home":
System.setProperty("gitbucket.home", dim[1]);
break;
case "--temp_dir":
tmpDirPath = dim[1];
break;
}
}
}
@@ -54,6 +67,15 @@ public class JettyLauncher {
// connector.setPort(port);
// server.addConnector(connector);
// Disabling Server header
for (Connector connector : server.getConnectors()) {
for (ConnectionFactory factory : connector.getConnectionFactories()) {
if (factory instanceof HttpConnectionFactory) {
((HttpConnectionFactory) factory).getHttpConfiguration().setSendServerVersion(false);
}
}
}
WebAppContext context = new WebAppContext();
File tmpDir;
@@ -85,7 +107,9 @@ public class JettyLauncher {
context.setInitParameter("org.scalatra.ForceHttps", "true");
}
server.setHandler(context);
Handler handler = addStatisticsHandler(context);
server.setHandler(handler);
server.setStopAtShutdown(true);
server.setStopTimeout(7_000);
server.start();
@@ -114,4 +138,12 @@ public class JettyLauncher {
}
dir.delete();
}
private static Handler addStatisticsHandler(Handler handler) {
// The graceful shutdown is implemented via the statistics handler.
// See the following: https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142
final StatisticsHandler statisticsHandler = new StatisticsHandler();
statisticsHandler.setHandler(handler);
return statisticsHandler;
}
}

View File

@@ -31,5 +31,6 @@ object GitBucketCoreModule extends Module("gitbucket-core",
new Version("4.10.0"),
new Version("4.11.0",
new LiquibaseMigration("update/gitbucket-core_4.11.xml")
)
),
new Version("4.12.0")
)

View File

@@ -37,7 +37,7 @@ object ApiRepository{
name = repository.repositoryName,
full_name = s"${repository.userName}/${repository.repositoryName}",
description = repository.description.getOrElse(""),
watchers = 0,
watchers = watchers,
forks = forkedCount,
`private` = repository.isPrivate,
default_branch = repository.defaultBranch,

View File

@@ -19,8 +19,8 @@ case class CreateAStatus(
def isValid: Boolean = {
CommitState.valueOf(state).isDefined &&
// only http
target_url.filterNot(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length<255).isEmpty &&
context.filterNot(f => f.length<255).isEmpty &&
description.filterNot(f => f.length<1000).isEmpty
target_url.forall(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length < 255) &&
context.forall(f => f.length < 255) &&
description.forall(f => f.length < 1000)
}
}

View File

@@ -15,7 +15,6 @@ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.scalatra.BadRequest
import java.util.Date
class AccountController extends AccountControllerBase
@@ -61,31 +60,31 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val sshKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim(label("Key" , text(required, validPublicKey)))
"publicKey" -> trim2(label("Key" , text(required, validPublicKey)))
)(SshKeyForm.apply)
val personalTokenForm = mapping(
"note" -> trim(label("Token", text(required, maxlength(100))))
"note" -> trim(label("Token", text(required, maxlength(100))))
)(PersonalTokenForm.apply)
case class NewGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String], members: String)
case class EditGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean()))
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean()))
)(EditGroupForm.apply)
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
@@ -150,20 +149,21 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_avatar"){
val userName = params("userName")
getAccountByUserName(userName).map{ account =>
contentType = "image/png"
getAccountByUserName(userName).flatMap{ account =>
response.setDateHeader("Last-Modified", account.updatedDate.getTime)
account.image.map{ image =>
RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image))
Some(RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image)))
}.getOrElse{
contentType = "image/png"
(if (account.isGroupAccount) {
if (account.isGroupAccount) {
TextAvatarUtil.textGroupAvatar(account.fullName)
} else {
TextAvatarUtil.textAvatar(account.fullName)
}).getOrElse(Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png"))
}
}
}.getOrElse{
NotFound()
response.setHeader("Cache-Control", "max-age=3600")
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
}
}

View File

@@ -125,7 +125,7 @@ trait ApiControllerBase extends ControllerBase {
get ("/api/v3/repos/:owner/:repo/branches/:branch")(referrersOnly { repository =>
//import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
branch <- params.get("branch") if repository.branchList.contains(branch)
br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
@@ -287,7 +287,7 @@ trait ApiControllerBase extends ControllerBase {
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
branch <- params.get("branch") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield {

View File

@@ -40,10 +40,6 @@ abstract class ControllerBase extends ScalatraFilter
contentType = formats("json")
}
// TODO Scala 2.11
// // Don't set content type via Accept header.
// override def format(implicit request: HttpServletRequest) = ""
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
val httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse]
@@ -151,7 +147,6 @@ abstract class ControllerBase extends ScalatraFilter
}
}
// TODO Scala 2.11
override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
includeContextPath: Boolean = true, includeServletPath: Boolean = true,
absolutize: Boolean = true, withSessionId: Boolean = true)
@@ -159,6 +154,18 @@ abstract class ControllerBase extends ScalatraFilter
if (path.startsWith("http")) path
else baseUrl + super.url(path, params, false, false, false)
/**
* Extends scalatra-form's trim rule to eliminate CR and LF.
*/
protected def trim2[T](valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T](){
def convert(value: String, messages: Messages): T = valueType.convert(trim(value), messages)
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Seq[(String, String)] =
valueType.validate(name, trim(value), params, messages)
private def trim(value: String): String = if(value == null) null else value.replaceAll("\r\n", "").trim
}
/**
* Use this method to response the raw data against XSS.
*/

View File

@@ -26,13 +26,13 @@ trait IndexControllerBase extends ControllerBase {
"password" -> trim(label("Password", text(required)))
)(SignInForm.apply)
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
// val searchForm = mapping(
// "query" -> trim(text(required)),
// "owner" -> trim(text(required)),
// "repository" -> trim(text(required))
// )(SearchForm.apply)
//
// case class SearchForm(query: String, owner: String, repository: String)
get("/"){
@@ -163,7 +163,7 @@ trait IndexControllerBase extends ControllerBase {
get("/search"){
val query = params.getOrElse("query", "").trim.toLowerCase
val visibleRepositories = getVisibleRepositories(context.loginAccount, None)
val visibleRepositories = getVisibleRepositories(context.loginAccount, repositoryUserName = None, withoutPhysicalInfo = true)
val repositories = visibleRepositories.filter { repository =>
repository.name.toLowerCase.indexOf(query) >= 0 || repository.owner.toLowerCase.indexOf(query) >= 0
}

View File

@@ -40,7 +40,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)
val optionsForm = mapping(
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), identifier, renameRepositoryName))),
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), repository, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type" , boolean())),
"issuesOption" -> trim(label("Issues Option" , text(required, featureOption))),
@@ -63,7 +63,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
val deployKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim(label("Key" , text(required))), // TODO duplication check in the repository?
"publicKey" -> trim2(label("Key" , text(required))), // TODO duplication check in the repository?
"allowWrite" -> trim(label("Key" , boolean()))
)(DeployKeyForm.apply)
@@ -139,6 +139,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getLfsDir(repository.owner, form.repositoryName))
}
}
// Move attached directory
defining(getAttachedDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getAttachedDir(repository.owner, form.repositoryName))
}
}
// Delete parent directory
FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name))
@@ -157,7 +163,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** Update default branch */
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
if(repository.branchList.find(_ == form.defaultBranch).isEmpty){
if(!repository.branchList.contains(form.defaultBranch)){
redirect(s"/${repository.owner}/${repository.name}/settings/options")
} else {
saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
@@ -174,7 +180,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._
val branch = params("branch")
if(repository.branchList.find(_ == branch).isEmpty){
if(!repository.branchList.contains(branch)){
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
@@ -352,6 +358,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getLfsDir(form.newOwner, repository.name))
}
}
// Move attached directory
defining(getAttachedDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getAttachedDir(form.newOwner, repository.name))
}
}
// Delere parent directory
FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name))

View File

@@ -232,7 +232,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
oldFileName = form.oldFileName,
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
charset = form.charset,
message = if(form.oldFileName.exists(_ == form.newFileName)){
message = if(form.oldFileName.contains(form.newFileName)){
form.message.getOrElse(s"Update ${form.newFileName}")
} else {
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
@@ -614,7 +614,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val permission = JGitUtil.processTree(git, headTip){ (path, tree) =>
// Add all entries except the editing file
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
if(!newPath.contains(path) && !oldPath.contains(path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
// Retrieve permission if file exists to keep it
@@ -670,12 +670,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
val revision = name.stripSuffix(suffix)
val filename = repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
val oid = git.getRepository.resolve(revision)
val revCommit = JGitUtil.getRevCommitFromId(git, oid)
val sha1 = oid.getName()
val repositorySuffix = (if(sha1.startsWith(revision)) sha1 else revision).replace('/','-')
val filename = repository.name + "-" + repositorySuffix + suffix
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${filename}")
@@ -683,6 +684,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
git.archive
.setFormat(suffix.tail)
.setPrefix(repository.name + "-" + repositorySuffix + "/")
.setTree(revCommit)
.setOutputStream(response.getOutputStream)
.call()

View File

@@ -286,7 +286,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.description, form.isRemoved)
updateGroup(groupName, form.description, form.url, form.isRemoved)
if(form.isRemoved){
// Remove from GROUP_MEMBER

View File

@@ -9,10 +9,10 @@ trait MilestoneComponent extends TemplateComponent { self: Profile =>
class Milestones(tag: Tag) extends Table[Milestone](tag, "MILESTONE") with MilestoneTemplate {
override val milestoneId = column[Int]("MILESTONE_ID", O AutoInc)
val title = column[String]("TITLE")
val description = column[String]("DESCRIPTION")
val dueDate = column[java.util.Date]("DUE_DATE")
val closedDate = column[java.util.Date]("CLOSED_DATE")
def * = (userName, repositoryName, milestoneId, title, description.?, dueDate.?, closedDate.?) <> (Milestone.tupled, Milestone.unapply)
val description = column[Option[String]]("DESCRIPTION")
val dueDate = column[Option[java.util.Date]]("DUE_DATE")
val closedDate = column[Option[java.util.Date]]("CLOSED_DATE")
def * = (userName, repositoryName, milestoneId, title, description, dueDate, closedDate) <> (Milestone.tupled, Milestone.unapply)
def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], milestoneId: Rep[Int]) = byMilestone(userName, repositoryName, milestoneId)

View File

@@ -86,7 +86,7 @@ class PluginRegistry {
def addRenderer(extension: String, renderer: Renderer): Unit = renderers += ((extension, renderer))
def getRenderer(extension: String): Renderer = renderers.get(extension).getOrElse(DefaultRenderer)
def getRenderer(extension: String): Renderer = renderers.getOrElse(extension, DefaultRenderer)
def renderableExtensions: Seq[String] = renderers.keys.toSeq
@@ -220,7 +220,7 @@ object PluginRegistry {
val classLoader = new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
try {
val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin]
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
val pluginId = plugin.pluginId
// Check duplication

View File

@@ -166,8 +166,8 @@ trait AccountService {
def updateGroup(groupName: String, description: Option[String], url: Option[String], removed: Boolean)(implicit s: Session): Unit =
Accounts.filter(_.userName === groupName.bind)
.map(t => (t.url.?, t.description.?, t.removed))
.update(url, description, removed)
.map(t => (t.url.?, t.description.?, t.updatedDate, t.removed))
.update(url, description, currentDate, removed)
def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = {
GroupMembers.filter(_.groupName === groupName.bind).delete

View File

@@ -111,7 +111,7 @@ trait IssuesService {
val (_, cs) = status.head
Some(CommitStatusInfo(
count = status.length,
successCount = status.filter(_._2.state == CommitState.SUCCESS).length,
successCount = status.count(_._2.state == CommitState.SUCCESS),
context = (if(status.length == 1) Some(cs.context) else None),
state = (if(status.length == 1) Some(cs.state) else None),
targetUrl = (if(status.length == 1) cs.targetUrl else None),

View File

@@ -21,7 +21,7 @@ trait MilestonesService {
def updateMilestone(milestone: Milestone)(implicit s: Session): Unit =
Milestones
.filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId))
.map (t => (t.title, t.description.?, t.dueDate.?, t.closedDate.?))
.map (t => (t.title, t.description, t.dueDate, t.closedDate))
.update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate)
def openMilestone(milestone: Milestone)(implicit s: Session): Unit =

View File

@@ -76,7 +76,7 @@ object ProtectedBranchService {
includeAdministrators: Boolean) extends AccountService with CommitStatusService {
def isAdministrator(pusher: String)(implicit session: Session): Boolean =
pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty
pusher == owner || getGroupMembers(owner).exists(gm => gm.userName == pusher && gm.isManager)
/**
* Can't be force pushed

View File

@@ -265,7 +265,7 @@ object PullRequestService {
val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")
state -> summary
}
lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.exists(_==s.context) }
lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.contains(s.context) }
lazy val isAllSuccess = commitStateSummary._1==CommitState.SUCCESS
}
}

View File

@@ -262,11 +262,19 @@ trait RepositoryService { self: AccountService =>
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
},
repository,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
getRepositoryManagers(repository.userName))
if(withoutPhysicalInfo){
-1
} else {
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
)
},
if(withoutPhysicalInfo){
Nil
} else {
getRepositoryManagers(repository.userName)
})
}
}
@@ -300,7 +308,7 @@ trait RepositoryService { self: AccountService =>
case None => Repositories filter(_.isPrivate === false.bind)
}).filter { t =>
repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
}.sortBy(_.lastActivityDate desc).list.map { repository =>
new RepositoryInfo(
if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
@@ -308,11 +316,19 @@ trait RepositoryService { self: AccountService =>
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
},
repository,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
getRepositoryManagers(repository.userName))
if(withoutPhysicalInfo){
-1
} else {
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
)
},
if(withoutPhysicalInfo) {
Nil
} else {
getRepositoryManagers(repository.userName)
})
}
}

View File

@@ -177,7 +177,7 @@ trait WikiService {
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
JGitUtil.processTree(git, headId){ (path, tree) =>
if(revertInfo.find(x => x.filePath == path).isEmpty){
if(!revertInfo.exists(x => x.filePath == path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}

View File

@@ -51,21 +51,21 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
private def pluginRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
settings: SystemSettings, isUpdating: Boolean, filter: GitRepositoryFilter): Unit = {
implicit val r = request
Database() withSession { implicit session =>
val account = for {
auth <- Option(request.getHeader("Authorization"))
Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password)
} yield {
request.setAttribute(Keys.Request.UserName, account.userName)
account
}
val account = for {
auth <- Option(request.getHeader("Authorization"))
Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password)
} yield {
request.setAttribute(Keys.Request.UserName, account.userName)
account
}
if(filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)){
chain.doFilter(request, response)
} else {
AuthUtil.requireAuth(response)
if (filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)) {
chain.doFilter(request, response)
} else {
AuthUtil.requireAuth(response)
}
}
}

View File

@@ -105,7 +105,7 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
}
}
case AuthType.DeployKeyType(key) => {
getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)) match {
getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).contains(key)) match {
case List(_) => true
case _ => false
}
@@ -123,7 +123,7 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
}
}
case AuthType.DeployKeyType(key) => {
getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)) match {
getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).contains(key)) match {
case List(x) if x.allowWrite => true
case _ => false
}

View File

@@ -68,7 +68,7 @@ class PublicKeyAuthenticator(genericUser: String) extends PublickeyAuthenticator
private def authenticateGenericUser(userName: String, key: PublicKey, session: ServerSession, genericUser: String)(implicit s: Session): Boolean = {
// find all users having the key we got from ssh
val possibleUserNames = getAllKeys().filter { sshKey =>
SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)
SshUtil.str2PublicKey(sshKey.publicKey).contains(key)
}.map(_.userName).distinct
// determine the user - if different accounts share the same key, tough luck
@@ -85,7 +85,7 @@ class PublicKeyAuthenticator(genericUser: String) extends PublickeyAuthenticator
}.getOrElse {
// search deploy keys
val existsDeployKey = getAllDeployKeys().exists { sshKey =>
SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)
SshUtil.str2PublicKey(sshKey.publicKey).contains(key)
}
if(existsDeployKey){
// found deploy key for repository

View File

@@ -18,16 +18,17 @@ object SshUtil {
val parts = key.split(" ")
if (parts.size < 2) {
logger.debug(s"Invalid PublicKey Format: ${key}")
return None
}
try {
val encodedKey = parts(1)
val decode = Base64.getDecoder.decode(Constants.encodeASCII(encodedKey))
Some(new ByteArrayBuffer(decode).getRawPublicKey)
} catch {
case e: Throwable =>
logger.debug(e.getMessage, e)
None
None
} else {
try {
val encodedKey = parts(1)
val decode = Base64.getDecoder.decode(Constants.encodeASCII(encodedKey))
Some(new ByteArrayBuffer(decode).getRawPublicKey)
} catch {
case e: Throwable =>
logger.debug(e.getMessage, e)
None
}
}
}

View File

@@ -952,7 +952,7 @@ object JGitUtil {
* @return the last modified commit of specified path
*/
def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = {
return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
}
def getBranches(owner: String, name: String, defaultBranch: String, origin: Boolean): Seq[BranchInfo] = {

View File

@@ -1,4 +1,5 @@
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.util.DatabaseConfig
@gitbucket.core.html.main("System settings"){
@gitbucket.core.admin.html.menu("system"){
@gitbucket.core.helper.html.information(info)
@@ -20,7 +21,17 @@
</tr>
<tr>
<td>DATABASE_URL</td>
<td>@gitbucket.core.util.DatabaseConfig.url</td>
@if(DatabaseConfig.url.startsWith("jdbc:h2:")) {
<td class="danger">
<p>@gitbucket.core.util.DatabaseConfig.url</p>
<p>
Your GitBucket is running on embedded H2 database.
Recommend to <a href="https://github.com/gitbucket/gitbucket/wiki/External-database-configuration">configure to use external database</a> if you would like to use GitBucket for important purpose.
</p>
</td>
}else{
<td>@gitbucket.core.util.DatabaseConfig.url</td>
}
</tr>
</table>
<!--====================================================================-->

View File

@@ -49,10 +49,7 @@ $(function(){
$('#filter-box').keyup(function(){
var inputVal = $('#filter-box').val();
$.each($('li.repo-link a'), function(index, elem) {
console.log(inputVal);
console.log(elem.text.trim());
console.log(elem.text.trim().lastIndexOf(inputVal, 0));
if (!inputVal || !elem.text.trim() || elem.text.trim().indexOf(inputVal) >= 0) {
if ( !inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >= 0 ) {
$(elem).parent().show();
} else {
$(elem).parent().hide();

View File

@@ -31,7 +31,7 @@
$('#branch-control-input').keyup(function() {
var inputVal = $('#branch-control-input').val();
$.each($('#branch-control-input').parent().parent().find('a'), function(index, elem) {
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
if (!inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >= 0) {
$(elem).parent().show();
} else {
$(elem).parent().hide();

View File

@@ -16,7 +16,7 @@
</div>
</div>
Showing <a href="javascript:void(0);" id="toggle-file-list">@diffs.size changed @helpers.plural(diffs.size, "file")</a>
<ul id="commit-file-list" style="display: none;clear:right">
<ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) =>
<li class="border">
<span class="pull-right diffstat" data-diff-id="@i"></span>

View File

@@ -32,7 +32,7 @@ $(function(){
$('#@{filter}-input').keyup(function() {
var inputVal = $('#@{filter}-input').val();
$.each($('#@{filter}-input').parent().parent().find('a'), function(index, elem) {
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
if ( !inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >=0 ) {
$(elem).parent().show();
} else {
$(elem).parent().hide();

View File

@@ -221,3 +221,27 @@ $(function(){
}
});
</script>
<script>
function dropdownFilter(dropdownItem,placeHolder,id) {
$('<li><input id="' + id + '" type="text" class="form-control input-sm dropdown-filter-input" placeholder="' + placeHolder + '"/></li>')
.insertBefore($("li:has(a" + dropdownItem + "):first"));
$('#'+id).keyup(function() {
var inputVal = $('#'+id).val();
$.each($('#'+id).parent().parent().find('a'), function(index, elem) {
if (!inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >= 0) {
$(elem).parent().show();
} else {
$(elem).parent().hide();
}
});
});
}
$(function(){
dropdownFilter('.origin-owner', 'Find Repository...', 'origin-owner-control-input');
dropdownFilter('.origin-branch','Find Branch...', 'origin-branch-control-input');
dropdownFilter('.forked-owner', 'Find Repository...', 'forked-owner-control-input');
dropdownFilter('.forked-branch','Find Branch...', 'forked-branch-control-input');
});
</script>

View File

@@ -342,6 +342,8 @@ div.account-image {
ul.dropdown-menu {
padding: 2px 0;
overflow: auto;
max-height: 100vh;
}
ul.dropdown-menu li {
@@ -556,6 +558,7 @@ pre.commit-description {
background-color: transparent;
padding: 2px;
margin: 0px;
white-space: pre-wrap;
}
#repository-url {
@@ -586,6 +589,7 @@ ul#commit-file-list {
ul#commit-file-list li.border {
padding-bottom: 2px;
border-top: 1px solid #eee;
clear: right;
}
li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 {

View File

@@ -29,7 +29,7 @@ class GitBucketCoreModuleSpec extends FunSuite {
}
test("Migration MySQL", ExternalDBTest){
val config = aMysqldConfig(v5_7_10)
val config = aMysqldConfig(v5_7_latest)
.withPort(3306)
.withUser("sa", "sa")
.withCharset(Charset.UTF8)