Compare commits

...

123 Commits
2.7 ... 3.0

Author SHA1 Message Date
Naoki Takezoe
6b37967162 Add Version 3.0 to versions 2015-03-03 14:05:13 +09:00
Naoki Takezoe
a03b9584ee Update gitbucket.version to 3.0.0 2015-03-03 13:58:06 +09:00
Naoki Takezoe
34649dfeda Update group id 2015-03-03 11:16:48 +09:00
Naoki Takezoe
bc84cfc2c8 Update assembly jar deploy script for 3.0 release 2015-03-03 11:07:29 +09:00
Naoki Takezoe
bcba1f068b Update README.md for 3.0 release 2015-03-03 10:42:36 +09:00
Naoki Takezoe
e6f30ef86b Fix Profile structure 2015-03-03 10:36:14 +09:00
takezoe
9e67999ef0 Temporary fix instantiation error 2015-03-03 02:42:35 +09:00
takezoe
be75cef752 Fix testcase 2015-03-03 02:38:59 +09:00
Naoki Takezoe
19ead71b48 Bump up version number to 3.0.0 2015-03-02 22:22:52 +09:00
Naoki Takezoe
7ebe1d6c62 Merge branch 'master' into profile_generalization 2015-03-02 22:21:00 +09:00
Naoki Takezoe
2331b58b87 Merge branch 'batch-edit'
Conflicts:
	src/main/twirl/issues/list.scala.html
	src/main/twirl/issues/listparts.scala.html
