Compare commits

...

58 Commits

Author SHA1 Message Date
Naoki Takezoe
a51b57af2a Update README.md 2017-09-24 14:23:43 +09:00
Naoki Takezoe
17ff024166 Update README.md 2017-09-24 14:14:00 +09:00
Naoki Takezoe
ed0c8e3f2c Bump gitbucket-notifications-plugin to 1.2.0 2017-09-24 14:00:33 +09:00
Naoki Takezoe
6b762b0693 Fix version as 4.17.0 2017-09-24 12:34:00 +09:00
Naoki Takezoe
62e6d0d6e8 (refs #1722)Bump markedj to 1.0.15 2017-09-24 12:30:44 +09:00
Naoki Takezoe
6947f57bd8 Merge pull request #1719 from gitbucket/drop-file-upload-limitation
Drop uploadable file type limitation
2017-09-22 01:18:19 +09:00
Naoki Takezoe
08295afb51 (refs #1714)Drop uploadable file type limitation 2017-09-21 16:51:15 +09:00
Naoki Takezoe
8d5b494785 Update doc for JRebel 2017-09-21 16:09:52 +09:00
Naoki Takezoe
8d52fc06ed Update doc for JRebel 2017-09-21 16:01:07 +09:00
Naoki Takezoe
85b83af73f Merge pull request #1717 from gitbucket/refuse-delete-default-branch
Refuse deletion of the default branch
2017-09-21 02:20:29 +09:00
Naoki Takezoe
d99898e191 Implement checking whether a deleting branch is the default branch 2017-09-21 02:08:19 +09:00
Naoki Takezoe
2044f5b838 Refuse deletion of the default branch 2017-09-20 17:46:53 +09:00
Naoki Takezoe
bb804f6597 Merge pull request #1696 from gitbucket/api_repository_ssh_url
Add ssh_url to web hook request and API response
2017-09-20 16:18:35 +09:00
Naoki Takezoe
4d3756ac0a Merge pull request #1712 from kounoike/pr-fix-1701
Allow anonymous access to github style git url redirect. fix #1701
2017-09-19 09:02:28 +09:00
Naoki Takezoe
0739b3048b Merge pull request #1713 from kounoike/pr-gc-call
repo > Settings > Danger Zone > Garbage collection doesn't executed.
2017-09-18 17:21:29 +09:00
KOUNOIKE Yuusuke
ff5a05511d git.gc() doesn't called. 2017-09-18 14:38:14 +09:00
KOUNOIKE Yuusuke
186ce769a2 allow anonymous access to git redirect. fix #1701 2017-09-18 14:03:04 +09:00
Naoki Takezoe
848c698491 Update sbt plugins 2017-09-18 02:18:28 +09:00
Naoki Takezoe
41ea2087d1 Fix guideline to accept any languages 2017-09-17 21:30:46 +09:00
Naoki Takezoe
1d77727867 Merge pull request #1711 from gitbucket/improve-mail-api
Make Mailer API more general
2017-09-17 01:32:16 +09:00
Naoki Takezoe
051d059e5c Improve Mailer API 2017-09-17 00:49:10 +09:00
Naoki Takezoe
324107beef Change the setting icon 2017-09-16 11:06:44 +09:00
Naoki Takezoe
801d71b6d2 Update Mailer.send() signature 2017-09-16 03:11:40 +09:00
Naoki Takezoe
106f7a41d8 Add Mailer.send() method without login account 2017-09-15 20:57:50 +09:00
Naoki Takezoe
1b46651c32 Merge pull request #1709 from kounoike/PR-keep-hash-at-signin
Keep hash when sign in.
2017-09-15 00:11:22 +09:00
KOUNOIKE Yuusuke
459b25e075 Change hash field to Option[String]. 2017-09-14 21:42:29 +09:00
Naoki Takezoe
e3c3a61f0b Fix NullPointerException in the first run from source code 2017-09-14 11:29:41 +09:00
KOUNOIKE Yuusuke
a311ee5ef5 Keep hash when sign in. fix #1706 2017-09-14 04:48:31 +09:00
Naoki Takezoe
b13decf0e9 (refs #1702)Implement keyboard shortcut "y"
which transfers a browser to URL with commit id.
2017-09-13 14:10:00 +09:00
Naoki Takezoe
f6b92ef40b Fix style of buttons 2017-09-13 03:36:04 +09:00
Naoki Takezoe
919b1d01e3 Fix labels and priorities styles 2017-09-13 03:23:15 +09:00
Naoki Takezoe
fe73c11611 Fix styles 2017-09-12 20:57:35 +09:00
Naoki Takezoe
c8af6c4b5a Fix styles 2017-09-12 20:54:15 +09:00
Naoki Takezoe
5d2d36dccf Fix styles 2017-09-12 20:48:37 +09:00
Naoki Takezoe
a11f711778 Merge pull request #1703 from kounoike/pr-switch-to-commitid-url
Change URL with commit ID when user selects line(s).
2017-09-12 18:52:28 +09:00
Naoki Takezoe
6920704caa Fix pull request title edit button 2017-09-12 16:19:57 +09:00
Naoki Takezoe
451a6ef359 Fix button style 2017-09-12 11:58:48 +09:00
KOUNOIKE Yuusuke
a93f4cc780 Transfer to URL with commit ID when select line(s). 2017-09-12 11:24:13 +09:00
Naoki Takezoe
f9fda26e7a Add RepositoryService#hasOwnerRole() 2017-09-11 02:27:22 +09:00
Naoki Takezoe
7435902b70 Generate submodule link only when url starts with http:// or https:// 2017-09-07 02:15:10 +09:00
Naoki Takezoe
feb57c97b9 Bump to 4.17.0-SNAPSHOT 2017-09-06 15:34:09 +09:00
Naoki Takezoe
7021942a6e Merge branch 'add_assignee_to_pr_api' 2017-09-05 20:37:03 +09:00
Naoki Takezoe
9251d64de8 Avoid database access in model
Modified to pass assignee from outside of model instead.
2017-09-05 19:47:41 +09:00
Naoki Takezoe
d0c99727e9 Fix testcases 2017-09-05 16:06:06 +09:00
Naoki Takezoe
1fbfcfb446 Add ssh_url to API response 2017-09-05 15:14:54 +09:00
Naoki Takezoe
38328b2ffe What charset should be used to make patch? 2017-09-05 12:02:23 +09:00
Naoki Takezoe
3cce4e5308 Merge pull request #1670 from gitbucket/feature/get-single-commit-api
Get single commit API
2017-09-05 11:57:04 +09:00
Naoki Takezoe
757292d670 Fix response of get single commit API
Fix response of get single commit API
2017-09-05 11:41:43 +09:00
Naoki Takezoe
ef16804b49 Make patch from DiffEntry 2017-09-05 11:27:59 +09:00
Naoki Takezoe
dafabb6278 Fix import 2017-09-05 09:31:45 +09:00
Naoki Takezoe
1f37362da4 Merge branch 'master' into feature/get-single-commit-api
# Conflicts:
#	src/main/scala/gitbucket/core/controller/ApiController.scala
2017-09-05 09:28:36 +09:00
Naoki Takezoe
1dba28d153 (refs #1643)Create issue reference comment
even if it can't relate committer’s email with GitBucket account
2017-09-04 19:17:25 +09:00
Naoki Takezoe
e83b017ef2 (refs #1695)Fix plugin controllers mapping 2017-09-04 18:46:07 +09:00
Naoki Takezoe
eeabbfd599 Fix plugin reloading from PluginWatchThread 2017-09-03 22:36:03 +09:00
Naoki Takezoe
55a8602bba (#1685)Call PullRequestHook.addedComment for PR code comment 2017-09-03 04:39:20 +09:00
Naoki Takezoe
2e80e3baaf Fix testcase 2017-09-03 03:52:00 +09:00
Naoki Takezoe
efdfe2b1b5 Implementing get single commit API 2017-08-13 01:15:18 +09:00
Yasuhiro Takagi
884fc5318a Add assignee entry for the result of pull request related api 2017-06-25 13:33:27 +09:00
60 changed files with 583 additions and 393 deletions

View File

@@ -2,7 +2,5 @@
- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues and pull requests whether there is a same request in the past.
- 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. If you don't wanna waste your time to make a pull request, ask us about your idea at [gitter room](https://gitter.im/gitbucket/gitbucket) before staring your work.
- We can also support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- You can edit the GitBucket documentation on Wiki if you have a GitHub account. When you find any mistakes or lacks in the documentation, please update it directly.
- Write an issue, a pull request, commit messages and comments in source code in English.
- All your contributions are handled as [Apache Software License, Version 2.0](https://github.com/gitbucket/gitbucket/blob/master/LICENSE). When you create a pull request or update the documentation, we assume you agreed this clause.

View File

@@ -16,4 +16,3 @@
- *describe the problem and its symptoms*
- *explain how to reproduce*
- *attach whatever information that can help understanding the context (screen capture, log files)*
- *do your best to use a correct english (re-read yourself)*

3
.github/SUPPORT.md vendored
View File

@@ -2,5 +2,4 @@
- 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 Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- Write an issue in English. Since we can't support issues written in other languages, we close them forcibly.
- Write issues in English if it's possible. It enables many of contributors to help you.

View File

@@ -66,12 +66,17 @@ Support
- 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.17.0 - 30 Sep 2017
- [gitbucket-ci-plugin](https://github.com/takezoe/gitbucket-ci-plugin) is available
- Transferring to URL with commit ID
- Drop uploadable file type limitation
- Improve Mailer API
- Web API and webhook enhancement
### 4.16.0 - 2 Sep 2017
- Support AdminLTE color skin
- Improve unexpected error handling

View File

@@ -1,6 +1,6 @@
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.16.0"
val GitBucketVersion = "4.17.0"
val ScalatraVersion = "2.5.0"
val JettyVersion = "9.3.19.v20170502"
@@ -29,7 +29,7 @@ libraryDependencies ++= Seq(
"io.github.gitbucket" %% "scalatra-forms" % "1.1.0",
"commons-io" % "commons-io" % "2.5",
"io.github.gitbucket" % "solidbase" % "1.0.2",
"io.github.gitbucket" % "markedj" % "1.0.14",
"io.github.gitbucket" % "markedj" % "1.0.15",
"org.apache.commons" % "commons-compress" % "1.13",
"org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.3",

View File

@@ -2,23 +2,15 @@ JRebel integration (optional)
=============================
[JRebel](https://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster.
JRebel is generally able to eliminate the need for the following slow "app restart" in sbt following a code change:
JRebel is generally able to eliminate the need for the slow "app restart" per modification of codes. Alsp it's only used during development, and doesn't change your deployed app in any way.
```
> jetty:start
```
While JRebel is not open source, it does reload your code faster than the `~;copy-resources;aux-compile` way of doing things using `sbt`.
It's only used during development, and doesn't change your deployed app in any way.
JRebel used to be free for Scala developers, but that changed recently, and now there's a cost associated with usage for Scala. There are trial plans and free non-commercial licenses available if you just want to try it out.
JRebel is not open source, but we can use it free for non-commercial use.
----
## 1. Get a JRebel license
Sign up for a [usage plan](https://my.jrebel.com/). You will need to create an account.
Sign up for a [myJRebel](https://my.jrebel.com/register). You will need to create an account.
## 2. Download JRebel
@@ -27,9 +19,7 @@ Next, unzip the downloaded file.
## 3. Activate
Follow the [instructions on the JRebel website](https://zeroturnaround.com/software/jrebel/download/prev-releases/) to activate your downloaded JRebel.
You can use the default settings for all the configurations.
Follow `readme.txt` in the extracted directory to activate your downloaded JRebel.
You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment.
@@ -41,13 +31,13 @@ You only need to tell jvm where to find the jrebel jar.
To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux), and add the following line:
```bash
export JREBEL=/path/to/jrebel/jrebel.jar
export JREBEL=/path/to/jrebel/legacy/jrebel.jar
```
For example, if you unzipped your JRebel download in your home directory, you whould use:
```bash
export JREBEL=~/jrebel/jrebel.jar
export JREBEL=~/jrebel/legacy/jrebel.jar
```
Now reload your shell:
@@ -73,39 +63,26 @@ $ ./sbt
You will start the servlet container slightly differently now that you're using sbt.
```
> jetty:start
> jetty:quickstart
:
[info] starting server ...
[success] Total time: 3 s, completed Jan 3, 2016 9:47:55 PM
2016-01-03 21:47:57 JRebel:
2016-01-03 21:47:57 JRebel: A newer version '6.3.1' is available for download
2016-01-03 21:47:57 JRebel: from http://zeroturnaround.com/software/jrebel/download/
2016-01-03 21:47:57 JRebel:
2016-01-03 21:47:58 JRebel: Contacting myJRebel server ..
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/classes' will be monitored for changes.
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/test-classes' will be monitored for changes.
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/webapp' will be monitored for changes.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: #############################################################
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: JRebel Legacy Agent 6.2.5 (201509291538)
2016-01-03 21:48:00 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Over the last 30 days JRebel prevented
2016-01-03 21:48:00 JRebel: at least 182 redeploys/restarts saving you about 7.4 hours.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Over the last 324 days JRebel prevented
2016-01-03 21:48:00 JRebel: at least 1538 redeploys/restarts saving you about 62.4 hours.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Licensed to nazo king (using myJRebel).
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: #############################################################
2016-01-03 21:48:00 JRebel:
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: #############################################################
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Legacy Agent 7.0.15 (201709080836)
2017-09-21 15:46:35 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Over the last 2 days JRebel prevented
2017-09-21 15:46:35 JRebel: at least 8 redeploys/restarts saving you about 0.3 hours.
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Licensed to Naoki Takezoe (using myJRebel).
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: #############################################################
2017-09-21 15:46:35 JRebel:
:
> ~ copy-resources
[success] Total time: 0 s, completed Jan 3, 2016 9:13:54 PM
> ~compile
[success] Total time: 2 s, completed 2017/09/21 15:50:06
1. Waiting for source changes... (press enter to interrupt)
```
@@ -114,12 +91,11 @@ For example, you can change the title on `src/main/twirl/gitbucket/core/main.sca
```html
:
<a class="navbar-brand" href="@path/">
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
@defining(AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
<a href="@context.path/" class="logo">
<img src="@helpers.assets("/common/images/gitbucket.svg")" style="width: 24px; height: 24px; display: inline;"/>
GitBucket
change code !!!!!!!!!!!!!!!!
<span class="header-version">@gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion</span>
</a>
:
```
@@ -128,21 +104,17 @@ If JRebel is doing is correctly installed you will see a notice for you:
```
1. Waiting for source changes... (press enter to interrupt)
2016-01-03 21:48:42 JRebel: Reloading class 'gitbucket.core.html.main$'.
[info] Wrote rebel.xml to /git/gitbucket/target/scala-2.11/resource_managed/main/rebel.xml
[info] Compiling 1 Scala source to /git/gitbucket/target/scala-2.11/classes...
[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
2. Waiting for source changes... (press enter to interrupt)
[info] Compiling 1 Scala source to /Users/naoki.takezoe/gitbucket/target/scala-2.12/classes...
[success] Total time: 1 s, completed 2017/09/21 15:55:40
```
And you reload browser, JRebel give notice of that it has reloaded classes:
```
[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
2. Waiting for source changes... (press enter to interrupt)
2016-01-03 21:49:13 JRebel: Reloading class 'gitbucket.core.html.main$'.
2017-09-21 15:55:40 JRebel: Reloading class 'gitbucket.core.html.main$'.
```
## 6. Limitations
JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routes patterns, there is nothing JRebel can do, you will have to run `jetty:start`.
JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routing patterns, there is nothing JRebel can do, you will have to restart by `jetty:quickstart`.

View File

@@ -5,9 +5,9 @@
"description": "Provides notifications feature on GitBucket.",
"versions": [
{
"version": "1.1.0",
"range": ">=4.16.0",
"file": "gitbucket-notifications-plugin_2.12-1.1.0.jar"
"version": "1.2.0",
"range": ">=4.17.0",
"file": "gitbucket-notifications-plugin_2.12-1.2.0.jar"
}
],
"default": true

View File

@@ -1,8 +1,8 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.1")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.7")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.0")
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3")
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M15")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC11")

View File

@@ -41,5 +41,6 @@ object GitBucketCoreModule extends Module("gitbucket-core",
),
new Version("4.14.1"),
new Version("4.15.0"),
new Version("4.16.0")
new Version("4.16.0"),
new Version("4.17.0")
)

View File

@@ -0,0 +1,124 @@
package gitbucket.core.api
import gitbucket.core.model.Account
import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo}
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import ApiCommits._
case class ApiCommits(
url: ApiPath,
sha: String,
html_url: ApiPath,
comment_url: ApiPath,
commit: Commit,
author: ApiUser,
committer: ApiUser,
parents: Seq[Tree],
stats: Stats,
files: Seq[File]
)
object ApiCommits {
case class Commit(
url: ApiPath,
author: ApiPersonIdent,
committer: ApiPersonIdent,
message: String,
comment_count: Int,
tree: Tree
)
case class Tree(
url: ApiPath,
sha: String
)
case class Stats(
additions: Int,
deletions: Int,
total: Int
)
case class File(
filename: String,
additions: Int,
deletions: Int,
changes: Int,
status: String,
raw_url: ApiPath,
blob_url: ApiPath,
patch: String
)
def apply(repositoryName: RepositoryName, commitInfo: CommitInfo, diffs: Seq[DiffInfo], author: Account, committer: Account,
commentCount: Int): ApiCommits = {
val files = diffs.map { diff =>
var additions = 0
var deletions = 0
diff.patch.getOrElse("").split("\n").foreach { line =>
if(line.startsWith("+")) additions = additions + 1
if(line.startsWith("-")) deletions = deletions + 1
}
File(
filename = if(diff.changeType == ChangeType.DELETE){ diff.oldPath } else { diff.newPath },
additions = additions,
deletions = deletions,
changes = additions + deletions,
status = diff.changeType match {
case ChangeType.ADD => "added"
case ChangeType.MODIFY => "modified"
case ChangeType.DELETE => "deleted"
case ChangeType.RENAME => "renamed"
case ChangeType.COPY => "copied"
},
raw_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.parents.head}/${diff.oldPath}")
} else {
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.id}/${diff.newPath}")
},
blob_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.parents.head}/${diff.oldPath}")
} else {
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.id}/${diff.newPath}")
},
patch = diff.patch.getOrElse("")
)
}
ApiCommits(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
sha = commitInfo.id,
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"),
comment_url = ApiPath(""),
commit = Commit(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
author = ApiPersonIdent.author(commitInfo),
committer = ApiPersonIdent.committer(commitInfo),
message = commitInfo.shortMessage,
comment_count = commentCount,
tree = Tree(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${commitInfo.id}"), // TODO This endpoint has not been implemented yet.
sha = commitInfo.id
)
),
author = ApiUser(author),
committer = ApiUser(committer),
parents = commitInfo.parents.map { parent =>
Tree(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${parent}"), // TODO This endpoint has not been implemented yet.
sha = parent
)
},
stats = Stats(
additions = files.map(_.additions).sum,
deletions = files.map(_.deletions).sum,
total = files.map(_.additions).sum + files.map(_.deletions).sum
),
files = files
)
}
}

View File

@@ -1,6 +1,13 @@
package gitbucket.core.api
/**
* path for api url. if set path '/repos/aa/bb' then, expand 'http://server:post/repos/aa/bb' when converted to json.
* Path for API url.
* If set path '/repos/aa/bb' then, expand 'http://server:port/repos/aa/bb' when converted to json.
*/
case class ApiPath(path: String)
/**
* Path for git repository via SSH.
* If set path '/aa/bb.git' then, expand 'git@server:port/aa/bb.git' when converted to json.
*/
case class SshPath(path: String)

View File

@@ -3,7 +3,6 @@ package gitbucket.core.api
import gitbucket.core.model.{Account, Issue, IssueComment, PullRequest}
import java.util.Date
/**
* https://developer.github.com/v3/pulls/
*/
@@ -19,7 +18,8 @@ case class ApiPullRequest(
merged_by: Option[ApiUser],
title: String,
body: String,
user: ApiUser) {
user: ApiUser,
assignee: Option[ApiUser]){
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
@@ -39,6 +39,7 @@ object ApiPullRequest{
headRepo: ApiRepository,
baseRepo: ApiRepository,
user: ApiUser,
assignee: Option[ApiUser],
mergedComment: Option[(IssueComment, Account)]
): ApiPullRequest =
ApiPullRequest(
@@ -59,14 +60,16 @@ object ApiPullRequest{
merged_by = mergedComment.map { case (_, account) => ApiUser(account) },
title = issue.title,
body = issue.content.getOrElse(""),
user = user
user = user,
assignee = assignee
)
case class Commit(
sha: String,
ref: String,
repo: ApiRepository)(baseOwner:String){
val label = if( baseOwner == repo.owner.login ){ ref }else{ s"${repo.owner.login}:${ref}" }
val label = if( baseOwner == repo.owner.login ){ ref } else { s"${repo.owner.login}:${ref}" }
val user = repo.owner
}
}

View File

@@ -24,6 +24,7 @@ case class ApiRepository(
val http_url = ApiPath(s"/git/${full_name}.git")
val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(s":${full_name}.git"))
}
object ApiRepository{
@@ -55,12 +56,13 @@ object ApiRepository{
def forDummyPayload(owner: ApiUser): ApiRepository =
ApiRepository(
name="dummy",
full_name=s"${owner.login}/dummy",
description="",
watchers=0,
forks=0,
`private`=false,
default_branch="master",
owner=owner)(true)
name = "dummy",
full_name = s"${owner.login}/dummy",
description = "",
watchers = 0,
forks = 0,
`private` = false,
default_branch = "master",
owner = owner
)(true)
}

View File

@@ -10,7 +10,7 @@ import scala.util.Try
object JsonFormat {
case class Context(baseUrl: String)
case class Context(baseUrl: String, sshUrl: Option[String])
val parserISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
@@ -33,23 +33,31 @@ object JsonFormat {
FieldSerializer[ApiComment]() +
FieldSerializer[ApiContents]() +
FieldSerializer[ApiLabel]() +
FieldSerializer[ApiCommits]() +
FieldSerializer[ApiCommits.Commit]() +
FieldSerializer[ApiCommits.Tree]() +
FieldSerializer[ApiCommits.Stats]() +
FieldSerializer[ApiCommits.File]() +
ApiBranchProtection.enforcementLevelSerializer
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
(
{
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
},
{
case ApiPath(path) => JString(c.baseUrl + path)
}
)
)
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](_ => ({
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
}, {
case ApiPath(path) => JString(c.baseUrl + path)
}))
def sshPathSerializer(c: Context) = new CustomSerializer[SshPath](_ => ({
case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) => SshPath(s.substring(c.sshUrl.get.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
}, {
case SshPath(path) => c.sshUrl.map { sshUrl => JString(sshUrl + path) } getOrElse JNothing
}))
/**
* convert object to json string
*/
def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
def apply(obj: AnyRef)(implicit c: Context): String =
Serialization.write(obj)(jsonFormats + apiPathSerializer(c) + sshPathSerializer(c))
}

View File

@@ -12,6 +12,7 @@ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util._
import gitbucket.core.view.helpers.{isRenderable, renderMarkup}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.scalatra.{Created, NoContent, UnprocessableEntity}
import scala.collection.JavaConverters._
@@ -50,6 +51,7 @@ trait ApiControllerBase extends ControllerBase {
with LabelsService
with MilestonesService
with PullRequestService
with CommitsService
with CommitStatusService
with RepositoryCreationService
with IssueCreationService
@@ -499,7 +501,7 @@ trait ApiControllerBase extends ControllerBase {
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account)] =
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
searchPullRequestByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
@@ -507,13 +509,14 @@ trait ApiControllerBase extends ControllerBase {
repos = repository.owner -> repository.name
)
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
})
@@ -530,6 +533,7 @@ trait ApiControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
JsonFormat(ApiPullRequest(
@@ -538,6 +542,7 @@ trait ApiControllerBase extends ControllerBase {
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
))
}) getOrElse NotFound()
@@ -628,6 +633,52 @@ trait ApiControllerBase extends ControllerBase {
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/repos/commits/#get-a-single-commit
*/
get("/api/v3/repos/:owner/:repo/commits/:sha")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
val sha = params("sha")
using(Git.open(getRepositoryDir(owner, name))){ git =>
val repo = git.getRepository
val objectId = repo.resolve(sha)
val commitInfo = using(new RevWalk(repo)){ revWalk =>
new CommitInfo(revWalk.parseCommit(objectId))
}
JsonFormat(ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.head, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, sha).size
))
}
})
private def getAccount(userName: String, email: String): Account = {
getAccountByMailAddress(email).getOrElse {
Account(
userName = userName,
fullName = userName,
mailAddress = email,
password = "xxx",
isAdmin = false,
url = None,
registeredDate = new java.util.Date(),
updatedDate = new java.util.Date(),
lastLoginDate = None,
image = None,
isGroupAccount = false,
isRemoved = true,
description = None
)
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName

View File

@@ -48,7 +48,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
}, FileUtil.isUploadableType)
}, _ => true)
}
post("/wiki/:owner/:repository"){
@@ -85,7 +85,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
fileName
}
}
}, FileUtil.isUploadableType)
}, _ => true)
}
} getOrElse BadRequest()
}
@@ -113,7 +113,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
}
}
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
private def execute(f: (FileItem, String) => Unit , mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)

View File

@@ -19,11 +19,12 @@ trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with UsersAuthenticator with ReferrerAuthenticator =>
case class SignInForm(userName: String, password: String)
case class SignInForm(userName: String, password: String, hash: Option[String])
val signinForm = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
"password" -> trim(label("Password", text(required))),
"hash" -> trim(optional(text()))
)(SignInForm.apply)
// val searchForm = mapping(
@@ -54,7 +55,7 @@ trait IndexControllerBase extends ControllerBase {
post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account)
case Some(account) => signin(account, form.hash)
case None => {
flash += "userName" -> form.userName
flash += "password" -> form.password
@@ -86,7 +87,7 @@ trait IndexControllerBase extends ControllerBase {
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: Account) = {
private def signin(account: Account, hash: Option[String]) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
@@ -98,7 +99,7 @@ trait IndexControllerBase extends ControllerBase {
if(redirectUrl.stripSuffix("/") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
redirect(redirectUrl + hash.getOrElse(""))
}
}.getOrElse {
redirect("/")

View File

@@ -29,7 +29,7 @@ trait PreProcessControllerBase extends ControllerBase {
*/
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register")) {
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs")) {
Unauthorized()
} else {
pass()

View File

@@ -324,8 +324,8 @@ trait PullRequestsControllerBase extends ControllerBase {
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner) {
@@ -411,8 +411,8 @@ trait PullRequestsControllerBase extends ControllerBase {
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
@@ -505,7 +505,7 @@ trait PullRequestsControllerBase extends ControllerBase {
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
private def parseCompareIdentifier(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))

View File

@@ -393,7 +393,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/gc")(ownerOnly { repository =>
LockUtil.lock(s"${repository.owner}/${repository.name}") {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.gc()
git.gc().call()
}
}
flash += "info" -> "Garbage collection has been executed."

View File

@@ -431,7 +431,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
try {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit =>
JGitUtil.getDiffs(git, id) match {
JGitUtil.getDiffs(git, id, false) match {
case (diffs, oldCommitId) =>
html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
@@ -478,9 +478,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get
form.issueId match {
case Some(issueId) =>
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
getPullRequest(repository.owner, repository.name, issueId).foreach { case (issue, pullRequest) =>
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId, form.content, issue, repository))
callPullRequestReviewCommentWebHook("create", comment, repository, issue, pullRequest, context.baseUrl, context.loginAccount.get)
}
case None =>
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(comment, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
})

View File

@@ -174,10 +174,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
try {
new Mailer(form.smtp).send(form.testAddress,
"Test message from GitBucket", context.loginAccount.get,
"This is a test message from GitBucket.", None
)
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send(
to = form.testAddress,
subject = "Test message from GitBucket",
textMsg = "This is a test message from GitBucket.",
htmlMsg = None,
loginAccount = context.loginAccount
)
"Test mail has been sent to: " + form.testAddress

View File

@@ -76,7 +76,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true, false).filter(_.newPath == pageName + ".md"), repository,
isEditable(repository), flash.get("info"))
}
})
@@ -85,7 +85,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true, false), repository,
isEditable(repository), flash.get("info"))
}
})

View File

@@ -3,18 +3,20 @@ package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.model.Issue
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.model.Profile._
import profile.api._
trait IssueHook {
def created(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def closed(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def reopened(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def created(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
}
trait PullRequestHook extends IssueHook {
def merged(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def merged(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
}

View File

@@ -257,13 +257,14 @@ object PluginRegistry {
if(installedDir.exists){
FileUtils.deleteDirectory(installedDir)
}
installedDir.mkdir()
installedDir.mkdirs()
val pluginJars = listPluginJars(pluginDir)
val extraJars = extraPluginDir.map { extraDir => listPluginJars(new File(extraDir)) }.getOrElse(Nil)
(extraJars ++ pluginJars).foreach { pluginJar =>
val installedJar = new File(installedDir, pluginJar.getName)
FileUtils.copyFile(pluginJar, installedJar)
logger.info(s"Initialize ${pluginJar.getName}")
@@ -400,12 +401,15 @@ class PluginWatchThread(context: ServletContext, dir: String) extends Thread wit
events.foreach { event =>
logger.info(event.kind + ": " + event.context)
}
gitbucket.core.servlet.Database() withTransaction { session =>
logger.info("Reloading plugins...")
PluginRegistry.reload(context, loadSystemSettings(), session.conn)
logger.info("Reloading finished.")
}
new Thread {
override def run(): Unit = {
gitbucket.core.servlet.Database() withTransaction { session =>
logger.info("Reloading plugins...")
PluginRegistry.reload(context, loadSystemSettings(), session.conn)
logger.info("Reloading finished.")
}
}
}.start()
}
detectedWatchKey.reset()
}

View File

@@ -202,15 +202,16 @@ trait IssuesService {
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, true, offset, limit, repos)
.join(PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.join(Repositories).on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byRepository(t1.userName, t1.repositoryName) }
.join(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => (t1, t5, t2.commentCount, t3, t4, t6) }
.join (PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.join (Repositories).on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byRepository(t1.userName, t1.repositoryName) }
.join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.joinLeft(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName}
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
.list
}
@@ -455,9 +456,8 @@ trait IssuesService {
def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session): Unit = {
extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
val userName = getAccountByMailAddress(commit.committerEmailAddress).map(_.userName).getOrElse(commit.committerName)
createComment(owner, repository, userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}

View File

@@ -46,12 +46,17 @@ trait ProtectedBranchService {
object ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService with RepositoryService with AccountService {
override def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Option[String] = {
val branch = command.getRefName.stripPrefix("refs/heads/")
if(branch != command.getRefName){
getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
val repositoryInfo = getRepository(owner, repository)
if(command.getType == ReceiveCommand.Type.DELETE && repositoryInfo.exists(_.repository.defaultBranch == branch)){
Some(s"refusing to delete the branch: ${command.getRefName}.")
} else {
getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
}
} else {
None
}

View File

@@ -230,7 +230,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true, false)
(commits, diffs)
}

View File

@@ -413,6 +413,15 @@ trait RepositoryService { self: AccountService =>
q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1)
}
def hasOwnerRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true
case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName)) => true
case _ => false
}
}
def hasDeveloperRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {

View File

@@ -140,17 +140,16 @@ object SystemSettingsService {
ldapAuthentication: Boolean,
ldap: Option[Ldap],
skinName: String){
def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
def sshAddress:Option[SshAddress] =
for {
host <- sshHost if ssh
}
yield SshAddress(
host,
sshPort.getOrElse(DefaultSshPort),
"git"
)
def baseUrl(request: HttpServletRequest): String = baseUrl.fold {
val url = request.getRequestURL.toString
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
url.substring(0, len).stripSuffix("/")
} (_.stripSuffix("/"))
def sshAddress:Option[SshAddress] = sshHost.collect { case host if ssh =>
SshAddress(host, sshPort.getOrElse(DefaultSshPort), "git")
}
}
case class Ldap(

View File

@@ -228,16 +228,18 @@ trait WebHookPullRequestService extends WebHookService {
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -273,12 +275,14 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._
for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
} yield {
val payload = WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
@@ -296,16 +300,17 @@ trait WebHookPullRequestService extends WebHookService {
trait WebHookPullRequestReviewCommentService extends WebHookService {
self: AccountService with RepositoryService with PullRequestService with IssuesService with CommitsService =>
def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)
def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo,
issue: Issue, pullRequest: PullRequest, baseUrl: String, sender: Account)
(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){
val users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestReviewCommentPayload(
@@ -313,6 +318,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
comment = comment,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -424,6 +430,7 @@ object WebHookService {
def apply(action: String,
issue: Issue,
issueUser: Account,
assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -441,6 +448,7 @@ object WebHookService {
headRepo = headRepoPayload,
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
)
@@ -495,6 +503,7 @@ object WebHookService {
comment: CommitComment,
issue: Issue,
issueUser: Account,
assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -502,7 +511,7 @@ object WebHookService {
baseOwner: Account,
sender: Account,
mergedComment: Option[(IssueComment, Account)]
) : WebHookPullRequestReviewCommentPayload = {
): WebHookPullRequestReviewCommentPayload = {
val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender)
@@ -521,6 +530,7 @@ object WebHookService {
headRepo = headRepoPayload,
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
),
repository = baseRepoPayload,

View File

@@ -156,9 +156,13 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository)
val settings = loadSystemSettings()
val baseUrl = settings.baseUrl(request)
val sshUrl = settings.sshAddress.map { x => s"${x.genericUser}@${x.host}:${x.port}" }
if(!repository.endsWith(".wiki")){
defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
@@ -166,7 +170,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
if(repository.endsWith(".wiki")){
defining(request) { implicit r =>
receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.stripSuffix(".wiki"), pusher, baseUrl))
receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.stripSuffix(".wiki"), pusher, baseUrl, sshUrl))
}
}
}
@@ -178,7 +182,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService with CommitsService {
@@ -219,7 +223,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
implicit val apiContext = api.JsonFormat.Context(baseUrl)
implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl)
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") {
@@ -320,7 +324,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: String)
class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook with WebHookService with AccountService with RepositoryService {
private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook])
@@ -329,7 +333,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
Database() withTransaction { implicit session =>
try {
commands.asScala.headOption.foreach { command =>
implicit val apiContext = api.JsonFormat.Context(baseUrl)
implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl)
val refName = command.getRefName.split("/")
val commitIds = if (refName(1) == "tags") {
None
@@ -347,7 +351,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
diffs._1.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") =>
val action = if(diff.changeType == ChangeType.ADD) "created" else "edited"
val fileName = diff.newPath
println(action + " - " + fileName + " - " + commit.id)
//println(action + " - " + fileName + " - " + commit.id)
(action, fileName, commit.id)
}
}

View File

@@ -24,7 +24,7 @@ class PluginControllerFilter extends Filter {
val controller = PluginRegistry().getControllers().filter { case (_, path) =>
val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI
val start = path.replaceFirst("/\\*$", "/")
path.endsWith("/*") && (requestUri + "/").startsWith(start)
(requestUri + "/").startsWith(start)
}
val filterChainWrapper = controller.foldLeft(chain){ case (chain, (controller, _)) =>

View File

@@ -154,7 +154,7 @@ class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCo
}
}
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshUrl: Option[String]) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService with DeployKeyService {
override protected def runTask(authType: AuthType): Unit = {
@@ -169,7 +169,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
val repository = git.getRepository
val receive = new ReceivePack(repository)
if (!repoName.endsWith(".wiki")) {
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl)
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
}
@@ -216,7 +216,7 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) exte
}
class GitCommandFactory(baseUrl: String) extends CommandFactory {
class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
@@ -227,7 +227,7 @@ class GitCommandFactory(baseUrl: String) extends CommandFactory {
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl)
case _ => new UnknownCommand(command)
}
}

View File

@@ -22,7 +22,7 @@ object SshServer {
provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setCommandFactory(new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}")))
server.setShellFactory(new NoShell(sshAddress))
}

View File

@@ -28,8 +28,6 @@ object FileUtil {
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isUploadableType(name: String): Boolean = mimeTypeWhiteList contains getMimeType(name)
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0)
@@ -53,16 +51,6 @@ object FileUtil {
}
}
val mimeTypeWhiteList: Array[String] = Array(
"application/pdf",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"image/gif",
"image/jpeg",
"image/png",
"text/plain")
def getLfsFilePath(owner: String, repository: String, oid: String): String =
Directory.getLfsDir(owner, repository) + "/" + oid

View File

@@ -22,7 +22,8 @@ object Implicits {
// Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = JsonFormat.Context(context.baseUrl)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x => s"${x.genericUser}@${x.host}:${x.port}" })
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
@@ -77,11 +78,6 @@ object Implicits {
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^" + quote(request.getContextPath) + "/git/", "/")
def baseUrl:String = {
val url = request.getRequestURL.toString
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
url.substring(0, len).stripSuffix("/")
}
}
implicit class RichSession(private val session: HttpSession) extends AnyVal {

View File

@@ -1,5 +1,7 @@
package gitbucket.core.util
import java.io.ByteArrayOutputStream
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
import Directory._
@@ -22,6 +24,7 @@ import java.util.function.Consumer
import org.cache2k.{Cache2kBuilder, CacheEntry}
import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException}
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
@@ -114,7 +117,8 @@ object JGitUtil {
newObjectId: Option[String],
oldMode: String,
newMode: String,
tooLarge: Boolean
tooLarge: Boolean,
patch: Option[String]
)
/**
@@ -515,9 +519,10 @@ object JGitUtil {
}
/**
* Returns the tuple of diff of the given commit and the previous commit id.
* Returns the tuple of diff of the given commit and parent commit ids.
* DiffInfos returned from this method don't include the patch property.
*/
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
def getDiffs(git: Git, id: String, fetchContent: Boolean): (List[DiffInfo], Option[String]) = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
i.hasNext match {
@@ -538,7 +543,7 @@ object JGitUtil {
} else {
commits(1)
}
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
(getDiffs(git, oldCommit.getName, id, fetchContent, false), Some(oldCommit.getName))
} else {
// initial commit
@@ -551,7 +556,7 @@ object JGitUtil {
buffer.append((if(!fetchContent){
DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
oldPath = "",
newPath = treeWalk.getPathString,
oldContent = None,
newContent = None,
@@ -561,12 +566,13 @@ object JGitUtil {
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString,
tooLarge = false
tooLarge = false,
patch = None
)
} else {
DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
oldPath = "",
newPath = treeWalk.getPathString,
oldContent = None,
newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
@@ -576,7 +582,8 @@ object JGitUtil {
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString,
tooLarge = false
tooLarge = false,
patch = None
)
}))
}
@@ -586,7 +593,7 @@ object JGitUtil {
}
}
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = {
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean, makePatch: Boolean): List[DiffInfo] = {
val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
@@ -612,7 +619,8 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = true
tooLarge = true,
patch = None
)
} else {
val oldIsImage = FileUtil.isImage(diff.getOldPath)
@@ -630,7 +638,8 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false
tooLarge = false,
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None)
)
} else {
DiffInfo(
@@ -645,13 +654,23 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false
tooLarge = false,
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None)
)
}
}
}.toList
}
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
val out = new ByteArrayOutputStream()
using(new DiffFormatter(out)){ formatter =>
formatter.setRepository(git.getRepository)
formatter.format(diff)
val patch = new String(out.toByteArray) // TODO charset???
patch.split("\n").drop(4).mkString("\n")
}
}
/**
* Returns the list of branch names of the specified commit.

View File

@@ -0,0 +1,68 @@
package gitbucket.core.util
import gitbucket.core.model.Account
import gitbucket.core.service.SystemSettingsService
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import SystemSettingsService.SystemSettings
class Mailer(settings: SystemSettings){
def send(to: String, subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Unit = {
createMail(subject, textMsg, htmlMsg, loginAccount).foreach { email =>
email.addTo(to).send
}
}
def sendBcc(bcc: Seq[String], subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Unit = {
createMail(subject, textMsg, htmlMsg, loginAccount).foreach { email =>
bcc.foreach { address =>
email.addBcc(address)
}
email.send()
}
}
def createMail(subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Option[HtmlEmail] = {
if(settings.notification == true){
settings.smtp.map { smtp =>
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
if(ssl == true) {
email.setSslSmtpPort(smtp.port.get.toString)
}
}
smtp.starttls.foreach { starttls =>
email.setStartTLSEnabled(starttls)
email.setStartTLSRequired(starttls)
}
smtp.fromAddress
.map (_ -> smtp.fromName.getOrElse(loginAccount.map(_.userName).getOrElse("GitBucket")))
.orElse (Some("notifications@gitbucket.com" -> loginAccount.map(_.userName).getOrElse("GitBucket")))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setTextMsg(textMsg)
htmlMsg.foreach { msg =>
email.setHtmlMsg(msg)
}
email
}
} else None
}
}
//class MockMailer extends Notifier {
// def toNotify(subject: String, textMsg: String, htmlMsg: Option[String] = None)
// (recipients: Account => Session => Seq[String])(implicit context: Context): Unit = ()
//}

View File

@@ -1,99 +0,0 @@
package gitbucket.core.util
import gitbucket.core.model.{Session, Account}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.servlet.Database
import scala.concurrent._
import scala.util.{Success, Failure}
import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory
import gitbucket.core.controller.Context
import SystemSettingsService.Smtp
/**
* The trait for notifications.
* This is used by notifications plugin, which provides notifications feature on GitBucket.
* Please see the plugin for details.
*/
trait Notifier {
def toNotify(subject: String, textMsg: String)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit = {
toNotify(subject, textMsg, None)(recipients)
}
def toNotify(subject: String, textMsg: String, htmlMsg: Option[String])
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit
}
object Notifier {
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
case _ => new MockMailer
}
}
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(subject: String, textMsg: String, htmlMsg: Option[String] = None)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit = {
context.loginAccount.foreach { loginAccount =>
val database = Database()
val f = Future {
database withSession { session =>
recipients(loginAccount)(session) foreach { to =>
send(to, subject, loginAccount, textMsg, htmlMsg)
}
}
"Notifications Successful."
}
f.onComplete {
case Success(s) => logger.debug(s)
case Failure(t) => logger.error("Notifications Failed.", t)
}
}
}
def send(to: String, subject: String, loginAccount: Account, textMsg: String, htmlMsg: Option[String] = None): Unit = {
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
if(ssl == true) {
email.setSslSmtpPort(smtp.port.get.toString)
}
}
smtp.starttls.foreach { starttls =>
email.setStartTLSEnabled(starttls)
email.setStartTLSRequired(starttls)
}
smtp.fromAddress
.map (_ -> smtp.fromName.getOrElse(loginAccount.userName))
.orElse (Some("notifications@gitbucket.com" -> loginAccount.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setTextMsg(textMsg)
htmlMsg.foreach { msg =>
email.setHtmlMsg(msg)
}
email.addTo(to).send
}
}
class MockMailer extends Notifier {
def toNotify(subject: String, textMsg: String, htmlMsg: Option[String] = None)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit = ()
}

View File

@@ -65,8 +65,6 @@ $(function(){
url: '@context.path/upload/file/@repository.owner/@repository.name',
maxFilesize: 10,
clickable: @clickable,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +

View File

@@ -4,9 +4,9 @@
@defining(label.map(_.labelId).getOrElse("new")){ labelId =>
<div id="edit-label-area-@labelId">
<form class="form-inline">
<input type="text" id="labelName-@labelId" style="width: 300px; float: left; margin-right: 4px;" class="form-control" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
<input type="text" id="labelName-@labelId" style="width: 300px; float: left; margin-right: 4px;" class="form-control input-sm" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
<div id="label-color-@labelId" class="input-group color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
<input type="text" class="form-control" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" style="width: 100px;">
<input type="text" class="form-control input-sm" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" style="width: 100px;">
<span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
</div>
<script>
@@ -14,8 +14,8 @@
</script>
<span class="pull-right">
<span id="label-error-@labelId" class="error"></span>
<input type="button" id="cancel-@labelId" class="btn btn-default label-edit-cancel" value="Cancel">
<input type="button" id="submit-@labelId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
<input type="button" id="cancel-@labelId" class="btn btn-sm btn-default label-edit-cancel" value="Cancel">
<input type="button" id="submit-@labelId" class="btn btn-sm btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
</span>
</form>
</div>

View File

@@ -7,9 +7,9 @@
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row" id="label-@label.labelId">
<div class="col-md-8">
<div style="margin-top: 6px">
<div>
<a href="@helpers.url(repository)/issues?labels=@helpers.urlEncode(label.labelName)" id="label-row-content-@label.labelId">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; border-radius: 4px;">
<i class="octicon octicon-tag" style="color: #@label.fontColor;"></i>
@label.labelName
</span>

View File

@@ -16,9 +16,7 @@
<table class="table table-bordered table-hover table-issues">
<thead>
<tr id="label-row-header">
<th style="background-color: #eee;">
<span class="small">@labels.size labels</span>
</th>
<th>@labels.size labels</th>
</tr>
</thead>
<tbody>

View File

@@ -13,15 +13,13 @@
<table class="table table-bordered table-hover table-issues">
<thead>
<tr>
<th style="background-color: #eee;">
<span class="small">
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
@milestones.count(_._1.closedDate.isEmpty) Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
@milestones.count(_._1.closedDate.isDefined) Closed
</a>
</span>
<th>
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
@milestones.count(_._1.closedDate.isEmpty) Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
@milestones.count(_._1.closedDate.isDefined) Closed
</a>
</th>
</tr>
</thead>

View File

@@ -4,19 +4,19 @@
@defining(priority.map(_.priorityId).getOrElse("new")){ priorityId =>
<div id="edit-priority-area-@priorityId">
<form class="form-inline">
<input type="text" id="priorityName-@priorityId" style="width: 300px; float: left; margin-right: 4px;" class="form-control" value="@priority.map(_.priorityName)"@if(priorityId == "new"){ placeholder="New priority name"}/>
<input type="text" id="priorityName-@priorityId" style="width: 200px; float: left; margin-right: 4px;" class="form-control input-sm" value="@priority.map(_.priorityName)"@if(priorityId == "new"){ placeholder="New priority name"}/>
<div id="priority-color-@priorityId" class="input-group color bscp" data-color="#@priority.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
<input type="text" class="form-control" id="priorityColor-@priorityId" value="#@priority.map(_.color).getOrElse("888888")" style="width: 100px;">
<input type="text" class="form-control input-sm" id="priorityColor-@priorityId" value="#@priority.map(_.color).getOrElse("888888")" style="width: 100px;">
<span class="input-group-addon"><i style="background-color: #@priority.map(_.color).getOrElse("888888");"></i></span>
</div>
<script>
$('div#priority-color-@priorityId').colorpicker({format: "hex"});
</script>
<input type="text" id="description-@priorityId" style="width: 500px; float: left; margin-left: 4px;" class="form-control" value="@priority.flatMap(_.description).getOrElse("")" placeholder="Description..." />
<input type="text" id="description-@priorityId" style="width: 500px; float: left; margin-left: 4px;" class="form-control input-sm" value="@priority.flatMap(_.description).getOrElse("")" placeholder="Description..." />
<span class="pull-right">
<span id="priority-error-@priorityId" class="error"></span>
<input type="button" id="cancel-@priorityId" class="btn btn-default priority-edit-cancel" value="Cancel">
<input type="button" id="submit-@priorityId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(priorityId == "new") "Create priority" else "Save changes")"/>
<input type="button" id="cancel-@priorityId" class="btn btn-sm btn-default priority-edit-cancel" value="Cancel">
<input type="button" id="submit-@priorityId" class="btn btn-sm btn-success" style="margin-bottom: 0px;" value="@(if(priorityId == "new") "Create priority" else "Save changes")"/>
</span>
</form>
</div>

View File

@@ -16,9 +16,7 @@
<table id="priorities-table" class="table table-bordered table-hover table-issues">
<thead>
<tr id="priority-row-header">
<th style="background-color: #eee;">
<span class="small">@priorities.size priorities</span>
</th>
<th>@priorities.size priorities</th>
</tr>
</thead>
<tbody>

View File

@@ -10,9 +10,9 @@
@if(hasWritePermission) {
<div class="pull-left priority-sort-handle" style="margin-top: 3px"><i class="octicon octicon-grabber"></i></div>
}
<div style="margin-top: 6px">
<div>
<a href="@helpers.url(repository)/issues?priority=@helpers.urlEncode(priority.priorityName)&state=open" id="priority-row-content-@priority.priorityId">
<span style="background-color: #@priority.color; color: #@priority.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
<span style="background-color: #@priority.color; color: #@priority.fontColor; padding: 8px; border-radius: 4px;">
<i class="octicon octicon-flame" style="color: #@priority.fontColor;"></i>&nbsp;@priority.priorityName
</span>
</a>

View File

@@ -57,7 +57,7 @@
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
}
@if(context.loginAccount.isDefined && (context.loginAccount.get.isAdmin || repository.managers.contains(context.loginAccount.get.userName))){
@menuitem("/settings", "settings", "Settings", "tools")
@menuitem("/settings", "settings", "Settings", "gear")
}
@gitbucket.core.plugin.PluginRegistry().getRepositoryMenus.map { menu =>
@menu(repository, context).map { link =>
@@ -88,9 +88,6 @@
forked from <a href="@context.path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
</div>
}
@x.description.map { description =>
<div class="normal muted" style="margin-left: 36px; font-size: 80%;">@Html(helpers.detectAndRenderLinks(description, repository))</div>
}
}
</div>
</div>

View File

@@ -23,7 +23,7 @@
<div>
<div class="show-title pull-right">
@if(isManageable || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn" href="#" id="edit">Edit</a>
<a class="btn btn-default" href="#" id="edit">Edit</a>
}
@if(context.loginAccount.isDefined){
<a class="btn btn-success" href="@helpers.url(repository)/compare">New pull request</a>

View File

@@ -11,6 +11,7 @@
@gitbucket.core.html.menu("files", repository){
<div class="head">
<div class="pull-right hide-if-blame"><div class="btn-group">
<a href="@helpers.url(repository)/blob/@latestCommit.id/@pathList.mkString("/")" data-hotkey="y" style="display: none;">Transfer to URL with SHA</a>
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t">Find file</a>
</div></div>
<div class="line-age-legend">
@@ -29,6 +30,7 @@
</ol>
<span>Older</span>
</div>
<div id="branchCtrlWrapper" style="display:inline;">
@gitbucket.core.helper.html.branchcontrol(
branch,
repository,
@@ -38,6 +40,7 @@
<li><a href="@helpers.url(repository)/blob/@helpers.encodeRefName(x)/@pathList.mkString("/")">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
}
}
</div>
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
@if(i == pathList.length - 1){
@@ -130,14 +133,18 @@ $(window).load(function(){
}
var line = pos[i].id.replace(/^L/,'');
var hash = location.hash;
var commitUrl = '@helpers.url(repository)/blob/@latestCommit.id/@pathList.mkString("/")';
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
var lines = hash.split('-');
location.hash = lines[0] + '-L' + line;
window.history.pushState('', '', commitUrl + lines[0] + '-L' + line);
} else {
var p = $("#L"+line).attr('id',"");
location.hash = '#L' + line;
window.history.pushState('', '', commitUrl + '#L' + line);
p.attr('id','L'+line);
}
$("#branchCtrlWrapper .btn .muted").text("tree:");
$("#branchCtrlWrapper .btn .strong").text("@latestCommit.id.substring(0, 10)");
updateHighlighting();
}).appendTo(pre);
}
}

View File

@@ -7,7 +7,7 @@
<table class="table table-hover branches">
<thead>
<tr>
<th style="background: #f5f5f5;color: #666;" colspan="3">All branches</th>
<th colspan="3">All branches</th>
</tr>
</thead>
<tbody>
@@ -15,7 +15,7 @@
<tr>
<td class="branch-details">
@if(isProtected){ <span class="octicon octicon-shield" title="This branch is protected"></span> }
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch.name)" class="branch-name">@branch.name</a>
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch.name)">@branch.name</a>
<span class="branch-meta">
<span>Updated @gitbucket.core.helper.html.datetimeago(branch.commitTime, false)
by <span>@helpers.user(branch.committerName, branch.committerEmailAddress, "muted-link")</span>
@@ -36,7 +36,7 @@
}
}
</td>
<td>
<td class="text-right">
<div class="branch-action">
@if(repository.repository.defaultBranch != branch.name){
@branch.mergeInfo.map{ info =>

View File

@@ -15,7 +15,7 @@
<tr>
<th class="box-header">
<div class="pull-right align-right">
<a href="@helpers.url(repository)/tree/@commit.id" class="btn btn-small">Browse code</a>
<a href="@helpers.url(repository)/tree/@commit.id" class="btn btn-default">Browse code</a>
</div>
<div class="commit-log">@helpers.link(commit.summary, repository)</div>
@if(commit.description.isDefined){

View File

@@ -22,9 +22,17 @@
s"${(repository.name :: pathList).mkString("/")} at ${helpers.encodeRefName(branch)} - ${repository.owner}/${repository.name}"
}, Some(repository)) {
@gitbucket.core.html.menu("files", repository, Some(branch), info, error){
@if(pathList.isEmpty) {
@repository.repository.description.map { description =>
<p class="pc" style="margin-bottom: 10px;">
<span class="normal muted">@Html(helpers.detectAndRenderLinks(description, repository))</span>
</p>
}
}
<div class="head" style="height: 24px;">
<div class="pull-right">
<div class="btn-group">
<a href="@helpers.url(repository)/tree/@latestCommit.id/@pathList.mkString("/")" data-hotkey="y" style="display: none;">Transfer to URL with SHA</a>
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t"><i class="octicon octicon-search"></i></a>
<a href="@helpers.url(repository)/commits/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default"><i class="octicon octicon-history"></i> @if(commitCount > 10000){10000+} else {@commitCount} @helpers.plural(commitCount, "commit")</a>
</div>
@@ -102,8 +110,8 @@
<tr>
<th colspan="4" class="latest-commit">
<div>
<div class="pull-right align-right monospace" style="line-height: 18px;">
<a href="@helpers.url(repository)/commit/@latestCommit.id" class="commit-id"><span class="muted">latest commit</span> @latestCommit.id.substring(0, 10)</a>
<div class="pull-right align-right" style="line-height: 18px;">
<a href="@helpers.url(repository)/commit/@latestCommit.id" class="commit-id"><span class="muted">latest commit</span> <span class="monospace">@latestCommit.id.substring(0, 10)</span></a>
</div>
<div class="author-info">
<div class="author">
@@ -145,21 +153,35 @@
</td>
<td class="ellipsis-cell" style="width: 20%; min-width: 160px;">
@if(file.isDirectory){
@if(file.linkUrl.isDefined){
<a href="@file.linkUrl">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
} else {
<a href="@helpers.url(repository)/tree@{(branch :: pathList).map(helpers.encodeRefName).mkString("/", "/", "/")}@{helpers.encodeRefName(file.name)}">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
}
@{file.linkUrl match {
case Some(linkUrl) if linkUrl.startsWith("http://") || linkUrl.startsWith("https://") => {
<a href={linkUrl}>
<span class="simplified-path">{file.name.split("/").toList.init match {
case Nil => ""
case list => list.mkString("", "/", "/")
}}</span>
{file.name.split("/").toList.last}
</a>
}
case Some(_) => {
<span>
<span class="simplified-path">{file.name.split("/").toList.init match {
case Nil => ""
case list => list.mkString("", "/", "/")
}}</span>
{file.name.split("/").toList.last}
</span>
}
case None => {
<a href={helpers.url(repository) + "/tree" + (branch :: pathList).map(helpers.encodeRefName).mkString("/", "/", "/") + helpers.encodeRefName(file.name)}>
<span class="simplified-path">{file.name.split("/").toList.init match {
case Nil => ""
case list => list.mkString("", "/", "/")
}}</span>
{file.name.split("/").toList.last}
</a>
}
}}
} else {
<a href="@helpers.url(repository)/blob@{(branch :: pathList).map(helpers.encodeRefName).mkString("/", "/", "/")}@{helpers.encodeRefName(file.name)}">@file.name</a>
}

View File

@@ -16,7 +16,8 @@
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" class="form-control" value="@password"/>
</div>
<input type="submit" class="btn btn-success" value="Sign in"/>
<input type="hidden" name="hash"/>
<input type="submit" class="btn btn-success" value="Sign in" onClick="this.form.hash.value = window.location.hash;"/>
@if(systemSettings.allowAccountRegistration){
or <a href="@context.path/register">Create new account</a>
}

View File

@@ -50,8 +50,6 @@ $(function(){
url: '@context.path/upload/wiki/@repository.owner/@repository.name',
maxFilesize: 10,
clickable: false,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';
@@ -62,8 +60,6 @@ $(function(){
$('.clickable').dropzone({
url: '@context.path/upload/wiki/@repository.owner/@repository.name',
maxFilesize: 10,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';
@@ -78,7 +74,7 @@ $(function(){
}
$('#delete').click(function(){
return confirm('Are you sure you want to delete this page?');
return confirm('Are you sure you want to delete this page?');
});
});
</script>

View File

@@ -392,8 +392,8 @@ a.btn-danger:hover .octicon {
/* Head Menu */
/****************************************************************************/
div.headbar {
padding-top: 19px;
margin-bottom: 20px;
padding-top: 4px;
margin-bottom: 8px;
}
/****************************************************************************/
@@ -438,8 +438,9 @@ div.repository-content {
margin-left: 40px;
}
table.branches>thead>tr>th, table.branches>tbody>tr>td{
table.branches>tbody>tr>td{
padding: 12px;
line-height: 30px;
}
.branches .muted-link{
@@ -450,15 +451,6 @@ table.branches>thead>tr>th, table.branches>tbody>tr>td{
color: #4183c4;
}
.branches .branch-name{
color: #4183c4;
display: inline-block;
padding: 2px 6px;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
background-color: rgba(209,227,237,0.5);
border-radius: 3px;
}
.branches .branch-meta{
color: #aaa;
line-height: 20px;

View File

@@ -22,7 +22,7 @@ class JsonFormatSpec extends FunSuite {
}
val sha1 = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
val repo1Name = RepositoryName("octocat/Hello-World")
implicit val context = JsonFormat.Context("http://gitbucket.exmple.com")
implicit val context = JsonFormat.Context("http://gitbucket.exmple.com", None)
val apiUser = ApiUser(
login = "octocat",
@@ -281,7 +281,8 @@ class JsonFormatSpec extends FunSuite {
merged_by = Some(apiUser),
title = "new-feature",
body = "Please pull these awesome changes",
user = apiUser
user = apiUser,
assignee = Some(apiUser)
)
val apiPullRequestJson = s"""{
@@ -311,6 +312,7 @@ class JsonFormatSpec extends FunSuite {
"title": "new-feature",
"body": "Please pull these awesome changes",
"user": $apiUserJson,
"assignee": $apiUserJson,
"html_url": "${context.baseUrl}/octocat/Hello-World/pull/1347",
"url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/pulls/1347",
"commits_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/pulls/1347/commits",

View File

@@ -5,7 +5,7 @@ import org.scalatest.FunSpec
class GitCommandFactorySpec extends FunSpec {
val factory = new GitCommandFactory("http://localhost:8080")
val factory = new GitCommandFactory("http://localhost:8080", None)
describe("createCommand") {
it("should return GitReceivePack when command is git-receive-pack"){

View File

@@ -119,8 +119,7 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
smtp = None,
ldapAuthentication = false,
ldap = None,
skinName = "skin-blue",
debug = false
skinName = "skin-blue"
)
/**