2015-03-02 22:20:22 +09:00
Naoki Takezoe
d495b04d85 Change package name 2015-03-02 22:15:56 +09:00
shimamoto
751a8703ef (refs #632) Implement all the check of javascript. 2015-03-02 06:31:59 +09:00
shimamoto
9019d93449 (refs #632) Revert. And remove all checked checkbox. 2015-03-02 01:21:11 +09:00
Naoki Takezoe
32006e02c0 Fix build error 2015-03-02 00:45:28 +09:00
Naoki Takezoe
5ba0f6d51e Generalize Profile for plug-ins as ProfileBase 2015-03-02 00:32:59 +09:00
shimamoto
c004d501f6 (refs #632) Implement all the check of javascript. 2015-03-01 23:38:08 +09:00
Naoki Takezoe
6067fa0fca Fix broken layout of Wiki compare view 2015-02-28 12:47:52 +09:00
Naoki Takezoe
e2c6658e59 Fix compilation error 2015-02-28 12:47:06 +09:00
Naoki Takezoe
0a6e50cbbe Compare view supports diff for commit id, not only branch name 2015-02-28 12:43:50 +09:00
Naoki Takezoe
3732963d4b Add image API for plug-in 2015-02-28 03:40:02 +09:00
Naoki Takezoe
20217058fe Merge branch 'feature/new-branches-ui' of https://github.com/team-lab/gitbucket into team-lab-feature/new-branches-ui
Conflicts:
	src/main/scala/service/PullRequestService.scala
2015-02-27 02:05:02 +09:00
Naoki Takezoe
e67365a19f Fix problem about milestone in issues and pull requests 2015-02-26 11:20:41 +09:00
Naoki Takezoe
c2f1817c6a Avoid exception when filter box is empty 2015-02-25 21:42:22 +09:00
Naoki Takezoe
4a948d9b01 Fix monospace style 2015-02-24 00:10:06 +09:00
Shintaro Murakami
377bc2703b (refs #630) Fix bug on changing issues status. 2015-02-23 02:41:56 +09:00
Naoki Takezoe
196890b26f Fix Test 2015-02-20 13:39:47 +09:00
Naoki Takezoe
491fc2c164 Merge branch 'new-plugin-system'
Conflicts:
	src/main/scala/servlet/InitializeListener.scala
2015-02-20 13:32:50 +09:00
Naoki Takezoe
1bed38f175 Change order of plug-in controller registration 2015-02-20 13:08:38 +09:00
Naoki Takezoe
b124c31f65 Plug-in action to be Scalatra controller 2015-02-20 09:29:44 +09:00
Naoki Takezoe
8c588cbd66 Provides Slick Session to plug-ins via ThreadLocal 2015-02-16 13:14:52 +09:00
shimamoto
334753b1ad Remove unnecessary ServletContext. 2015-02-15 21:39:31 +09:00
Naoki Takezoe
f6e7401d1b Add script to deploy assembly jar 2015-02-14 23:56:42 +09:00
Naoki Takezoe
ab564cc2d4 Merge branch 'master' into new-plugin-system
Conflicts:
	src/main/scala/servlet/AutoUpdateListener.scala
2015-02-14 23:40:43 +09:00
Naoki Takezoe
b10839a5c2 Add comment 2015-02-14 23:28:21 +09:00
shimamoto
bb3323fb0e Merge pull request #605 from rlazoti/add-connection-pool
Add Connection Pool
2015-02-14 22:56:00 +09:00
Naoki Takezoe
0b15ecbacd Add pom.xml for the assembly jar 2015-02-11 22:28:29 +09:00
Naoki Takezoe
9f6afaed07 Add Result case classes for plugin 2015-02-10 02:28:22 +09:00
Naoki Takezoe
925420734e Render Html with layout template 2015-02-08 23:51:21 +09:00
Naoki Takezoe
fb6bb12c52 Give Context to plugin actions 2015-02-08 23:09:30 +09:00
Naoki Takezoe
22d12d0488 Use util.Version for GitBucket migration 2015-02-08 22:35:58 +09:00
Naoki Takezoe
6888d959e1 Add migration system for plugins 2015-02-08 22:31:09 +09:00
Naoki Takezoe
65e66f52f6 Merge branch 'marklacroix-sidemenu-tooltips' 2015-02-07 17:58:04 +09:00
Naoki Takezoe
225abfa126 Merge branch 'sidemenu-tooltips' of https://github.com/marklacroix/gitbucket into marklacroix-sidemenu-tooltips 2015-02-07 17:54:29 +09:00
Naoki Takezoe
876757f2d4 Merge pull request #620 from nus/fix-facebox-resources
Fix facebox resource URLs.
2015-02-07 17:51:32 +09:00
Naoki Takezoe
7e77398645 Add prototype of global action support 2015-02-07 13:12:05 +09:00
Naoki Takezoe
73c76a5a88 Add plugin interfaces 2015-02-07 10:03:23 +09:00
Mark LaCroix
e5fca0d6cc Fix blinking tooltips on side menu 2015-02-06 16:48:54 -05:00
Naoki Takezoe
eed7e5177f Merge branch 'master' into new-plugin-system 2015-02-06 22:39:04 +09:00
Yota Ichino
dd54ab31cb Fix facebox resource URLs. 2015-02-05 01:50:07 +09:00
Naoki Takezoe
0085cb24ad Add description about 2.8 2015-02-01 13:00:31 +09:00
Naoki Takezoe
6a758902ef Small fix for #615 2015-02-01 12:55:37 +09:00
Naoki Takezoe
0d81a9a9b6 Merge pull request #615 from team-lab/fix/xss-by-raw-html
fix/xss by raw html
2015-02-01 12:44:15 +09:00
Naoki Takezoe
6e4f6da633 Merge pull request #612 from team-lab/fix/update-pullrequest-on-commit-by-online-editor
fix/update pullrequest when file edited by online editor
2015-01-31 18:19:46 +09:00
Naoki Takezoe
15118ca5c1 Merge pull request #614 from HairyFotr/patch-typo
Fix typo
2015-01-31 13:58:55 +09:00
HairyFotr
8161560757 Fix typo 2015-01-30 21:34:25 +01:00
nazoking
9ba564c864 test/html is cause of xss 2015-01-30 15:32:53 +09:00
nazoking
06b5b92673 update pullrequest commitId on file edited by online editor 2015-01-30 04:14:04 +09:00
nazoking
a417c373f1 'New Pull Request' button if you logined. 2015-01-30 00:55:02 +09:00
nazoking
5f5cc8d454 add action link to pull request. 2015-01-30 00:30:11 +09:00
Naoki Takezoe
b9b6589bd7 Update README.md 2015-01-29 21:47:54 +09:00
Naoki Takezoe
b79f6a5fa0 Update README.md 2015-01-29 21:47:00 +09:00
nazoking
0acbaeae86 new branches ui like GitHub. 2015-01-29 21:38:50 +09:00
Shintaro Murakami
bd046da3d0 (refs #532) Fix rendering of link over image 2015-01-28 00:09:34 +09:00
Naoki Takezoe
a889ed7c46 Merge pull request #591 from marklacroix/anon-access
(refs #274) Add option to deny anonymous (i.e. unauthorized) access
2015-01-27 10:28:11 +09:00
Naoki Takezoe
e24684cb2b Update favicon 2015-01-25 20:01:12 +09:00
Naoki Takezoe
5f939c18b4 (refs #609)Convert labelId when rename repository 2015-01-25 14:45:37 +09:00
takezoe
140f1eb31b Add sbt-assembly configuration 2015-01-25 02:31:21 +09:00
takezoe
d412dd5009 (refs #600)Fix broken layout 2015-01-25 01:16:03 +09:00
Mark LaCroix
8643bfeb37 Merge remote-tracking branch 'upstream/master' into anon-access
Conflicts:
	src/main/scala/app/SystemSettingsController.scala
	src/main/scala/service/SystemSettingsService.scala
	src/test/scala/view/AvatarImageProviderSpec.scala
2015-01-21 15:49:42 -05:00
Naoki Takezoe
31b6adf0e5 Merge pull request #606 from bati11/fix-mergeguide-text
Fix merge guide's text
2015-01-21 01:39:37 +09:00
bati11
f1ac2b3507 Change checkout branch name from "master" to ${pullreq.branch} 2015-01-20 23:32:47 +09:00
Rodrigo Lazoti
172af307a6 add connection pool to Database object 2015-01-20 12:14:28 -02:00
Naoki Takezoe
135e1ef73d Merge pull request #602 from mrkm4ntr/default-privacy-option-to-create-repo
(refs #495,#595) Add configuration to set default visibility option to create new repositories.
2015-01-20 10:59:02 +09:00
Naoki Takezoe
da55bf6af3 Apply new icon 2015-01-18 14:17:41 +09:00
Naoki Takezoe
883a9c8b17 Improve Wiki rendering performance 2015-01-18 14:06:19 +09:00
Naoki Takezoe
7da89940e3 Use the issues list template for the pull request list in the dashboard 2015-01-18 03:59:33 +09:00
Shintaro Murakami
3233b0ae3c Fix failed test. 2015-01-17 23:28:08 +09:00
Shintaro Murakami
4c2ed09915 (refs #495,#595) Add configuration to set default visibility option to create new repositories. 2015-01-17 23:04:41 +09:00
Naoki Takezoe
256b6c480f Merge pull request #546 from rlazoti/add-link-to-dashboard
Add repository's link to issue and pull request list on dashboard
2015-01-17 16:44:04 +09:00
Naoki Takezoe
dc311837f9 Merge pull request #596 from tnayuki/streaming-archive
not to make a temporary file when archive
2015-01-17 16:29:36 +09:00
Naoki Takezoe
92aec48c99 Merge pull request #547 from mslinn/master
Change Bootstrap's default color pink for code tags to match github's color
2015-01-17 16:17:43 +09:00
Naoki Takezoe
a6ada8c457 Merge pull request #582 from team-lab/add-message-to-login
Show information message on singin view.
2015-01-17 14:50:37 +09:00
Shintaro Murakami
dcc601502e (refs #589) Prevent adding event handler several times 2015-01-14 23:04:23 +09:00
Shintaro Murakami
dd58d8c804 (refs #598) Exclude count of pull requests from that of issues. 2015-01-12 22:50:12 +09:00
Shintaro Murakami
2ade54b7e3 (#refs 549) Change "…" at skipped line to pseudo element 2015-01-11 01:04:13 +09:00
Shintaro Murakami
136c5854f3 (refs #593) Expand column size of FILE_NAME in COMIT_COMMENT 2015-01-11 00:28:52 +09:00
Shintaro Murakami
c597238d9c (refs #549) Selecting lines in diff without line numbers. 2015-01-10 01:22:47 +09:00
Toru Nayuki
2552a58e08 not to make a temporary file when archive 2015-01-09 18:39:48 +09:00
nazoking
74ad5872a3 Revert "add information to singup view"
This reverts commit f7fd53bf09.
2015-01-09 14:55:28 +09:00
Shintaro Murakami
485d502bd3 (refs #584) Refactored 2015-01-09 00:16:09 +09:00
Naoki Takezoe
47bc8d030e Merge pull request #590 from ghmer/master
Allow LDAPS connections instead of only allowing TLS enabled connections
2015-01-08 02:44:36 +09:00
Mark LaCroix
48fe7133f7 Add anonymous access option to tests 2015-01-07 09:47:36 -05:00
Mark LaCroix
5d962dc5e4 Add option to deny anonymous (i.e. unauthorized) access 2015-01-07 09:17:22 -05:00
Mario Enrico Ragucci
31e8e5a951 code alignment. We want a pretty pull request! 2015-01-07 07:46:59 +01:00
Mario Enrico Ragucci
858373c628 small beautifying change to have code properly aligned 2015-01-07 07:45:18 +01:00
Mario Enrico Ragucci
7f142d2c0d Introducing "Enable SSL" option on LDAP settings 2015-01-07 07:41:41 +01:00
Shintaro Murakami
08b86232a8 Merge pull request #589 from mrkm4ntr/toggle-line-notes
Add checkbox to toggle inline notes.
2015-01-06 23:48:11 +09:00
Shintaro Murakami
6bf4f42fdb Add checkbox to toggle line notes 2015-01-06 23:27:03 +09:00
Shintaro Murakami
f3c7de36d8 Remove filter setting for old plugin 2015-01-05 22:28:30 +09:00
Naoki Takezoe
19f556de57 Merge pull request #587 from mrkm4ntr/comment-for-split-diff
(refs #564) Comment for side-by-side diff available
2015-01-04 13:46:33 +09:00
Naoki Takezoe
e4467df411 Merge pull request #586 from team-lab/feature/add-stat-icon-on-diff
add icon on each diff header
2015-01-04 13:25:15 +09:00
Shintaro Murakami
8d305a1fb1 Merge branch 'master' of https://github.com/takezoe/gitbucket into comment-for-split-diff 2015-01-04 10:47:51 +09:00
Shintaro Murakami
b47153e645 Remove old plugin test 2015-01-04 10:40:10 +09:00
Shintaro Murakami
c71766c84b (refs #564) Comment for side-by-side diff available 2015-01-03 17:33:33 +09:00
Naoki Takezoe
23e4d679ae Merge branch 'purge-old-plugin-system' 2015-01-03 04:47:10 +09:00
Naoki Takezoe
182acb2e02 Trim each lines of command guidance 2015-01-02 19:11:50 +09:00
nazoking
b255b15006 add icon on diff view 2015-01-02 17:35:53 +09:00
Naoki Takezoe
b458f88161 Remove enable.plugin flag 2015-01-02 02:27:35 +09:00
Naoki Takezoe
398d8f2f1c Merge branch 'master' into purge-old-plugin-system 2015-01-02 02:03:00 +09:00
Naoki Takezoe
85c1a56cbf Purge old plugin system 2015-01-02 01:59:21 +09:00
Shintaro Murakami
da216c6960 (refs #585) Fix issue in markdown preview 2014-12-31 16:24:30 +09:00
Naoki Takezoe
bc91b153bf Merge pull request #574 from michaeljayt/add-fork-options
Add fork to group options
2014-12-31 01:08:27 +09:00
Shintaro Murakami
bc50b47d3a (refs #584) Fix the activity of commenting to pull request. 2014-12-31 00:27:47 +09:00
michaeljayt
aed15a7f25 Skip the group popup when user has no group 2014-12-30 14:26:30 +08:00
michaeljayt
a1f09117b0 Fix security issue on fork 2014-12-30 08:50:19 +08:00
michaeljayt
0a4a4a51ca Add fork to group options 2014-12-30 08:50:19 +08:00
nazoking
f7fd53bf09 add information to singup view 2014-12-29 20:54:22 +09:00
nazoking
cbfb863a54 Add information message to singin view. 2014-12-29 19:56:52 +09:00
Mike Slinn
2848f07b83 Merge remote-tracking branch 'upstream/master' 2014-11-08 04:11:55 -08:00
Mike Slinn
55224ddcd8 Changed Bootstrap's default color pink for code tags to match github's color 2014-11-08 04:07:14 -08:00
Rodrigo Lazoti
054ae75b6b Add repository's link to issues and pull request list on dashboard 2014-11-07 10:55:08 -02:00
Mike Slinn
a10188260c Update README.md 2014-10-03 15:26:42 -07:00
229 changed files with 3046 additions and 2663 deletions

View File

@@ -1,7 +1,7 @@
GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://travis-ci.org/takezoe/gitbucket.svg?branch=master)](https://travis-ci.org/takezoe/gitbucket)
=========
GitBucket is the easily installable Github clone written with Scala.
GitBucket is the easily installable GitHub clone powered by Scala.
Features
@@ -79,6 +79,19 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 3.0 - 3 Mar 2015
- New plug-in system is available
- Connection pooling by c3p0
- New branch UI
- Compare between specified commit ids
### 2.8 - 1 Feb 2015
- New logo and icons
- New system setting options to control visibility
- Comment on side-by-side diff
- Information message on sign-in page
- Fork repository by group account
### 2.7 - 29 Dec 2014
- Comment for commit and diff
- Fix security issue in markdown rendering

View File

@@ -5,7 +5,7 @@
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.11"/>
<property name="gitbucket.version" value="0.0.1"/>
<property name="gitbucket.version" value="3.0.0"/>
<property name="jetty.version" value="8.1.8.v20121106"/>
<property name="servlet.version" value="3.0.0.v201112011016"/>

View File

@@ -8,6 +8,6 @@ Common scripts are in this directory.
This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
To run:
1. Edit `gitbucket.conf` to suit.
2. Type: `install`
1. Edit `gitbucket.conf` to suit.
2. Type: `install`

9
etc/deploy-assemby-jar.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
mvn deploy:deploy-file \
-DgroupId=gitbucket\
-DartifactId=gitbucket-assembly\
-Dversion=3.0.0\
-Dpackaging=jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-3.0.0.jar\
-DrepositoryId=sourceforge.jp\
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/

17
etc/pom.xml Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jp.sf.amateras</groupId>
<artifactId>gitbucket-assembly</artifactId>
<version>0.0.1</version>
<build>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>1.0-beta-6</version>
</extension>
</extensions>
</build>
</project>

View File

@@ -4,11 +4,13 @@ import org.scalatra.sbt._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
import play.twirl.sbt.SbtTwirl
import play.twirl.sbt.Import.TwirlKeys._
import sbtassembly._
import sbtassembly.AssemblyKeys._
object MyBuild extends Build {
val Organization = "jp.sf.amateras"
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "0.0.1"
val Version = "3.0.0"
val ScalaVersion = "2.11.2"
val ScalatraVersion = "2.3.0"
@@ -17,6 +19,17 @@ object MyBuild extends Build {
file(".")
)
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
.settings(
test in assembly := {},
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) =>
(xs map {_.toLowerCase}) match {
case ("manifest.mf" :: Nil) => MergeStrategy.discard
case _ => MergeStrategy.discard
}
case x => MergeStrategy.first
}
)
.settings(
sourcesInBase := false,
organization := Organization,
@@ -44,14 +57,16 @@ object MyBuild extends Build {
"org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.4.180",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.11" % "test",
"com.mchange" % "c3p0" % "0.9.5",
"com.typesafe" % "config" % "1.2.1",
"com.typesafe.play" %% "twirl-compiler" % "1.0.2"
),
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),

View File

@@ -7,3 +7,5 @@ addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")

View File

@@ -1,4 +1,4 @@
package util;
package gitbucket.core.util;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.diff.RawText;

View File

@@ -0,0 +1,6 @@
db {
driver = "org.h2.Driver"
url = "jdbc:h2:${DatabaseHome};MVCC=true"
user = "sa"
password = "sa"
}

View File

@@ -0,0 +1 @@
ALTER TABLE COMMIT_COMMENT ALTER COLUMN FILE_NAME NVARCHAR(260);

View File

@@ -1,5 +1,9 @@
import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
import app._
import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.servlet.{TransactionFilter, BasicAuthenticationFilter}
import gitbucket.core.util.Directory
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
import javax.servlet._
@@ -10,12 +14,16 @@ class ScalatraBootstrap extends LifeCycle {
// Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
// Register controllers
context.mount(new AnonymousAccessController, "/*")
PluginRegistry().getControllers.foreach { case (controller, path) =>
context.mount(controller, path)
}
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
@@ -32,7 +40,7 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new RepositorySettingsController, "/*")
// Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
val dir = new java.io.File(Directory.GitBucketHome)
if(!dir.exists){
dir.mkdirs()
}

View File

@@ -1,202 +0,0 @@
package app
import service.{AccountService, SystemSettingsService}
import SystemSettingsService._
import util.AdminAuthenticator
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import ssh.SshServer
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import plugin.{Plugin, PluginSystem}
import org.scalatra.Ok
import util.Implicits._
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase {
self: AccountService with AdminAuthenticator =>
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"ssh" -> trim(label("SSH access", boolean())),
"sshPort" -> trim(label("SSH port", optional(number()))),
"smtp" -> optionalIfNotChecked("notification", mapping(
"host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply)),
"ldapAuthentication" -> trim(label("LDAP", boolean())),
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
"host" -> trim(label("LDAP host", text(required))),
"port" -> trim(label("LDAP port", optional(number()))),
"bindDN" -> trim(label("Bind DN", optional(text()))),
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
"additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else Nil
}
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
get("/admin/system")(adminOnly {
admin.html.system(flash.get("info"))
})
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
SshServer.stop()
}
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(request.getServletContext,
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
SshServer.stop()
}
flash += "info" -> "System settings has been updated."
redirect("/admin/system")
})
get("/admin/plugins")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins)
} else NotFound
})
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds)
installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
get("/admin/plugins/available")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
admin.plugins.html.available(availablePlugins)
} else NotFound
})
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
get("/admin/plugins/console")(adminOnly {
if(enablePluginSystem){
admin.plugins.html.console()
} else NotFound
})
post("/admin/plugins/console")(adminOnly {
if(enablePluginSystem){
val script = request.getParameter("script")
val result = plugin.ScalaPlugin.eval(script)
Ok()
} else NotFound
})
// TODO Move these methods to PluginSystem or Service?
private def deletePlugins(pluginIds: List[String]): Unit = {
pluginIds.foreach { pluginId =>
plugin.PluginSystem.uninstall(pluginId)
val dir = new java.io.File(PluginHome, pluginId)
if(dir.exists && dir.isDirectory){
FileUtils.deleteQuietly(dir)
PluginSystem.uninstall(pluginId)
}
}
}
private def installPlugins(pluginIds: List[String]): Unit = {
val dir = getPluginCacheDir()
val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id)
if(pluginDir.exists){
FileUtils.deleteDirectory(pluginDir)
}
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
PluginSystem.installPlugin(plugin.id)
}
}
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
val repositoryRoot = getPluginCacheDir()
if(repositoryRoot.exists && repositoryRoot.isDirectory){
PluginSystem.repositories.flatMap { repo =>
val repoDir = new java.io.File(repositoryRoot, repo.id)
if(repoDir.exists && repoDir.isDirectory){
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
val propertyFile = new java.io.File(plugin, "plugin.properties")
val properties = new java.util.Properties()
if(propertyFile.exists && propertyFile.isFile){
using(new FileInputStream(propertyFile)){ in =>
properties.load(in)
}
}
SystemSettingsControllerBase.AvailablePlugin(
repository = repo.id,
id = properties.getProperty("id"),
version = properties.getProperty("version"),
author = properties.getProperty("author"),
url = properties.getProperty("url"),
description = properties.getProperty("description"),
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
case Some(x) => "installed"
case None => "available"
})
}
} else Nil
}
} else Nil
}
}
object SystemSettingsControllerBase {
case class AvailablePlugin(repository: String, id: String, version: String,
author: String, url: String, description: String, status: String)
}

View File

@@ -1,19 +1,22 @@
package app
package gitbucket.core.controller
import service._
import util._
import util.StringUtil._
import util.Directory._
import util.ControlUtil._
import util.Implicits._
import ssh.SshUtil
import gitbucket.core.account.html
import gitbucket.core.helper
import gitbucket.core.model.GroupMember
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.StringUtil._
import gitbucket.core.ssh.SshUtil
import gitbucket.core.service._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import model.GroupMember
class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
@@ -88,6 +91,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
case class AccountForm(accountName: String)
val accountForm = mapping(
"account" -> trim(label("Group/User name", text(required, validAccountName)))
)(AccountForm.apply)
/**
* Displays user information.
*/
@@ -97,21 +106,21 @@ trait AccountControllerBase extends AccountManagementControllerBase {
params.getOrElse("tab", "repositories") match {
// Public Activity
case "activity" =>
_root_.account.html.activity(account,
gitbucket.core.account.html.activity(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getActivitiesByUser(userName, true))
// Members
case "members" if(account.isGroupAccount) => {
val members = getGroupMembers(account.userName)
_root_.account.html.members(account, members.map(_.userName),
gitbucket.core.account.html.members(account, members.map(_.userName),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
// Repositories
case _ => {
val members = getGroupMembers(account.userName)
_root_.account.html.repositories(account,
gitbucket.core.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
@@ -129,8 +138,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_avatar"){
val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image =>
contentType = FileUtil.getMimeType(image)
new java.io.File(getUserUploadDir(userName), image)
RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image))
} getOrElse {
contentType = "image/png"
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
@@ -140,7 +148,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
account.html.edit(x, flash.get("info"))
html.edit(x, flash.get("info"))
} getOrElse NotFound
})
@@ -184,7 +192,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_ssh")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
account.html.ssh(x, getPublicKeys(x.userName))
html.ssh(x, getPublicKeys(x.userName))
} getOrElse NotFound
})
@@ -206,7 +214,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
if(context.loginAccount.isDefined){
redirect("/")
} else {
account.html.register()
html.register()
}
} else NotFound
}
@@ -220,7 +228,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
get("/groups/new")(usersOnly {
account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
})
post("/groups/new", newGroupForm)(usersOnly { form =>
@@ -236,7 +244,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:groupName/_editgroup")(managersOnly {
defining(params("groupName")){ groupName =>
account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
@@ -285,7 +293,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Show the new repository form.
*/
get("/new")(usersOnly {
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic)
})
/**
@@ -354,11 +362,31 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName)
groups match {
case _: List[String] =>
val managerPermissions = groups.map { group =>
val members = getGroupMembers(group)
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
}
helper.html.forkrepository(
repository,
(groups zip managerPermissions).toMap
)
case _ => redirect(s"/${loginUserName}")
}
})
LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}")
redirect(s"/${accountName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
@@ -366,7 +394,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
createRepository(
repositoryName = repository.name,
userName = loginUserName,
userName = accountName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
@@ -376,22 +404,22 @@ trait AccountControllerBase extends AccountManagementControllerBase {
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
insertDefaultLabels(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
getRepositoryDir(accountName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
getWikiRepositoryDir(accountName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
redirect(s"/${accountName}/${repository.name}")
}
}
})
@@ -431,4 +459,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case None => Some("Key is invalid.")
}
}
private def validAccountName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
getAccountByUserName(value) match {
case Some(_) => None
case None => Some("Invalid Group/User Account.")
}
}
}
}

View File

@@ -0,0 +1,14 @@
package gitbucket.core.controller
class AnonymousAccessController extends AnonymousAccessControllerBase
trait AnonymousAccessControllerBase extends ControllerBase {
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register")) {
Unauthorized()
} else {
pass()
}
}
}

View File

@@ -1,16 +1,16 @@
package app
package gitbucket.core.controller
import _root_.util.Directory._
import _root_.util.Implicits._
import _root_.util.ControlUtil._
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
import gitbucket.core.service.{AccountService, SystemSettingsService}
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.model.Account
import org.scalatra._
import org.scalatra.json._
import org.json4s._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import model._
import service.{SystemSettingsService, AccountService}
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
import org.scalatra.i18n._
@@ -104,10 +104,10 @@ abstract class ControllerBase extends ScalatraFilter
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound()
} else {
org.scalatra.NotFound(html.error("Not Found"))
org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
}
protected def Unauthorized()(implicit context: app.Context) =
protected def Unauthorized()(implicit context: Context) =
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.Unauthorized()
} else {
@@ -134,6 +134,18 @@ abstract class ControllerBase extends ScalatraFilter
if (path.startsWith("http")) path
else baseUrl + super.url(path, params, false, false, false)
/**
* Use this method to response the raw data against XSS.
*/
protected def RawData[T](contentType: String, rawData: T): T = {
if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){
this.contentType = "text/plain"
} else {
this.contentType = contentType
}
response.addHeader("X-Content-Type-Options", "nosniff")
rawData
}
}
/**

View File

@@ -1,9 +1,10 @@
package app
package gitbucket.core.controller
import service._
import util.{StringUtil, UsersAuthenticator, Keys}
import util.Implicits._
import service.IssuesService.IssueSearchCondition
import gitbucket.core.dashboard.html
import gitbucket.core.service.{RepositoryService, PullRequestService, AccountService, IssuesService}
import gitbucket.core.util.{StringUtil, Keys, UsersAuthenticator}
import gitbucket.core.util.Implicits._
import gitbucket.core.service.IssuesService._
class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService
@@ -96,7 +97,7 @@ trait DashboardControllerBase extends ControllerBase {
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request)
dashboard.html.issues(
html.issues(
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page,
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
@@ -119,7 +120,7 @@ trait DashboardControllerBase extends ControllerBase {
val allRepos = getAllRepositories(userName)
val page = IssueSearchCondition.page(request)
dashboard.html.pulls(
html.pulls(
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page,
countIssue(condition.copy(state = "open" ), true, allRepos: _*),

View File

@@ -1,8 +1,8 @@
package app
package gitbucket.core.controller
import util.{Keys, FileUtil}
import util.ControlUtil._
import util.Directory._
import gitbucket.core.util.{Keys, FileUtil}
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
import org.apache.commons.io.FileUtils

View File

@@ -1,8 +1,11 @@
package app
package gitbucket.core.controller
import util._
import util.Implicits._
import service._
import gitbucket.core.html
import gitbucket.core.helper.xml
import gitbucket.core.model.Account
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService}
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
@@ -61,13 +64,13 @@ trait IndexControllerBase extends ControllerBase {
get("/activities.atom"){
contentType = "application/atom+xml; type=feed"
helper.xml.feed(getRecentActivities())
xml.feed(getRecentActivities())
}
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: model.Account) = {
private def signin(account: Account) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)

View File

@@ -1,14 +1,17 @@
package app
package gitbucket.core.controller
import gitbucket.core.issues.html
import gitbucket.core.model.Issue
import gitbucket.core.service._
import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import jp.sf.amateras.scalatra.forms._
import service._
import IssuesService._
import util._
import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok
import model.Issue
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -60,7 +63,7 @@ trait IssuesControllerBase extends ControllerBase {
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
getIssue(owner, name, issueId) map {
issues.html.issue(
html.issue(
_,
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
@@ -75,7 +78,7 @@ trait IssuesControllerBase extends ControllerBase {
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create(
html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestones(owner, name),
getLabels(owner, name),
@@ -192,13 +195,13 @@ trait IssuesControllerBase extends ControllerBase {
getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect {
case t if t == "html" => issues.html.editissue(
case t if t == "html" => html.editissue(
x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
"content" -> Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
))
}
@@ -210,7 +213,7 @@ trait IssuesControllerBase extends ControllerBase {
getComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect {
case t if t == "html" => issues.html.editcomment(
case t if t == "html" => html.editcomment(
x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
@@ -226,14 +229,14 @@ trait IssuesControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}
})
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}
})
@@ -247,7 +250,7 @@ trait IssuesControllerBase extends ControllerBase {
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount)
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound
} getOrElse Ok()
})
@@ -292,8 +295,7 @@ trait IssuesControllerBase extends ControllerBase {
(Directory.getAttachedDir(repository.owner, repository.name) match {
case dir if(dir.exists && dir.isDirectory) =>
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
contentType = FileUtil.getMimeType(file.getName)
file
RawData(FileUtil.getMimeType(file.getName), file)
}
case _ => None
}) getOrElse NotFound
@@ -302,7 +304,7 @@ trait IssuesControllerBase extends ControllerBase {
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
@@ -326,13 +328,13 @@ trait IssuesControllerBase extends ControllerBase {
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] =
(getAction: Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
defining(repository.owner, repository.name){ case (owner, name) =>
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue =>
getIssue(owner, name, issueId.toString) flatMap { issue =>
val (action, recordActivity) =
getAction(issue)
.collect {
@@ -347,11 +349,10 @@ trait IssuesControllerBase extends ControllerBase {
}
.getOrElse(None -> None)
val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get )
match {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
val commentId = (content, action) match {
case (None, None) => None
case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action))
case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment")))
}
// record comment activity if comment is entered
@@ -372,7 +373,7 @@ trait IssuesControllerBase extends ControllerBase {
content foreach {
f.toNotify(repository, issueId, _){
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
}
}
action foreach {
@@ -382,7 +383,7 @@ trait IssuesControllerBase extends ControllerBase {
}
}
issue -> commentId
commentId.map( issue -> _ )
}
}
}
@@ -396,7 +397,7 @@ trait IssuesControllerBase extends ControllerBase {
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null){
if(q == null || q.trim.isEmpty){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
@@ -404,7 +405,7 @@ trait IssuesControllerBase extends ControllerBase {
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(
html.list(
"issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,

View File

@@ -1,9 +1,10 @@
package app
package gitbucket.core.controller
import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
import service._
import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import util.Implicits._
import org.scalatra.i18n.Messages
import org.scalatra.Ok
@@ -23,7 +24,7 @@ trait LabelsControllerBase extends ControllerBase {
)(LabelForm.apply)
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
issues.labels.html.list(
html.list(
getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
@@ -31,12 +32,12 @@ trait LabelsControllerBase extends ControllerBase {
})
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
issues.labels.html.edit(None, repository)
html.edit(None, repository)
})
ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
issues.labels.html.label(
html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
@@ -46,13 +47,13 @@ trait LabelsControllerBase extends ControllerBase {
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
issues.labels.html.edit(Some(label), repository)
html.edit(Some(label), repository)
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
issues.labels.html.label(
html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),

View File

@@ -1,11 +1,11 @@
package app
package gitbucket.core.controller
import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
import util.Implicits._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
@@ -23,7 +23,7 @@ trait MilestonesControllerBase extends ControllerBase {
)(MilestoneForm.apply)
get("/:owner/:repository/issues/milestones")(referrersOnly { repository =>
issues.milestones.html.list(
html.list(
params.getOrElse("state", "open"),
getMilestonesWithIssueCount(repository.owner, repository.name),
repository,
@@ -31,7 +31,7 @@ trait MilestonesControllerBase extends ControllerBase {
})
get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly {
issues.milestones.html.edit(None, _)
html.edit(None, _)
})
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
@@ -41,7 +41,7 @@ trait MilestonesControllerBase extends ControllerBase {
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
params("milestoneId").toIntOpt.map{ milestoneId =>
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
} getOrElse NotFound
})

View File

@@ -1,23 +1,25 @@
package app
package gitbucket.core.controller
import util._
import util.Directory._
import util.Implicits._
import util.ControlUtil._
import service._
import gitbucket.core.pulls.html
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.view
import gitbucket.core.view.helpers
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.transport.RefSpec
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._
import service.PullRequestService._
import gitbucket.core.service._
import gitbucket.core.service.IssuesService._
import gitbucket.core.service.PullRequestService._
import gitbucket.core.service.WebHookService.WebHookPayload
import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload
import util.JGitUtil.DiffInfo
import util.JGitUtil.CommitInfo
class PullRequestsController extends PullRequestsControllerBase
@@ -77,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
pulls.html.pullreq(
html.pullreq(
issue, pullreq,
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
.sortWith((a, b) => a.registeredDate before b.registeredDate),
@@ -99,7 +101,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
pulls.html.mergeguide(
html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
pullreq,
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
@@ -242,8 +244,8 @@ trait PullRequestsControllerBase extends ControllerBase {
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
@@ -259,21 +261,24 @@ trait PullRequestsControllerBase extends ControllerBase {
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val (oldId, newId) =
if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
// Branch name
val rootId = JGitUtil.getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originId,
forkedRepository.owner, forkedRepository.name, forkedId)
val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
(oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId))
} else {
// Commit id
(oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId))
}
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner, originRepository.name, oldId.getName,
forkedRepository.owner, forkedRepository.name, newId.getName)
pulls.html.compare(
html.compare(
commits,
diffs,
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
@@ -281,8 +286,8 @@ trait PullRequestsControllerBase extends ControllerBase {
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
originBranch,
forkedBranch,
originId,
forkedId,
oldId.getName,
newId.getName,
forkedRepository,
@@ -315,7 +320,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
html.mergecheck(
checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch))
}
@@ -447,7 +452,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit)
}.toList.splitWith { (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
@@ -466,7 +471,7 @@ trait PullRequestsControllerBase extends ControllerBase {
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(
gitbucket.core.issues.html.list(
"pulls",
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page,

View File

@@ -1,15 +1,17 @@
package app
package gitbucket.core.controller
import service._
import util.Directory._
import util.Implicits._
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
import gitbucket.core.settings.html
import gitbucket.core.model.WebHook
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
import gitbucket.core.service.WebHookService.WebHookPayload
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
import util.ControlUtil._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
@@ -63,7 +65,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the Options page.
*/
get("/:owner/:repository/settings/options")(ownerOnly {
settings.html.options(_, flash.get("info"))
html.options(_, flash.get("info"))
})
/**
@@ -105,7 +107,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the Collaborators page.
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
settings.html.collaborators(
html.collaborators(
getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
repository)
@@ -135,7 +137,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook page.
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
})
/**
@@ -167,7 +169,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook(repository.owner, repository.name,
List(model.WebHook(repository.owner, repository.name, form.url)),
List(WebHook(repository.owner, repository.name, form.url)),
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
)
}
@@ -181,7 +183,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the danger zone.
*/
get("/:owner/:repository/settings/danger")(ownerOnly {
settings.html.danger(_)
html.danger(_)
})
/**

View File

@@ -1,13 +1,18 @@
package app
package gitbucket.core.controller
import _root_.util.JGitUtil.CommitInfo
import util.Directory._
import util.Implicits._
import _root_.util.ControlUtil._
import _root_.util._
import service._
import gitbucket.core.repo.html
import gitbucket.core.helper
import gitbucket.core.service._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.Account
import gitbucket.core.service.WebHookService.WebHookPayload
import gitbucket.core.view
import gitbucket.core.view.helpers
import org.scalatra._
import java.io.File
import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
@@ -17,11 +22,10 @@ import org.eclipse.jgit.treewalk._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.revwalk.RevCommit
import service.WebHookService.WebHookPayload
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService
/**
@@ -29,7 +33,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
*/
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
@@ -57,7 +61,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
oldLineNumber: Option[Int],
newLineNumber: Option[Int],
content: String,
pullRequest: Boolean
issueId: Option[Int]
)
val editorForm = mapping(
@@ -83,7 +87,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"oldLineNumber" -> trim(label("Old line number", optional(number()))),
"newLineNumber" -> trim(label("New line number", optional(number()))),
"content" -> trim(label("Content", text(required))),
"pullRequest" -> trim(label("In pull request", boolean()))
"issueId" -> trim(label("Issue Id", optional(number())))
)(CommentForm.apply)
/**
@@ -91,7 +95,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/
post("/:owner/:repository/_preview")(referrersOnly { repository =>
contentType = "text/html"
view.helpers.markdown(params("content"), repository,
helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean,
params("enableTaskList").toBoolean,
@@ -127,7 +131,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
@@ -138,7 +142,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
})
@@ -150,7 +154,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
@@ -163,7 +167,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
@@ -214,11 +218,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
RawData(FileUtil.getContentType(path, bytes), bytes)
}
} else {
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
} getOrElse NotFound
@@ -234,7 +237,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
getCommitComments(repository.owner, repository.name, id, false),
@@ -247,20 +250,23 @@ trait RepositoryViewerControllerBase extends ControllerBase {
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
form.fileName, form.oldLineNumber, form.newLineNumber, form.pullRequest)
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
})
ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository =>
val id = params("id")
val fileName = params.get("fileName")
val oldLineNumber = params.get("oldLineNumber") flatMap {b => Some(b.toInt)}
val newLineNumber = params.get("newLineNumber") flatMap {b => Some(b.toInt)}
val pullRequest = params.get("pullRequest")
repo.html.commentform(
val oldLineNumber = params.get("oldLineNumber") map (_.toInt)
val newLineNumber = params.get("newLineNumber") map (_.toInt)
val issueId = params.get("issueId") map (_.toInt)
html.commentform(
commitId = id,
fileName, oldLineNumber, newLineNumber, pullRequest.map(_.toBoolean).getOrElse(false),
fileName, oldLineNumber, newLineNumber, issueId,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
repository = repository
)
@@ -269,8 +275,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.pullRequest)
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
@@ -279,7 +288,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getCommitComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect {
case t if t == "html" => repo.html.editcomment(
case t if t == "html" => html.editcomment(
x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
@@ -317,14 +326,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays branches.
*/
get("/:owner/:repository/branches")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// retrieve latest update date of each branch
val branchInfo = repository.branchList.map { branchName =>
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
(branchName, revCommit.getCommitterIdent.getWhen)
}
repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
}
val branches = JGitUtil.getBranches(repository.owner, repository.name, repository.repository.defaultBranch)
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
.reverse
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
/**
@@ -364,7 +370,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays tags.
*/
get("/:owner/:repository/tags")(referrersOnly {
repo.html.tags(_)
html.tags(_)
})
/**
@@ -381,7 +387,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
})
get("/:owner/:repository/network/members")(referrersOnly { repository =>
repo.html.forked(
html.forked(
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name),
@@ -392,7 +398,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repository)
})
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
private def splitPath(repository: RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst {
@@ -415,7 +421,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
if(repository.commitCount == 0){
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} else {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// get specified commit
@@ -434,8 +440,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
repo.html.files(revision, repository,
html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
context.loginAccount match {
case None => List()
case account: Option[Account] => getGroupsByUserName(account.get.userName)
}, // groups of current user
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
flash.get("info"), flash.get("error"))
@@ -445,7 +455,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
private def commitFile(repository: service.RepositoryService.RepositoryInfo,
private def commitFile(repository: RepositoryService.RepositoryInfo,
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
content: String, charset: String, message: String) = {
@@ -486,6 +496,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// update pull request
updatePullRequests(repository.owner, repository.name, branch)
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
@@ -522,7 +535,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
val revision = name.stripSuffix(suffix)
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists) {
@@ -530,24 +543,26 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
workDir.mkdirs
val file = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + 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))
using(new java.io.FileOutputStream(file)) { out =>
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(out)
.call()
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
file
response.setHeader("Content-Disposition", s"attachment; filename=${filename}")
response.setBufferSize(1024 * 1024);
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(response.getOutputStream)
.call()
Unit
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
}

View File

@@ -1,9 +1,10 @@
package app
package gitbucket.core.controller
import util._
import gitbucket.core.search.html
import gitbucket.core.service._
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
import ControlUtil._
import Implicits._
import service._
import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase
@@ -34,12 +35,12 @@ trait SearchControllerBase extends ControllerBase { self: RepositoryService
}
target.toLowerCase match {
case "issue" => search.html.issues(
case "issue" => html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => search.html.code(
case _ => html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)

View File

@@ -0,0 +1,85 @@
package gitbucket.core.controller
import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, SystemSettingsService}
import gitbucket.core.util.AdminAuthenticator
import gitbucket.core.ssh.SshServer
import SystemSettingsService._
import jp.sf.amateras.scalatra.forms._
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase {
self: AccountService with AdminAuthenticator =>
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"ssh" -> trim(label("SSH access", boolean())),
"sshPort" -> trim(label("SSH port", optional(number()))),
"smtp" -> optionalIfNotChecked("notification", mapping(
"host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply)),
"ldapAuthentication" -> trim(label("LDAP", boolean())),
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
"host" -> trim(label("LDAP host", text(required))),
"port" -> trim(label("LDAP port", optional(number()))),
"bindDN" -> trim(label("Bind DN", optional(text()))),
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
"additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else Nil
}
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
get("/admin/system")(adminOnly {
html.system(flash.get("info"))
})
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
SshServer.stop()
}
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
SshServer.stop()
}
flash += "info" -> "System settings has been updated."
redirect("/admin/system")
})
}

View File

@@ -1,11 +1,12 @@
package app
package gitbucket.core.controller
import service._
import util.AdminAuthenticator
import util.StringUtil._
import util.ControlUtil._
import util.Directory._
import util.Implicits._
import gitbucket.core.service.{RepositoryService, AccountService}
import gitbucket.core.admin.users.html
import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils
@@ -75,11 +76,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap
admin.users.html.list(users, members, includeRemoved)
html.list(users, members, includeRemoved)
})
get("/admin/users/_newuser")(adminOnly {
admin.users.html.user(None)
html.user(None)
})
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
@@ -90,7 +91,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
admin.users.html.user(getAccountByUserName(userName, true))
html.user(getAccountByUserName(userName, true))
})
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
@@ -124,7 +125,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
})
get("/admin/users/_newgroup")(adminOnly {
admin.users.html.group(None, Nil)
html.group(None, Nil)
})
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
@@ -140,7 +141,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
get("/admin/users/:groupName/_editgroup")(adminOnly {
defining(params("groupName")){ groupName =>
admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})

View File

@@ -1,14 +1,14 @@
package app
package gitbucket.core.controller
import service._
import util._
import util.Directory._
import util.ControlUtil._
import util.Implicits._
import gitbucket.core.wiki.html
import gitbucket.core.service.{RepositoryService, WikiService, ActivityService, AccountService}
import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages
import java.util.ResourceBundle
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
@@ -36,7 +36,7 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page =>
wiki.html.page("Home", page, getWikiPageList(repository.owner, repository.name),
html.page("Home", page, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
})
@@ -45,7 +45,7 @@ trait WikiControllerBase extends ControllerBase {
val pageName = StringUtil.urlDecode(params("page"))
getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
})
@@ -55,7 +55,7 @@ trait WikiControllerBase extends ControllerBase {
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository)
case Left(_) => NotFound
}
}
@@ -66,7 +66,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
})
@@ -75,7 +75,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
})
@@ -105,7 +105,7 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
})
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
@@ -120,7 +120,7 @@ trait WikiControllerBase extends ControllerBase {
})
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
wiki.html.edit("", None, _)
html.edit("", None, _)
})
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
@@ -147,14 +147,14 @@ trait WikiControllerBase extends ControllerBase {
})
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
wiki.html.pages(getWikiPageList(repository.owner, repository.name), repository,
html.pages(getWikiPageList(repository.owner, repository.name), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
case Right((logs, hasNext)) => html.history(None, logs, repository)
case Left(_) => NotFound
}
}
@@ -164,8 +164,7 @@ trait WikiControllerBase extends ControllerBase {
val path = multiParams("splat").head
getFileContent(repository.owner, repository.name, path).map { bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
RawData(FileUtil.getContentType(path, bytes), bytes)
} getOrElse NotFound
})

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait AccountComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait ActivityComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
protected[model] trait TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait CollaboratorComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait Comment {
val commentedUserName: String

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait GroupMemberComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait IssueComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait IssueLabelComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait LabelComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait MilestoneComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait PluginComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,25 +1,37 @@
package model
package gitbucket.core.model
trait Profile {
val profile: slick.driver.JdbcProfile
import profile.simple._
// java.util.Date Mapped Column Types
/**
* java.util.Date Mapped Column Types
*/
implicit val dateColumnType = MappedColumnType.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime),
t => new java.util.Date(t.getTime)
d => new java.sql.Timestamp(d.getTime),
t => new java.util.Date(t.getTime)
)
/**
* Extends Column to add conditional condition
*/
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
}
object Profile extends {
trait ProfileProvider { self: Profile =>
val profile = slick.driver.H2Driver
}
} with AccountComponent
trait CoreProfile extends ProfileProvider with Profile
with AccountComponent
with ActivityComponent
with CollaboratorComponent
with CommitCommentComponent
@@ -33,11 +45,6 @@ object Profile extends {
with RepositoryComponent
with SshKeyComponent
with WebHookComponent
with PluginComponent with Profile {
with PluginComponent
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
}
object Profile extends CoreProfile

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait PullRequestComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait RepositoryComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait SshKeyComponent { self: Profile =>
import profile.simple._

View File

@@ -1,4 +1,4 @@
package model
package gitbucket.core.model
trait WebHookComponent extends TemplateComponent { self: Profile =>
import profile.simple._

View File

@@ -1,3 +1,5 @@
package gitbucket.core
package object model {
type Session = slick.jdbc.JdbcBackend#Session
}

View File

@@ -0,0 +1,10 @@
package gitbucket.core.plugin
/**
* Provides a helper method to generate data URI of images registered by plug-in.
*/
object Images {
def dataURI(id: String) = s"data:image/png;base64,${PluginRegistry().getImage(id)}"
}

View File

@@ -0,0 +1,28 @@
package gitbucket.core.plugin
import gitbucket.core.util.Version
/**
* Trait for define plugin interface.
* To provide plugin, put Plugin class which mixed in this trait into the package root.
*/
trait Plugin {
val pluginId: String
val pluginName: String
val description: String
val versions: Seq[Version]
/**
* This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry.
*/
def initialize(registry: PluginRegistry): Unit
/**
* This method is invoked in shutdown of plugin system.
* If the plugin has any resources, release them in this method.
*/
def shutdown(registry: PluginRegistry): Unit
}

View File

@@ -0,0 +1,161 @@
package gitbucket.core.plugin
import java.io.{File, FilenameFilter, InputStream}
import java.net.URLClassLoader
import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JDBCUtil._
import gitbucket.core.util.{Version, Versions}
import org.apache.commons.codec.binary.{Base64, StringUtils}
import org.slf4j.LoggerFactory
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
class PluginRegistry {
private val plugins = new ListBuffer[PluginInfo]
private val javaScripts = new ListBuffer[(String, String)]
private val controllers = new ListBuffer[(ControllerBase, String)]
private val images = mutable.Map[String, String]()
def addPlugin(pluginInfo: PluginInfo): Unit = {
plugins += pluginInfo
}
def getPlugins(): List[PluginInfo] = plugins.toList
def addImage(id: String, in: InputStream): Unit = {
val bytes = using(in){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false))
images += ((id, encoded))
}
def getImage(id: String): String = images(id)
def addController(controller: ControllerBase, path: String): Unit = {
controllers += ((controller, path))
}
def getControllers(): List[(ControllerBase, String)] = controllers.toList
def addJavaScript(path: String, script: String): Unit = {
javaScripts += Tuple2(path, script)
}
//def getJavaScripts(): List[(String, String)] = javaScripts.toList
def getJavaScript(currentPath: String): Option[String] = {
javaScripts.find(x => currentPath.matches(x._1)).map(_._2)
}
private case class GlobalAction(
method: String,
path: String,
function: (HttpServletRequest, HttpServletResponse, Context) => Any
)
private case class RepositoryAction(
method: String,
path: String,
function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any
)
}
/**
* Provides entry point to PluginRegistry.
*/
object PluginRegistry {
private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
private val instance = new PluginRegistry()
/**
* Returns the PluginRegistry singleton instance.
*/
def apply(): PluginRegistry = instance
/**
* Initializes all installed plugins.
*/
def initialize(context: ServletContext, conn: java.sql.Connection): Unit = {
val pluginDir = new File(PluginHome)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
}).foreach { pluginJar =>
val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
try {
val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin]
// Migration
val headVersion = plugin.versions.head
val currentVersion = conn.find("SELECT * FROM PLUGIN WHERE PLUGIN_ID = ?", plugin.pluginId)(_.getString("VERSION")) match {
case Some(x) => {
val dim = x.split("\\.")
Version(dim(0).toInt, dim(1).toInt)
}
case None => Version(0, 0)
}
Versions.update(conn, headVersion, currentVersion, plugin.versions, new URLClassLoader(Array(pluginJar.toURI.toURL))){ conn =>
currentVersion.versionString match {
case "0.0" =>
conn.update("INSERT INTO PLUGIN (PLUGIN_ID, VERSION) VALUES (?, ?)", plugin.pluginId, headVersion.versionString)
case _ =>
conn.update("UPDATE PLUGIN SET VERSION = ? WHERE PLUGIN_ID = ?", headVersion.versionString, plugin.pluginId)
}
}
// Initialize
plugin.initialize(instance)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
version = plugin.versions.head.versionString,
description = plugin.description,
pluginClass = plugin
))
} catch {
case e: Exception => {
logger.error(s"Error during plugin initialization", e)
}
}
}
}
}
def shutdown(context: ServletContext): Unit = {
instance.getPlugins().foreach { pluginInfo =>
try {
pluginInfo.pluginClass.shutdown(instance)
} catch {
case e: Exception => {
logger.error(s"Error during plugin shutdown", e)
}
}
}
}
}
case class PluginInfo(
pluginId: String,
pluginName: String,
version: String,
description: String,
pluginClass: Plugin
)

View File

@@ -0,0 +1,11 @@
package gitbucket.core.plugin
import play.twirl.api.Html
/**
* Defines result case classes returned by plugin controller.
*/
object Results {
case class Redirect(path: String)
case class Fragment(html: Html)
}

View File

@@ -0,0 +1,11 @@
package gitbucket.core.plugin
import scala.slick.jdbc.JdbcBackend.Session
/**
* Provides Slick Session to Plug-ins.
*/
object Sessions {
val sessions = new ThreadLocal[Session]
implicit def session: Session = sessions.get()
}

View File

@@ -1,14 +1,14 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.{GroupMember, Account}
import gitbucket.core.model.Profile._
import gitbucket.core.util.{StringUtil, LDAPUtil}
import gitbucket.core.service.SystemSettingsService.SystemSettings
import profile.simple._
import model.{Account, GroupMember}
// TODO [Slick 2.0]NOT import directly?
import model.Profile.dateColumnType
import service.SystemSettingsService.SystemSettings
import util.StringUtil._
import util.LDAPUtil
import StringUtil._
import org.slf4j.LoggerFactory
// TODO Why is direct import required?
import gitbucket.core.model.Profile.dateColumnType
trait AccountService {

View File

@@ -1,8 +1,9 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.Activity
import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil
import profile.simple._
import model.Activity
trait ActivityService {
@@ -121,7 +122,7 @@ trait ActivityService {
currentDate)
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
branchName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
branchName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"push",
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
@@ -129,7 +130,7 @@ trait ActivityService {
currentDate)
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"create_tag",
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
@@ -137,7 +138,7 @@ trait ActivityService {
currentDate)
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
@@ -160,10 +161,10 @@ trait ActivityService {
None,
currentDate)
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String)(implicit s: Session): Unit =
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
None,
currentDate)

View File

@@ -1,13 +1,14 @@
package service
package gitbucket.core.service
import gitbucket.core.model.CommitComment
import gitbucket.core.util.{StringUtil, Implicits}
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import model.Profile._
import gitbucket.core.model.Profile._
import profile.simple._
import model.CommitComment
import util.Implicits._
import util.StringUtil._
import Implicits._
import StringUtil._
trait CommitsService {

View File

@@ -1,13 +1,13 @@
package service
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import model.Profile._
import gitbucket.core.model.Profile._
import profile.simple._
import model.{Issue, IssueComment, IssueLabel, Label}
import util.Implicits._
import util.StringUtil._
trait IssuesService {
import IssuesService._
@@ -139,11 +139,16 @@ trait IssuesService {
.map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
//(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestone == Some(None)) &&
(t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) &&
// Milestone filter
(Milestones filter { t2 =>
(t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) &&
(t2.title === condition.milestone.get.get.bind)
} exists, condition.milestone.flatten.isDefined) &&
// Label filter
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
@@ -322,7 +327,7 @@ object IssuesService {
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None,
milestone: Option[Option[String]] = None,
author: Option[String] = None,
assigned: Option[String] = None,
mentioned: Option[String] = None,
@@ -333,7 +338,7 @@ object IssuesService {
groups: Set[String] = Set.empty){
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
labels.isEmpty && milestone.isEmpty && author.isEmpty && assigned.isEmpty &&
state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty
}
@@ -348,8 +353,8 @@ object IssuesService {
).flatten ++
labels.map(label => s"label:${label}") ++
List(
milestoneId.map { _ match {
case Some(x) => s"milestone:${milestoneId}"
milestone.map { _ match {
case Some(x) => s"milestone:${x}"
case None => "no:milestone"
}},
(sort, direction) match {
@@ -368,8 +373,8 @@ object IssuesService {
def toURL: String =
"?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
milestoneId.map { _ match {
case Some(x) => "milestone=" + x
milestone.map { _ match {
case Some(x) => "milestone=" + urlEncode(x)
case None => "milestone=none"
}},
author .map(x => "author=" + urlEncode(x)),
@@ -416,7 +421,7 @@ object IssuesService {
conditions.get("milestone").flatMap(_.headOption) match {
case None => None
case Some("none") => Some(None)
case Some(x) => milestones.get(x).map(x => Some(x))
case Some(x) => Some(Some(x)) //milestones.get(x).map(x => Some(x))
},
conditions.get("author").flatMap(_.headOption),
conditions.get("assignee").flatMap(_.headOption),
@@ -437,7 +442,7 @@ object IssuesService {
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map {
case "none" => None
case x => x.toIntOpt
case x => Some(x)
},
param(request, "author"),
param(request, "assigned"),

View File

@@ -1,8 +1,8 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.Label
import gitbucket.core.model.Profile._
import profile.simple._
import model.Label
trait LabelsService {

View File

@@ -1,10 +1,10 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.Milestone
import gitbucket.core.model.Profile._
import profile.simple._
import model.Milestone
// TODO [Slick 2.0]NOT import directly?
import model.Profile.dateColumnType
// TODO Why is direct import required?
import gitbucket.core.model.Profile.dateColumnType
trait MilestonesService {

View File

@@ -1,8 +1,8 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.Plugin
import gitbucket.core.model.Profile._
import profile.simple._
import model.Plugin
trait PluginService {

View File

@@ -1,8 +1,9 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.{Issue, PullRequest}
import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil
import profile.simple._
import model.{PullRequest, Issue}
trait PullRequestService { self: IssuesService =>
import PullRequestService._
@@ -81,6 +82,38 @@ trait PullRequestService { self: IssuesService =>
.map { case (t1, t2) => t1 }
.list
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){
val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest(
pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.issueId,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
}
}
def getPullRequestByRequestCommit(userName: String, repositoryName: String, toBranch:String, fromBranch: String, commitId: String)
(implicit s: Session): Option[(PullRequest, Issue)] = {
if(toBranch == fromBranch){
None
} else {
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t1.userName === userName.bind) &&
(t1.repositoryName === repositoryName.bind) &&
(t1.branch === toBranch.bind) &&
(t1.requestUserName === userName.bind) &&
(t1.requestRepositoryName === repositoryName.bind) &&
(t1.requestBranch === fromBranch.bind) &&
(t1.commitIdTo === commitId.bind)
}
.firstOption
}
}
}
object PullRequestService {

View File

@@ -1,13 +1,15 @@
package service
package gitbucket.core.service
import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._
import util.ControlUtil._
import gitbucket.core.model.Issue
import gitbucket.core.util._
import gitbucket.core.util.StringUtil
import Directory._
import ControlUtil._
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git
import model.Profile._
import gitbucket.core.model.Profile._
import profile.simple._
trait RepositorySearchService { self: IssuesService =>
@@ -108,7 +110,7 @@ object RepositorySearchService {
case class SearchResult(
files : List[(String, String)],
issues: List[(model.Issue, Int, String)])
issues: List[(Issue, Int, String)])
case class IssueSearchResult(
issueId: Int,

View File

@@ -1,9 +1,9 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.{Collaborator, Repository, Account}
import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil
import profile.simple._
import model.{Repository, Account, Collaborator}
import util.JGitUtil
trait RepositoryService { self: AccountService =>
import RepositoryService._
@@ -94,9 +94,17 @@ trait RepositoryService { self: AccountService =>
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Convert labelId
val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap
val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap
IssueLabels.insertAll(issueLabels.map(x => x.copy(
labelId = newLabelMap(oldLabelMap(x.labelId)),
userName = newUserName,
repositoryName = newRepositoryName
)) :_*)
if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
@@ -189,7 +197,7 @@ trait RepositoryService { self: AccountService =>
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
issues.size,
issues.count(_ == false),
issues.count(_ == true),
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
@@ -367,7 +375,7 @@ object RepositoryService {
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]){
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)

View File

@@ -1,7 +1,9 @@
package service
package gitbucket.core.service
import model.{Account, Issue, Session}
import util.Implicits.request2Session
import gitbucket.core.model.{Session, Issue, Account}
import gitbucket.core.util.Implicits
import gitbucket.core.controller.Context
import Implicits.request2Session
/**
* This service is used for a view helper mainly.
@@ -11,25 +13,22 @@ import util.Implicits.request2Session
*/
trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
private implicit def context2Session(implicit context: app.Context): Session =
private implicit def context2Session(implicit context: Context): Session =
request2Session(context.request)
def getIssue(userName: String, repositoryName: String, issueId: String)
(implicit context: app.Context): Option[Issue] = {
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
super.getIssue(userName, repositoryName, issueId)
}
}
def getAccountByUserName(userName: String)
(implicit context: app.Context): Option[Account] = {
def getAccountByUserName(userName: String)(implicit context: Context): Option[Account] = {
context.cache(s"account.${userName}"){
super.getAccountByUserName(userName)
}
}
def getAccountByMailAddress(mailAddress: String)
(implicit context: app.Context): Option[Account] = {
def getAccountByMailAddress(mailAddress: String)(implicit context: Context): Option[Account] = {
context.cache(s"account.${mailAddress}"){
super.getAccountByMailAddress(mailAddress)
}

View File

@@ -1,8 +1,8 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.SshKey
import gitbucket.core.model.Profile._
import profile.simple._
import model.SshKey
trait SshKeyService {

View File

@@ -1,7 +1,8 @@
package service
package gitbucket.core.service
import util.Directory._
import util.ControlUtil._
import gitbucket.core.util.{Directory, ControlUtil}
import Directory._
import ControlUtil._
import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
@@ -14,6 +15,8 @@ trait SystemSettingsService {
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
settings.information.foreach(x => props.setProperty(Information, x))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
props.setProperty(Ssh, settings.ssh.toString)
@@ -42,6 +45,7 @@ trait SystemSettingsService {
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x))
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
ldap.ssl.foreach(x => props.setProperty(LdapSsl, x.toString))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
}
@@ -63,6 +67,8 @@ trait SystemSettingsService {
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
getValue(props, Ssh, false),
@@ -92,6 +98,7 @@ trait SystemSettingsService {
getOptionValue(props, LdapFullNameAttribute, None),
getOptionValue(props, LdapMailAddressAttribute, None),
getOptionValue[Boolean](props, LdapTls, None),
getOptionValue[Boolean](props, LdapSsl, None),
getOptionValue(props, LdapKeystore, None)))
} else {
None
@@ -109,6 +116,8 @@ object SystemSettingsService {
baseUrl: Option[String],
information: Option[String],
allowAccountRegistration: Boolean,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean,
gravatar: Boolean,
notification: Boolean,
ssh: Boolean,
@@ -134,6 +143,7 @@ object SystemSettingsService {
fullNameAttribute: Option[String],
mailAttribute: Option[String],
tls: Option[Boolean],
ssl: Option[Boolean],
keystore: Option[String])
case class Smtp(
@@ -152,6 +162,8 @@ object SystemSettingsService {
private val BaseURL = "base_url"
private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration"
private val AllowAnonymousAccess = "allow_anonymous_access"
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val Ssh = "ssh"
@@ -174,6 +186,7 @@ object SystemSettingsService {
private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val LdapTls = "ldap.tls"
private val LdapSsl = "ldap.ssl"
private val LdapKeystore = "ldap.keystore"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
@@ -195,7 +208,7 @@ object SystemSettingsService {
else value
}
// TODO temporary flag
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
// // TODO temporary flag
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
}

View File

@@ -1,13 +1,14 @@
package service
package gitbucket.core.service
import model.Profile._
import gitbucket.core.model.{WebHook, Account}
import gitbucket.core.model.Profile._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.JGitUtil
import profile.simple._
import model.{WebHook, Account}
import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo
import util.JGitUtil
import RepositoryService.RepositoryInfo
import org.eclipse.jgit.diff.DiffEntry
import util.JGitUtil.CommitInfo
import JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity

View File

@@ -1,9 +1,10 @@
package service
package gitbucket.core.service
import java.util.Date
import gitbucket.core.model.Account
import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._
import org.eclipse.jgit.api.Git
import util._
import _root_.util.ControlUtil._
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import org.eclipse.jgit.lib._
import org.eclipse.jgit.dircache.DirCache
@@ -12,7 +13,7 @@ import java.io.ByteArrayInputStream
import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._
import service.RepositoryService.RepositoryInfo
import RepositoryService.RepositoryInfo
object WikiService {
@@ -46,7 +47,7 @@ object WikiService {
trait WikiService {
import WikiService._
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
def createWikiRepository(loginAccount: Account, owner: String, repository: String): Unit =
LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
if(!dir.exists){
@@ -102,7 +103,7 @@ trait WikiService {
* Reverts specified changes.
*/
def revertWikiPage(owner: String, repository: String, from: String, to: String,
committer: model.Account, pageName: Option[String]): Boolean = {
committer: Account, pageName: Option[String]): Boolean = {
case class RevertInfo(operation: String, filePath: String, source: String)
@@ -204,7 +205,7 @@ trait WikiService {
* Save the wiki page.
*/
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
content: String, committer: Account, message: String, currentId: Option[String]): Option[String] = {
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder()

View File

@@ -0,0 +1,91 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http._
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
import gitbucket.core.util.{ControlUtil, Keys, Implicits}
import org.slf4j.LoggerFactory
import Implicits._
import ControlUtil._
/**
* Provides BASIC Authentication for [[GitRepositoryServlet]].
*/
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter])
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
implicit val request = req.asInstanceOf[HttpServletRequest]
val response = res.asInstanceOf[HttpServletResponse]
val wrappedResponse = new HttpServletResponseWrapper(response){
override def setCharacterEncoding(encoding: String) = {}
}
val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString)
val settings = loadSystemSettings()
try {
defining(request.paths){
case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
case Some(repository) => {
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
chain.doFilter(req, wrappedResponse)
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => {
authenticate(settings, username, password) match {
case Some(account) => {
if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
}
chain.doFilter(req, wrappedResponse)
}
case None => requireAuth(response)
}
}
case _ => requireAuth(response)
}
}
}
}
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
case _ => {
logger.debug(s"Not enough path arguments: ${request.paths}")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
} catch {
case ex: Exception => {
logger.error("error", ex)
requireAuth(response)
}
}
}
private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
}
private def decodeAuthHeader(header: String): String = {
try {
new String(new sun.misc.BASE64Decoder().decodeBuffer(header.substring(6)))
} catch {
case _: Throwable => ""
}
}
}

View File

@@ -1,5 +1,8 @@
package servlet
package gitbucket.core.servlet
import gitbucket.core.model.Session
import gitbucket.core.service._
import gitbucket.core.util._
import org.eclipse.jgit.http.server.GitServlet
import org.eclipse.jgit.lib._
import org.eclipse.jgit.transport._
@@ -9,21 +12,19 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
import service._
import gitbucket.core.util.StringUtil
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import WebHookService._
import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo
import service.IssuesService.IssueSearchCondition
import model.Session
import JGitUtil.CommitInfo
import IssuesService.IssueSearchCondition
/**
* Provides Git repository via HTTP.
*
* This servlet provides only Git repository functionality.
* Authentication is provided by [[servlet.BasicAuthenticationFilter]].
* Authentication is provided by [[BasicAuthenticationFilter]].
*/
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
@@ -174,7 +175,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
case ReceiveCommand.Type.CREATE |
ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(branchName)
updatePullRequests(owner, repository, branchName)
case _ =>
}
}
@@ -211,26 +212,4 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
}
}
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
oldGit.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
.call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
pullreq.userName, pullreq.repositoryName, pullreq.branch,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
}
}
}
}

View File

@@ -1,62 +1,31 @@
package servlet
package gitbucket.core.servlet
import java.io.File
import java.sql.{DriverManager, Connection}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util._
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
import org.apache.commons.io.IOUtils
import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.slf4j.LoggerFactory
import util.Directory._
import util.ControlUtil._
import util.JDBCUtil._
import Directory._
import ControlUtil._
import JDBCUtil._
import org.eclipse.jgit.api.Git
import util.Directory
import plugin.PluginUpdateJob
import service.SystemSettingsService
import gitbucket.core.util.Versions
import gitbucket.core.util.Directory
import gitbucket.core.plugin._
object AutoUpdate {
/**
* Version of GitBucket
*
* @param majorVersion the major version
* @param minorVersion the minor version
*/
case class Version(majorVersion: Int, minorVersion: Int){
private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
/**
* Execute update/MAJOR_MINOR.sql to update schema to this version.
* If corresponding SQL file does not exist, this method do nothing.
*/
def update(conn: Connection): Unit = {
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
if(in != null){
val sql = IOUtils.toString(in, "UTF-8")
using(conn.createStatement()){ stmt =>
logger.debug(sqlPath + "=" + sql)
stmt.executeUpdate(sql)
}
}
}
}
/**
* MAJOR.MINOR
*/
val versionString = s"${majorVersion}.${minorVersion}"
}
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 0),
new Version(2, 8),
new Version(2, 7) {
override def update(conn: Connection): Unit = {
super.update(conn)
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT * FROM REPOSITORY"){ rs =>
// Rename attached files directory from /issues to /comments
val userName = rs.getString("USER_NAME")
@@ -94,8 +63,8 @@ object AutoUpdate {
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection): Unit = {
super.update(conn)
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
val curInfo = rs.getString("ADDITIONAL_INFO")
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
@@ -103,20 +72,22 @@ object AutoUpdate {
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
}
}
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
FileUtils.deleteDirectory(new File(Directory.PluginHome))
ignore {
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
//FileUtils.deleteDirectory(new File(Directory.PluginHome))
}
}
},
new Version(2, 2),
new Version(2, 1),
new Version(2, 0){
override def update(conn: Connection): Unit = {
override def update(conn: Connection, cl: ClassLoader): Unit = {
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
super.update(conn)
super.update(conn, cl)
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
@@ -144,8 +115,8 @@ object AutoUpdate {
Version(1, 5),
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection): Unit = {
super.update(conn)
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
// Fix wiki repository configuration
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
@@ -194,15 +165,14 @@ object AutoUpdate {
}
/**
* Update database schema automatically in the context initializing.
* Initialize GitBucket system.
* Update database schema and load plug-ins automatically in the context initializing.
*/
class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory
class InitializeListener extends ServletContextListener {
import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private val scheduler = StdSchedulerFactory.getDefaultScheduler
private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if(dataDir != null){
@@ -210,69 +180,28 @@ class AutoUpdateListener extends ServletContextListener {
}
org.h2.Driver.load()
val context = event.getServletContext
context.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
defining(getConnection(event.getServletContext)){ conn =>
defining(getConnection()){ conn =>
// Migration
logger.debug("Start schema update")
try {
defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){
logger.debug("No update")
} else if(!versions.contains(currentVersion)){
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
}
}
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn =>
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
}
logger.debug("End schema update")
// Load plugins
logger.debug("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, conn)
}
if(SystemSettingsService.enablePluginSystem){
getDatabase(context).withSession { implicit session =>
logger.debug("Starting plugin system...")
try {
plugin.PluginSystem.init()
scheduler.start()
PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
} catch {
case ex: Throwable => {
logger.error("Failed to initialize plugin system", ex)
ex.printStackTrace()
throw ex
}
}
}
}
}
def contextDestroyed(sce: ServletContextEvent): Unit = {
scheduler.shutdown()
def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown plugins
PluginRegistry.shutdown(event.getServletContext)
}
private def getConnection(servletContext: ServletContext): Connection =
private def getConnection(): Connection =
DriverManager.getConnection(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
private def getDatabase(servletContext: ServletContext): scala.slick.jdbc.JdbcBackend.Database =
slick.jdbc.JdbcBackend.Database.forURL(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
DatabaseConfig.url,
DatabaseConfig.user,
DatabaseConfig.password)
}

View File

@@ -1,8 +1,9 @@
package servlet
package gitbucket.core.servlet
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
import gitbucket.core.util.Directory
import org.apache.commons.io.FileUtils
import util.Directory._
import Directory._
/**
* Removes session associated temporary files when session is destroyed.

View File

@@ -0,0 +1,60 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http.HttpServletRequest
import com.mchange.v2.c3p0.ComboPooledDataSource
import gitbucket.core.util.DatabaseConfig
import org.slf4j.LoggerFactory
import slick.jdbc.JdbcBackend.{Database => SlickDatabase, Session}
import gitbucket.core.util.Keys
/**
* Controls the transaction with the open session in view pattern.
*/
class TransactionFilter extends Filter {
private val logger = LoggerFactory.getLogger(classOf[TransactionFilter])
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){
// assets don't need transaction
chain.doFilter(req, res)
} else {
Database() withTransaction { session =>
logger.debug("begin transaction")
req.setAttribute(Keys.Request.DBSession, session)
chain.doFilter(req, res)
logger.debug("end transaction")
}
}
}
}
object Database {
private val logger = LoggerFactory.getLogger(Database.getClass)
private val db: SlickDatabase = {
val datasource = new ComboPooledDataSource
datasource.setDriverClass(DatabaseConfig.driver)
datasource.setJdbcUrl(DatabaseConfig.url)
datasource.setUser(DatabaseConfig.user)
datasource.setPassword(DatabaseConfig.password)
logger.debug("load database connection pool")
SlickDatabase.forDataSource(datasource)
}
def apply(): SlickDatabase = db
def getSession(req: ServletRequest): Session =
req.getAttribute(Keys.Request.DBSession).asInstanceOf[Session]
}

View File

@@ -1,24 +1,24 @@
package ssh
package gitbucket.core.ssh
import gitbucket.core.model.Session
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
import gitbucket.core.servlet.{Database, CommitLogHook}
import gitbucket.core.util.{Directory, ControlUtil}
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
import org.slf4j.LoggerFactory
import java.io.{InputStream, OutputStream}
import util.ControlUtil._
import ControlUtil._
import org.eclipse.jgit.api.Git
import util.Directory._
import Directory._
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.command.UnknownCommand
import servlet.{Database, CommitLogHook}
import service.{AccountService, RepositoryService, SystemSettingsService}
import org.eclipse.jgit.errors.RepositoryNotFoundException
import javax.servlet.ServletContext
import model.Session
object GitCommand {
val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
}
abstract class GitCommand(val context: ServletContext, val owner: String, val repoName: String) extends Command {
abstract class GitCommand(val owner: String, val repoName: String) extends Command {
self: RepositoryService with AccountService =>
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
@@ -31,7 +31,7 @@ abstract class GitCommand(val context: ServletContext, val owner: String, val re
private def newTask(user: String): Runnable = new Runnable {
override def run(): Unit = {
Database(context) withSession { implicit session =>
Database() withSession { implicit session =>
try {
runTask(user)
callback.onExit(0)
@@ -80,7 +80,7 @@ abstract class GitCommand(val context: ServletContext, val owner: String, val re
}
class GitUploadPack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
class GitUploadPack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName)
with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -97,7 +97,7 @@ class GitUploadPack(context: ServletContext, owner: String, repoName: String, ba
}
class GitReceivePack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
class GitReceivePack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName)
with SystemSettingsService with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -119,15 +119,15 @@ class GitReceivePack(context: ServletContext, owner: String, repoName: String, b
}
class GitCommandFactory(context: ServletContext, baseUrl: String) extends CommandFactory {
class GitCommandFactory(baseUrl: String) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
logger.debug(s"command: $command")
command match {
case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(context, owner, repoName, baseUrl)
case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(context, owner, repoName, baseUrl)
case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(owner, repoName, baseUrl)
case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(owner, repoName, baseUrl)
case _ => new UnknownCommand(command)
}
}
}
}

View File

@@ -1,10 +1,10 @@
package ssh
package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService
import org.apache.sshd.common.Factory
import org.apache.sshd.server.{Environment, ExitCallback, Command}
import java.io.{OutputStream, InputStream}
import org.eclipse.jgit.lib.Constants
import service.SystemSettingsService
class NoShell extends Factory[Command] with SystemSettingsService {
override def create(): Command = new Command() {

View File

@@ -1,16 +1,15 @@
package ssh
package gitbucket.core.ssh
import gitbucket.core.service.SshKeyService
import gitbucket.core.servlet.Database
import org.apache.sshd.server.PublickeyAuthenticator
import org.apache.sshd.server.session.ServerSession
import java.security.PublicKey
import service.SshKeyService
import servlet.Database
import javax.servlet.ServletContext
class PublicKeyAuthenticator(context: ServletContext) extends PublickeyAuthenticator with SshKeyService {
class PublicKeyAuthenticator extends PublickeyAuthenticator with SshKeyService {
override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = {
Database(context) withSession { implicit session =>
Database() withSession { implicit session =>
getPublicKeys(username).exists { sshKey =>
SshUtil.str2PublicKey(sshKey.publicKey) match {
case Some(publicKey) => key.equals(publicKey)

View File

@@ -1,10 +1,10 @@
package ssh
package gitbucket.core.ssh
import javax.servlet.{ServletContext, ServletContextEvent, ServletContextListener}
import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util.Directory
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
import util.Directory
import service.SystemSettingsService
import java.util.concurrent.atomic.AtomicBoolean
object SshServer {
@@ -12,17 +12,17 @@ object SshServer {
private val server = org.apache.sshd.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false)
private def configure(context: ServletContext, port: Int, baseUrl: String) = {
private def configure(port: Int, baseUrl: String) = {
server.setPort(port)
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser"))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(context))
server.setCommandFactory(new GitCommandFactory(context, baseUrl))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setShellFactory(new NoShell)
}
def start(context: ServletContext, port: Int, baseUrl: String) = {
def start(port: Int, baseUrl: String) = {
if(active.compareAndSet(false, true)){
configure(context, port, baseUrl)
configure(port, baseUrl)
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
}
@@ -55,8 +55,7 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
case None =>
logger.error("Could not start SshServer because the baseUrl is not configured.")
case Some(baseUrl) =>
SshServer.start(sce.getServletContext,
settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
SshServer.start(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
}
}
}

View File

@@ -1,4 +1,4 @@
package ssh
package gitbucket.core.ssh
import java.security.PublicKey
import org.slf4j.LoggerFactory

View File

@@ -1,10 +1,10 @@
package util
package gitbucket.core.util
import app.ControllerBase
import service._
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.{RepositoryService, AccountService}
import RepositoryService.RepositoryInfo
import util.Implicits._
import util.ControlUtil._
import Implicits._
import ControlUtil._
/**
* Allows only oneself and administrators.

View File

@@ -1,4 +1,4 @@
package util
package gitbucket.core.util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
@@ -37,4 +37,10 @@ object ControlUtil {
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
def ignore[T](f: => Unit): Unit = try {
f
} catch {
case e: Exception => ()
}
}

View File

@@ -0,0 +1,19 @@
package gitbucket.core.util
import com.typesafe.config.ConfigFactory
import Directory.DatabaseHome
object DatabaseConfig {
private val config = ConfigFactory.load("database")
private val dbUrl = config.getString("db.url")
def url(directory: Option[String]): String =
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
val url: String = url(None)
val user: String = config.getString("db.user")
val password: String = config.getString("db.password")
val driver: String = config.getString("db.driver")
}

View File

@@ -1,7 +1,7 @@
package util
package gitbucket.core.util
import java.io.File
import util.ControlUtil._
import ControlUtil._
import org.apache.commons.io.FileUtils
/**

View File

@@ -1,9 +1,9 @@
package util
package gitbucket.core.util
import org.apache.commons.io.FileUtils
import java.net.URLConnection
import java.io.File
import util.ControlUtil._
import ControlUtil._
import scala.util.Random
object FileUtil {

View File

@@ -1,9 +1,10 @@
package util
package gitbucket.core.util
import gitbucket.core.servlet.Database
import scala.util.matching.Regex
import scala.util.control.Exception._
import slick.jdbc.JdbcBackend
import servlet.Database
import javax.servlet.http.{HttpSession, HttpServletRequest}
/**

View File

@@ -1,7 +1,7 @@
package util
package gitbucket.core.util
import java.sql._
import util.ControlUtil._
import ControlUtil._
import scala.collection.mutable.ListBuffer
/**
@@ -18,6 +18,14 @@ object JDBCUtil {
}
}
def find[T](sql: String, params: Any*)(f: ResultSet => T): Option[T] = {
execute(sql, params: _*){ stmt =>
using(stmt.executeQuery()){ rs =>
if(rs.next) Some(f(rs)) else None
}
}
}
def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = {
execute(sql, params: _*){ stmt =>
using(stmt.executeQuery()){ rs =>

View File

@@ -1,9 +1,10 @@
package util
package gitbucket.core.util
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
import util.Directory._
import util.StringUtil._
import util.ControlUtil._
import Directory._
import StringUtil._
import ControlUtil._
import scala.annotation.tailrec
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib._
@@ -13,9 +14,9 @@ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec
import java.util.Date
import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
@@ -133,6 +134,10 @@ object JGitUtil {
*/
case class SubmoduleInfo(name: String, path: String, url: String)
case class BranchMergeInfo(ahead: Int, behind: Int, isMerged: Boolean)
case class BranchInfo(name: String, committerName: String, commitTime: Date, committerEmailAddress:String, mergeInfo: Option[BranchMergeInfo], commitId: String)
/**
* Returns RevCommit from the commit or tag id.
*
@@ -674,6 +679,25 @@ object JGitUtil {
}.head.id
}
/**
* Fetch pull request contents into refs/pull/${issueId}/head and return (commitIdTo, commitIdFrom)
*/
def updatePullRequest(userName: String, repositoryName:String, branch: String, issueId: Int,
requestUserName: String, requestRepositoryName: String, requestBranch: String):(String, String) =
using(Git.open(Directory.getRepositoryDir(userName, repositoryName)),
Git.open(Directory.getRepositoryDir(requestUserName, requestRepositoryName))){ (oldGit, newGit) =>
oldGit.fetch
.setRemote(Directory.getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head").setForceUpdate(true))
.call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${issueId}/head").getName
val commitIdFrom = getForkedCommitId(oldGit, newGit,
userName, repositoryName, branch,
requestUserName, requestRepositoryName, requestBranch)
(commitIdTo, commitIdFrom)
}
/**
* Returns the last modified commit of specified path
* @param git the Git object
@@ -685,4 +709,43 @@ object JGitUtil {
return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
}
def getBranches(owner: String, name: String, defaultBranch: String): Seq[BranchInfo] = {
using(Git.open(getRepositoryDir(owner, name))){ git =>
val repo = git.getRepository
val defaultObject = repo.resolve(defaultBranch)
git.branchList.call.asScala.map { ref =>
val walk = new RevWalk(repo)
try{
val defaultCommit = walk.parseCommit(defaultObject)
val branchName = ref.getName.stripPrefix("refs/heads/")
val branchCommit = if(branchName == defaultBranch){
defaultCommit
}else{
walk.parseCommit(ref.getObjectId)
}
val when = branchCommit.getCommitterIdent.getWhen
val committer = branchCommit.getCommitterIdent.getName
val committerEmail = branchCommit.getCommitterIdent.getEmailAddress
val mergeInfo = if(branchName==defaultBranch){
None
}else{
walk.reset()
walk.setRevFilter( RevFilter.MERGE_BASE )
walk.markStart(branchCommit)
walk.markStart(defaultCommit)
val mergeBase = walk.next()
walk.reset()
walk.setRevFilter(RevFilter.ALL)
Some(BranchMergeInfo(
ahead = RevWalkUtils.count(walk, branchCommit, mergeBase),
behind = RevWalkUtils.count(walk, defaultCommit, mergeBase),
isMerged = walk.isMergedInto(branchCommit, defaultCommit)))
}
BranchInfo(branchName, committer, when, committerEmail, mergeInfo, ref.getObjectId.name)
} finally {
walk.dispose();
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package util
package gitbucket.core.util
/**
* Define key strings for request attributes, session attributes or flash attributes.

View File

@@ -1,13 +1,13 @@
package util
package gitbucket.core.util
import util.ControlUtil._
import service.SystemSettingsService
import gitbucket.core.model.Account
import ControlUtil._
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.Ldap
import com.novell.ldap._
import java.security.Security
import org.slf4j.LoggerFactory
import service.SystemSettingsService.Ldap
import scala.annotation.tailrec
import model.Account
/**
* Utility for LDAP authentication.
@@ -48,6 +48,7 @@ object LDAPUtil {
dn = ldapSettings.bindDN.getOrElse(""),
password = ldapSettings.bindPassword.getOrElse(""),
tls = ldapSettings.tls.getOrElse(false),
ssl = ldapSettings.ssl.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "System LDAP authentication failed."
){ conn =>
@@ -65,6 +66,7 @@ object LDAPUtil {
dn = userDN,
password = password,
tls = ldapSettings.tls.getOrElse(false),
ssl = ldapSettings.ssl.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed."
){ conn =>
@@ -96,7 +98,7 @@ object LDAPUtil {
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
}
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, ssl: Boolean, keystore: String, error: String)
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) {
// Dynamically set Sun as the security provider
@@ -109,7 +111,13 @@ object LDAPUtil {
}
}
val conn: LDAPConnection = new LDAPConnection(new LDAPJSSEStartTLSFactory())
val conn: LDAPConnection =
if(ssl) {
new LDAPConnection(new LDAPJSSESecureSocketFactory())
}else {
new LDAPConnection(new LDAPJSSEStartTLSFactory())
}
try {
// Connect to the server
conn.connect(host, port)

View File

@@ -1,8 +1,8 @@
package util
package gitbucket.core.util
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.{ReentrantLock, Lock}
import util.ControlUtil._
import ControlUtil._
object LockUtil {

View File

@@ -1,22 +1,24 @@
package util
package gitbucket.core.util
import gitbucket.core.model.{Session, Issue}
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService}
import gitbucket.core.servlet.Database
import gitbucket.core.view.Markdown
import scala.concurrent._
import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory
import app.Context
import model.Session
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import servlet.Database
import gitbucket.core.controller.Context
import SystemSettingsService.Smtp
import _root_.util.ControlUtil.defining
import ControlUtil.defining
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit session: Session, context: Context) =
protected def recipients(issue: Issue)(notify: String => Unit)(implicit session: Session, context: Context) =
(
// individual repository's owner
issue.userName ::
@@ -27,7 +29,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
)
.distinct
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) )
}
@@ -67,14 +69,14 @@ class Mailer(private val smtp: Smtp) extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context) = {
val database = Database(context.request.getServletContext)
val database = Database()
val f = Future {
database withSession { implicit session =>
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
defining(
s"[${r.name}] ${issue.title} (#${issueId})" ->
msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
msg(Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
recipients(issue) { to =>
val email = new HtmlEmail
email.setHostName(smtp.host)
@@ -113,4 +115,4 @@ class Mailer(private val smtp: Smtp) extends Notifier {
class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit = {}
}
}

View File

@@ -1,8 +1,8 @@
package util
package gitbucket.core.util
import java.net.{URLDecoder, URLEncoder}
import org.mozilla.universalchardet.UniversalDetector
import util.ControlUtil._
import ControlUtil._
import org.apache.commons.io.input.BOMInputStream
import org.apache.commons.io.IOUtils
@@ -30,7 +30,7 @@ object StringUtil {
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
/**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
* Make string from byte array. Character encoding is detected automatically by [[StringUtil.detectEncoding]].
* And if given bytes contains UTF-8 BOM, it's removed from returned string.
*/
def convertFromByteArray(content: Array[Byte]): String =

View File

@@ -1,4 +1,4 @@
package util
package gitbucket.core.util
import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages

View File

@@ -0,0 +1,67 @@
package gitbucket.core.util
import java.sql.Connection
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import ControlUtil._
case class Version(majorVersion: Int, minorVersion: Int) {
private val logger = LoggerFactory.getLogger(classOf[Version])
/**
* Execute update/MAJOR_MINOR.sql to update schema to this version.
* If corresponding SQL file does not exist, this method do nothing.
*/
def update(conn: Connection, cl: ClassLoader): Unit = {
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
using(cl.getResourceAsStream(sqlPath)){ in =>
if(in != null){
val sql = IOUtils.toString(in, "UTF-8")
using(conn.createStatement()){ stmt =>
logger.debug(sqlPath + "=" + sql)
stmt.executeUpdate(sql)
}
}
}
}
/**
* MAJOR.MINOR
*/
val versionString = s"${majorVersion}.${minorVersion}"
}
object Versions {
private val logger = LoggerFactory.getLogger(Versions.getClass)
def update(conn: Connection, headVersion: Version, currentVersion: Version, versions: Seq[Version], cl: ClassLoader)
(save: Connection => Unit): Unit = {
logger.debug("Start schema update")
try {
if(currentVersion == headVersion){
logger.debug("No update")
} else if(currentVersion.versionString != "0.0" && !versions.contains(currentVersion)){
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn, cl))
save(conn)
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
}
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
}
logger.debug("End schema update")
}
}

View File

@@ -1,8 +1,9 @@
package view
package gitbucket.core.view
import service.RequestCache
import gitbucket.core.controller.Context
import gitbucket.core.service.RequestCache
import gitbucket.core.util.StringUtil
import play.twirl.api.Html
import util.StringUtil
trait AvatarImageProvider { self: RequestCache =>
@@ -11,7 +12,7 @@ trait AvatarImageProvider { self: RequestCache =>
* Looks up Gravatar if avatar icon has not been configured in user settings.
*/
protected def getAvatarImageHtml(userName: String, size: Int,
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html = {
val src = if(mailAddress.isEmpty){
// by user name

View File

@@ -1,15 +1,17 @@
package view
package gitbucket.core.view
import service.RequestCache
import util.Implicits.RichString
import gitbucket.core.controller.Context
import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.Implicits
import gitbucket.core.util.Implicits.RichString
trait LinkConverter { self: RequestCache =>
/**
* Converts issue id, username and commit id to link.
*/
protected def convertRefsLinks(value: String, repository: service.RepositoryService.RepositoryInfo,
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
protected def convertRefsLinks(value: String, repository: RepositoryService.RepositoryInfo,
issueIdPrefix: String = "#")(implicit context: Context): String = {
value
// escape HTML tags
.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")

View File

@@ -1,26 +1,32 @@
package view
package gitbucket.core.view
import util.StringUtil
import util.ControlUtil._
import util.Directory._
import org.parboiled.common.StringUtils
import org.pegdown._
import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer
import java.util.Locale
import java.util.regex.Pattern
import gitbucket.core.controller.Context
import gitbucket.core.service.{RepositoryService, RequestCache, WikiService}
import gitbucket.core.util.StringUtil
import org.parboiled.common.StringUtils
import org.pegdown.LinkRenderer.Rendering
import org.pegdown._
import org.pegdown.ast._
import scala.collection.JavaConverters._
import service.{RequestCache, WikiService}
object Markdown {
/**
* Converts Markdown of Wiki pages to HTML.
*/
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean,
enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = {
def toHtml(markdown: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): String = {
// escape issue id
val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
@@ -35,12 +41,16 @@ object Markdown {
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML
).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages).toHtml(rootNode)
}
}
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean) extends LinkRenderer with WikiService {
class GitBucketLinkRender(
context: Context,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
pages: List[String]) extends LinkRenderer with WikiService {
override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){
try {
@@ -54,7 +64,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){
if(pages.contains(page)){
new Rendering(url, label)
} else {
new Rendering(url, label).withAttribute("class", "absent")
@@ -87,13 +97,14 @@ class GitBucketVerbatimSerializer extends VerbatimSerializer {
class GitBucketHtmlSerializer(
markdown: String,
repository: service.RepositoryService.RepositoryInfo,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean
)(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink),
hasWritePermission: Boolean,
pages: List[String]
)(implicit val context: Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink, pages),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache {
@@ -185,6 +196,32 @@ class GitBucketHtmlSerializer(
printTag(node, "li")
}
}
override def visit(node: ExpLinkNode) {
printLink(linkRenderer.render(node, printLinkChildrenToString(node)))
}
def printLinkChildrenToString(node: SuperNode) = {
val priorPrinter = printer
printer = new Printer()
visitLinkChildren(node)
val result = printer.getString()
printer = priorPrinter
result
}
def visitLinkChildren(node: SuperNode) {
import scala.collection.JavaConversions._
node.getChildren.foreach(child => child match {
case node: ExpImageNode => visitLinkChild(node)
case node: SuperNode => visitLinkChildren(node)
case _ => child.accept(this)
})
}
def visitLinkChild(node: ExpImageNode) {
printer.print("<img src=\"").print(fixUrl(node.url, true)).print("\" alt=\"").printEncoded(printChildrenToString(node)).print("\"/>")
}
}
object GitBucketHtmlSerializer {

View File

@@ -1,4 +1,4 @@
package view
package gitbucket.core.view
/**
* Provides control information for pagination.

View File

@@ -1,9 +1,12 @@
package view
import java.util.{Locale, Date, TimeZone}
package gitbucket.core.view
import java.text.SimpleDateFormat
import java.util.{Date, Locale, TimeZone}
import gitbucket.core.controller.Context
import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.{JGitUtil, StringUtil}
import play.twirl.api.Html
import util.StringUtil
import service.RequestCache
/**
* Provides helper methods for Twirl templates.
@@ -38,10 +41,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
/**
*
* Format java.util.Date to "x {seconds/minutes/hours/days} ago"
* If duration over 1 month, format to "d MMM (yyyy)"
*
*/
def datetimeAgoRecentOnly(date: Date): String = {
val duration = new Date().getTime - date.getTime
@@ -77,9 +78,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def plural(count: Int, singular: String, plural: String = ""): String =
if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] =
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, RepositoryService.RepositoryInfo, Boolean, Boolean, Context) => Html)] =
Seq(
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context))
)
@@ -88,13 +89,18 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
def markdown(value: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: Context): Html = {
val fileNameLower = filePath.reverse.head.toLowerCase
renderersBySuffix.find { case (suffix, _) => fileNameLower.endsWith(suffix) } match {
@@ -111,20 +117,20 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Returns &lt;img&gt; which displays the avatar icon for the given user name.
* This method looks up Gravatar if avatar icon has not been configured in user settings.
*/
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: Context): Html =
getAvatarImageHtml(userName, size, "", tooltip)
/**
* Returns &lt;img&gt; which displays the avatar icon for the given mail address.
* This method looks up Gravatar if avatar icon has not been configured in user settings.
*/
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
def avatar(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html =
getAvatarImageHtml(commit.authorName, size, commit.authorEmailAddress)
/**
* Converts commit id, issue id and username to the link.
*/
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
def link(value: String, repository: RepositoryService.RepositoryInfo)(implicit context: Context): Html =
Html(convertRefsLinks(value, repository))
def cut(value: String, length: Int): String =
@@ -144,14 +150,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* Convert link notations in the activity message.
*/
def activityMessage(message: String)(implicit context: app.Context): Html =
def activityMessage(message: String)(implicit context: Context): Html =
Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
)
@@ -167,34 +173,34 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* Generates the url to the repository.
*/
def url(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): String =
def url(repository: RepositoryService.RepositoryInfo)(implicit context: Context): String =
s"${context.path}/${repository.owner}/${repository.name}"
/**
* Generates the url to the account page.
*/
def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}"
def url(userName: String)(implicit context: Context): String = s"${context.path}/${userName}"
/**
* Returns the url to the root of assets.
*/
def assets(implicit context: app.Context): String = s"${context.path}/assets"
def assets(implicit context: Context): String = s"${context.path}/assets"
/**
* Generates the text link to the account page.
* If user does not exist or disabled, this method returns user name as text without link.
*/
def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: app.Context): Html =
def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: Context): Html =
userWithContent(userName, mailAddress, styleClass)(Html(userName))
/**
* Generates the avatar link to the account page.
* If user does not exist or disabled, this method returns avatar image without link.
*/
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html =
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html =
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip))
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: app.Context): Html =
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html =
(if(mailAddress.isEmpty){
getAccountByUserName(userName)
} else {
@@ -247,6 +253,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
}
def pre(value: Html): Html = Html(s"<pre>${value.body.trim.split("\n").map(_.trim).mkString("\n")}</pre>")
/**
* Implicit conversion to add mkHtml() to Seq[Html].
*/

View File

@@ -1,22 +0,0 @@
package plugin
import plugin.PluginSystem._
import java.sql.Connection
trait Plugin {
val id: String
val version: String
val author: String
val url: String
val description: String
def repositoryMenus : List[RepositoryMenu]
def globalMenus : List[GlobalMenu]
def repositoryActions : List[RepositoryAction]
def globalActions : List[Action]
def javaScripts : List[JavaScript]
}
object PluginConnectionHolder {
val threadLocal = new ThreadLocal[Connection]
}

View File

@@ -1,194 +0,0 @@
package plugin
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
import util.Directory._
import util.ControlUtil._
import org.apache.commons.io.{IOUtils, FileUtils}
import Security._
import service.PluginService
import model.Profile._
import profile.simple._
import java.io.FileInputStream
import java.sql.Connection
import app.Context
import service.RepositoryService.RepositoryInfo
/**
* Provides extension points to plug-ins.
*/
object PluginSystem extends PluginService {
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
private val initialized = new AtomicBoolean(false)
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
def install(plugin: Plugin): Unit = {
pluginsMap.put(plugin.id, plugin)
}
def plugins: List[Plugin] = pluginsMap.values.toList
def uninstall(id: String)(implicit session: Session): Unit = {
pluginsMap.remove(id)
// Delete from PLUGIN table
deletePlugin(id)
// Drop tables
val pluginDir = new java.io.File(PluginHome)
val sqlFile = new java.io.File(pluginDir, s"${id}/sql/drop.sql")
if(sqlFile.exists){
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
using(session.conn.createStatement()){ stmt =>
stmt.executeUpdate(sql)
}
}
}
def repositories: List[PluginRepository] = repositoriesList.toList
/**
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
*/
def init()(implicit session: Session): Unit = {
if(initialized.compareAndSet(false, true)){
// Load installed plugins
val pluginDir = new java.io.File(PluginHome)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
installPlugin(dir.getName)
}
}
// Add default plugin repositories
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
}
}
// TODO Method name seems to not so good.
def installPlugin(id: String)(implicit session: Session): Unit = {
val pluginHome = new java.io.File(PluginHome)
val pluginDir = new java.io.File(pluginHome, id)
val scalaFile = new java.io.File(pluginDir, "plugin.scala")
if(scalaFile.exists && scalaFile.isFile){
val properties = new java.util.Properties()
using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
properties.load(in)
}
val pluginId = properties.getProperty("id")
val version = properties.getProperty("version")
val author = properties.getProperty("author")
val url = properties.getProperty("url")
val description = properties.getProperty("description")
val source = s"""
|val id = "${pluginId}"
|val version = "${version}"
|val author = "${author}"
|val url = "${url}"
|val description = "${description}"
""".stripMargin + FileUtils.readFileToString(scalaFile, "UTF-8")
try {
// Compile and eval Scala source code
ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
ScalaPlugin.compileTemplate(
id.replace("-", ""),
file.getName.stripSuffix(".scala.html"),
IOUtils.toString(new FileInputStream(file)))
}.mkString("\n") + source)
// Migrate database
val plugin = getPlugin(pluginId)
if(plugin.isEmpty){
registerPlugin(model.Plugin(pluginId, version))
migrate(session.conn, pluginId, "0.0")
} else {
updatePlugin(model.Plugin(pluginId, version))
migrate(session.conn, pluginId, plugin.get.version)
}
} catch {
case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e)
}
}
}
// TODO Should PluginSystem provide a way to migrate resources other than H2?
private def migrate(conn: Connection, pluginId: String, current: String): Unit = {
val pluginDir = new java.io.File(PluginHome)
// TODO Is ot possible to use this migration system in GitBucket migration?
val dim = current.split("\\.")
val currentVersion = Version(dim(0).toInt, dim(1).toInt)
val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql")
if(sqlDir.exists && sqlDir.isDirectory){
sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file =>
val array = file.getName.replaceFirst("\\.sql", "").split("_")
Version(array(0).toInt, array(1).toInt)
}
.sorted.reverse.takeWhile(_ > currentVersion)
.reverse.foreach { version =>
val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql")
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
using(conn.createStatement()){ stmt =>
stmt.executeUpdate(sql)
}
}
}
}
case class Version(major: Int, minor: Int) extends Ordered[Version] {
override def compare(that: Version): Int = {
if(major != that.major){
major.compare(that.major)
} else{
minor.compare(that.minor)
}
}
def displayString: String = major + "." + minor
}
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
def javaScripts : List[JavaScript] = pluginsMap.values.flatMap(_.javaScripts).toList
// Case classes to hold plug-ins information internally in GitBucket
case class PluginRepository(id: String, url: String)
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
case class Action(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any)
case class RepositoryAction(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any)
case class Button(label: String, href: String)
case class JavaScript(filter: String => Boolean, script: String)
/**
* Checks whether the plugin is updatable.
*/
def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
if(oldVersion == newVersion){
false
} else {
val dim1 = oldVersion.split("\\.").map(_.toInt)
val dim2 = newVersion.split("\\.").map(_.toInt)
dim1.zip(dim2).foreach { case (a, b) =>
if(a < b){
return true
} else if(a > b){
return false
}
}
return false
}
}
}

View File

@@ -1,66 +0,0 @@
package plugin
import util.Directory._
import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
import org.quartz.{Scheduler, JobExecutionContext, Job}
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
class PluginUpdateJob extends Job {
private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
private var failedCount = 0
/**
* Clone or pull all plugin repositories
*
* TODO Support plugin repository access through the proxy server
*/
override def execute(context: JobExecutionContext): Unit = {
try {
if(failedCount > 3){
logger.error("Skip plugin information updating because failed count is over limit")
} else {
logger.info("Start plugin information updating")
PluginSystem.repositories.foreach { repository =>
logger.info(s"Updating ${repository.id}: ${repository.url}...")
val dir = getPluginCacheDir()
val repo = new java.io.File(dir, repository.id)
if(repo.exists){
// pull if the repository is already cloned
Git.open(repo).pull().call()
} else {
// clone if the repository is not exist
Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
}
}
logger.info("End plugin information updating")
}
} catch {
case e: Exception => {
failedCount = failedCount + 1
logger.error("Failed to update plugin information", e)
}
}
}
}
object PluginUpdateJob {
def schedule(scheduler: Scheduler): Unit = {
val job = newJob(classOf[PluginUpdateJob])
.withIdentity("pluginUpdateJob")
.build()
val trigger = newTrigger()
.withIdentity("pluginUpdateTrigger")
.startNow()
.withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
.build()
scheduler.scheduleJob(job, trigger)
}
}

View File

@@ -1,77 +0,0 @@
package plugin
import scala.collection.mutable.ListBuffer
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import app.Context
import plugin.PluginSystem._
import plugin.PluginSystem.RepositoryMenu
import plugin.Security._
import service.RepositoryService.RepositoryInfo
import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox
import play.twirl.compiler.TwirlCompiler
import scala.io.Codec
// TODO This is a sample implementation for Scala based plug-ins.
class ScalaPlugin(val id: String, val version: String,
val author: String, val url: String, val description: String) extends Plugin {
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[RepositoryAction]()
private val globalActionList = ListBuffer[Action]()
private val javaScriptList = ListBuffer[JavaScript]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
def javaScripts : List[JavaScript] = javaScriptList.toList
def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
}
def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
globalMenuList += GlobalMenu(label, url, icon, condition)
}
def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
globalActionList += Action(method, path, security, function)
}
def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = {
repositoryActionList += RepositoryAction(method, path, security, function)
}
def addJavaScript(filter: String => Boolean, script: String): Unit = {
javaScriptList += JavaScript(filter, script)
}
}
object ScalaPlugin {
def define(id: String, version: String, author: String, url: String, description: String)
= new ScalaPlugin(id, version, author, url, description)
def eval(source: String): Any = {
val toolbox = currentMirror.mkToolBox()
val tree = toolbox.parse(source)
toolbox.eval(tree)
}
def compileTemplate(packageName: String, name: String, source: String): String = {
val result = TwirlCompiler.parseAndGenerateCodeNewParser(
Array(packageName, name),
source.getBytes("UTF-8"),
Codec(scala.util.Properties.sourceEncoding),
"",
"play.twirl.api.HtmlFormat.Appendable",
"play.twirl.api.HtmlFormat",
"",
false)
result.replaceFirst("package .*", "")
}
}

View File

@@ -1,36 +0,0 @@
package plugin
/**
* Defines enum case classes to specify permission for actions which is provided by plugin.
*/
object Security {
sealed trait Security
/**
* All users and guests
*/
case class All() extends Security
/**
* Only signed-in users
*/
case class Login() extends Security
/**
* Only repository owner and collaborators
*/
case class Member() extends Security
/**
* Only repository owner and managers of group repository
*/
case class Owner() extends Security
/**
* Only administrators
*/
case class Admin() extends Security
}

Some files were not shown because too many files have changed in this diff Show More