Compare commits

..

127 Commits
2.2 ... 2.4.1

Author SHA1 Message Date
Naoki Takezoe
ca0f888a99 Update for 2.4.1 2014-10-06 13:56:33 +09:00
Naoki Takezoe
3b08dc2e41 (refs #510)Fix dropdown presentation 2014-10-06 13:47:41 +09:00
Naoki Takezoe
cc128a49c1 (refs #510)Dirty fix for Firefox 2014-10-06 13:37:32 +09:00
Naoki Takezoe
e0148695f2 (refs #509)Fix link broken bug in Wiki 2014-10-06 13:22:42 +09:00
Naoki Takezoe
afe0b1dd71 Fix link bug in pull requests 2014-10-06 03:08:35 +09:00
Naoki Takezoe
353852d6da Update README.md 2014-10-06 02:14:10 +09:00
Naoki Takezoe
28585d1a3d Merge branch 'new-issue-ui' 2014-10-06 01:54:19 +09:00
shimamoto
9d69a48c65 (refs #488) Fix bug for refer comment. 2014-10-06 01:40:23 +09:00
shimamoto
2f95c76634 (refs #488) Fixed compile error. 2014-10-06 01:39:42 +09:00
shimamoto
eac9f0e6ff (refs #488) Fixed the action for assignee. 2014-10-06 01:30:30 +09:00
Naoki Takezoe
043fc21e05 Add Version 2.4 2014-10-06 01:05:40 +09:00
shimamoto
5854a75615 (refs #488) Fixed the action for label and milestone. 2014-10-06 00:52:14 +09:00
Naoki Takezoe
7b02946496 (refs #506)Fix generated URL for images 2014-10-05 23:07:15 +09:00
Naoki Takezoe
70f0ffd4f4 Fix label text color 2014-10-05 20:01:34 +09:00
Naoki Takezoe
91b82c2652 (refs #505)Disable the plugin system in default 2014-10-05 18:08:33 +09:00
shimamoto
b1017140aa (refs #488) Fixed the action for issue and comment content change. 2014-10-05 17:13:58 +09:00
takezoe
fc806b8813 Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-10-05 15:37:22 +09:00
takezoe
836913482b (refs #488)Issue label editing is completed 2014-10-05 15:37:09 +09:00
Naoki Takezoe
b3df3f44c6 Merge pull request #500 from mrkm4ntr/split-diff-style
Modify styles of split diff.
2014-10-05 11:56:49 +09:00
shimamoto
4ffbf89e74 (refs #488) Fixed the action for issue title change. 2014-10-05 04:43:11 +09:00
shimamoto
9851c7d93d (refs #488) Changing the issue edit. 2014-10-04 23:53:37 +09:00
Tomofumi Tanaka
2201f2b202 Simplify query 2014-10-04 00:31:23 +09:00
takezoe
c92e71bb7a Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-10-01 02:25:43 +09:00
takezoe
d271fac350 (refs #488)Add transparent label icons 2014-10-01 02:25:19 +09:00
Naoki Takezoe
ce4522fc30 (refs #488)Remove top margin of the clear condition link 2014-09-30 02:31:11 +09:00
Tomofumi Tanaka
a178c48de6 Merge branch 'fix-498-dash-pr' 2014-09-29 20:51:32 +09:00
Tomofumi Tanaka
9d1323a044 (refs #498)Reformat counting pull request query 2014-09-29 16:38:12 +09:00
Tomofumi Tanaka
43babfed94 (refs #498) Correct pull request counts 2014-09-29 14:00:29 +09:00
Tomofumi Tanaka
6fa7ea30fb (refs #498)Returns private own repository
RepositoryService.getAllRepositories should returns
private own repository too.
2014-09-29 11:53:19 +09:00
Tomofumi Tanaka
d78315695b (refs $#498)Don't show private repo user doesn't have permission
This fix in the dashboard pull request view
But this fix still has a problem show wrong count number pull request.
2014-09-29 11:27:48 +09:00
shimamoto
16021865cb (refs #488) Fixed the screen layout. 2014-09-28 21:15:30 +09:00
Naoki Takezoe
b516be242d (refs #488)Show number of issues for each labels 2014-09-28 11:43:58 +09:00
Naoki Takezoe
0124f7cc3c (refs #488)Change permission to access to labels 2014-09-28 11:35:19 +09:00
Naoki Takezoe
f3eec35287 (refs #488)Fix nav-pills style in the header of the issues system 2014-09-28 11:24:27 +09:00
Naoki Takezoe
fb396a33b0 (refs #488)Remove unnecessary div 2014-09-28 03:05:04 +09:00
Naoki Takezoe
3370499421 (refs #488)Apply new UI to labels 2014-09-28 03:04:36 +09:00
shimamoto
d847e27cf9 (refs #488) Move Milestone and Assignee. 2014-09-27 23:48:38 +09:00
Naoki Takezoe
9684b158ce (refs #488)Apply new UI to the milestone create / edit page 2014-09-26 02:14:30 +09:00
Naoki Takezoe
8456808a8e (refs #488)Add milestone icons 2014-09-25 09:37:13 +09:00
Naoki Takezoe
9747899a19 (refs #488)Update icons 2014-09-25 09:36:48 +09:00
shimamoto
099304605e Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-09-25 04:49:07 +09:00
shimamoto
30994d0465 (refs #488) WIP: Fixing the screen layout for the issue detail. 2014-09-25 04:48:39 +09:00
Naoki Takezoe
71fdbe7b71 (refs #488)Apply new UI to Milestones tab 2014-09-25 02:32:55 +09:00
Naoki Takezoe
86432c5ffe (refs #488)Small fix 2014-09-25 01:52:32 +09:00
takezoe
4dfa1fb0f8 Merge remote-tracking branch 'origin/new-issue-ui' into new-issue-ui 2014-09-23 23:26:54 +09:00
takezoe
db59a7652f (refs #488)Add icons for new Issues UI 2014-09-23 23:26:46 +09:00
shimamoto
417470a81c (refs #488) Fixed the screen layout for the issue creation. 2014-09-23 21:42:56 +09:00
Naoki Takezoe
cc639da17e (refs #488)Unify issues and pull requests template 2014-09-23 18:11:46 +09:00
Naoki Takezoe
f619f4a9bc (refs #488)Remove unnecessary template arguments 2014-09-23 17:50:47 +09:00
Naoki Takezoe
5dffc2a64e (refs #488)Batch edit for pull requests 2014-09-23 17:41:53 +09:00
Naoki Takezoe
bb63a8d14c (refs #488)Remove unnecessary template arguments 2014-09-23 17:23:52 +09:00
Naoki Takezoe
c1263cc16d (refs #488)Batch edit for issues 2014-09-23 14:57:29 +09:00
Shintaro Murakami
49f2e7d70f Add empty style. 2014-09-23 02:10:46 +09:00
Shintaro Murakami
f93b535f70 Modify styles of split diff. 2014-09-23 00:07:00 +09:00
shimamoto
e16d3c823b Update slick version. 2014-09-22 21:59:17 +09:00
Naoki Takezoe
7a6fdbcf50 (refs #488)Fix the "New pull request" button 2014-09-22 19:26:36 +09:00
Naoki Takezoe
46041a3762 (refs #488)Apply new UI to the pull request list 2014-09-22 19:10:39 +09:00
Naoki Takezoe
20b0553f7f (refs #488)Exclude pull requests from the issue list 2014-09-22 18:38:28 +09:00
Naoki Takezoe
5870cacf44 (refs #488)Fix presentation of issue list header 2014-09-22 18:29:30 +09:00
Naoki Takezoe
cb512cd98d (refs #488)Add link to clear search condition 2014-09-22 10:18:26 +09:00
Naoki Takezoe
90487eb7b7 (refs #488)Merge user filter into IssueSearchCondition 2014-09-22 10:08:22 +09:00
Naoki Takezoe
706fa77de3 (refs #488)Add service.IssuesService.IssueInfo 2014-09-21 02:06:38 +09:00
Naoki Takezoe
3b1367dd8e (refs #488)Displays the milestone on the issue list. 2014-09-20 03:02:52 +09:00
Naoki Takezoe
f4e4506517 (refs #488)Start to apply new issue UI to the issue list. 2014-09-16 00:46:38 +09:00
Naoki Takezoe
287a0b6669 (refs #488)Copy listparts.scala.html from issues/pulls to dashboard. 2014-09-15 23:47:06 +09:00
Naoki Takezoe
5bddd352af (refs #453)Fix "Test Hook" behavior 2014-09-15 13:04:39 +09:00
Naoki Takezoe
9c6ea8fb9d Fix client side validation error 2014-09-15 12:39:30 +09:00
Naoki Takezoe
32e8bf46a7 Merge pull request #491 from mrkm4ntr/myBranch
Change to show a shortcut path of the directory whose ancestors have only one child directory.
2014-09-15 02:12:52 +09:00
Naoki Takezoe
d61fe1bf84 (refs #483)Fix link in markdown 2014-09-14 20:31:02 +09:00
Naoki Takezoe
47dbea947d (refs #487) split diff is available 2014-09-14 10:53:17 +09:00
Naoki Takezoe
97c6b0495e Fix Warnings 2014-09-13 19:01:49 +09:00
Tomofumi Tanaka
a602ece8e9 (refs #490)Set HEAD ref when saved default branch
GitBucket allows user to configure default branch in the repository.
But it's only affect repository viewer in the GitBucekt world.
This change set default branch in the Git world.
2014-09-12 22:01:21 +09:00
Shintaro Murakami
cf6dca84d8 Release TreeWalk in recuresive function. 2014-09-11 00:06:28 +09:00
Shintaro Murakami
79432ff8ad Like GitHub, show a shortcut path of the directory whoes ancestors have only one child directory. 2014-09-10 07:07:04 +09:00
tanacasino
b8613431de Merge pull request #484 from douglarek/patch-1
Use a shebang (#!/bin/sh) to run sbt.sh
2014-09-04 11:42:17 +09:00
Lingchao Xin
698eafa562 Use a shebang (#!/bin/sh) to run sbt.sh
If no shebang provided in a shell file, it will be executed by default shell.

However this script is not compatible with Fish Shell, so use sh to execute it.
2014-09-04 10:25:24 +08:00
Naoki Takezoe
d33886db89 Add RawData and 404 error response for plugin action 2014-09-03 02:26:06 +09:00
Naoki Takezoe
cde09d3a59 Fix plugin path problem 2014-09-03 01:46:37 +09:00
Naoki Takezoe
5674f0e980 Update README.md 2014-09-01 14:34:58 +09:00
Naoki Takezoe
b9ade60eb2 (refs #464)Improve plugin installing/updating behavior 2014-09-01 01:11:19 +09:00
Naoki Takezoe
96303723fa (refs #464)Clear plugins before upgrading to 2.3. 2014-09-01 00:29:13 +09:00
Naoki Takezoe
0f5dbc5788 Merge branch 'master' into scala-plugin
Conflicts:
	project/build.scala
2014-09-01 00:02:31 +09:00
Naoki Takezoe
8df0c3a439 (refs #476)Change Jetty temp directory to GITBUCKET_HOME/tmp 2014-08-31 23:31:59 +09:00
Tomofumi Tanaka
ca6a86816a (refs #461)Correct atom feed datetime format 2014-08-25 22:32:07 +09:00
Tomofumi Tanaka
3ea939798f Remove unused import 2014-08-24 22:18:21 +09:00
Tomofumi Tanaka
d947410e3c (refs #434)Refactor to get last modified commit 2014-08-24 22:18:21 +09:00
Tomofumi Tanaka
db59bc08ac (refs #434)Show only last modified commit 2014-08-24 22:18:14 +09:00
Naoki Takezoe
95a8649f79 (refs #464)Add Fragment rendering support for Ajax 2014-08-24 13:56:49 +09:00
Naoki Takezoe
ffd10122ed (refs #464)Some improve for plugin API
- Place holder support for db API
- Redirect support for plugin action
2014-08-23 17:57:06 +09:00
Naoki Takezoe
c4c39f36e9 (refs #464)Add db.update() to update DB from plugin 2014-08-23 03:28:13 +09:00
Naoki Takezoe
96900c3cbf (refs #464)Remove unnecessary App mix-in 2014-08-23 03:26:19 +09:00
Tomofumi Tanaka
69fa370d12 Tweak font size and family in blog/diff view 2014-08-22 00:02:58 +09:00
Tomofumi Tanaka
7496437d11 Remove unused h7 2014-08-20 22:34:48 +09:00
shimamoto
33b7d09af7 Update slick version to 2.1.0. 2014-08-17 18:25:06 +09:00
Naoki Takezoe
53d0974760 (refs #457)Fix the target of updateRef 2014-08-17 13:01:05 +09:00
Naoki Takezoe
a87399f223 (refs #464)Add a new parameter to specify request method for plugin actions 2014-08-16 16:02:02 +09:00
Naoki Takezoe
975dfb17e1 (refs #464)Twirl support for plugin 2014-08-16 02:53:43 +09:00
Naoki Takezoe
8b8bd0289b (refs #464)Fix test case 2014-08-14 22:44:04 +09:00
Naoki Takezoe
3bb69c623b (refs #464)Switch to play-twirl 2014-08-14 18:37:37 +09:00
Naoki Takezoe
dd427bdbef Merge branch 'master' of https://github.com/takezoe/gitbucket 2014-08-14 14:01:33 +09:00
Naoki Takezoe
b40657a14a (refs #467)Reverse tag table ordering 2014-08-14 14:01:11 +09:00
Tomofumi Tanaka
21ca5b2eec (refs #471)Show the copy button only when flash is available
Check flash availability before Showing the copy button.
2014-08-14 12:43:55 +09:00
Naoki Takezoe
b78d584d8a (refs #464)Add drop tables capability when plugin is uninstalled 2014-08-14 01:56:47 +09:00
Naoki Takezoe
e6b666a66a (refs #464)Implementing database migration system for plugin 2014-08-13 23:16:13 +09:00
Naoki Takezoe
bab93ea4f5 (refs #464)Fix compilation error 2014-08-13 22:06:08 +09:00
Naoki Takezoe
7fe98253ae Merge pull request #452 from mslinn/master
Enhanced install script so it works under Ubuntu and Mac OS/X
2014-08-13 15:38:44 +09:00
Naoki Takezoe
13385cbced (refs #464)Add PLUGIN table for plugin management 2014-08-13 02:23:29 +09:00
Naoki Takezoe
3f20cec7b2 (refs #464)Add Scaladoc 2014-08-12 00:37:36 +09:00
Naoki Takezoe
a0e4b020ca (refs #464)Authentication for actions which are defined by plugin is completed 2014-08-12 00:33:13 +09:00
Naoki Takezoe
ea5d898b27 (refs #464)Add Security sealed trait which is used by plugin 2014-08-12 00:02:48 +09:00
Naoki Takezoe
4e652b5ccd (refs #464)Add authentication for plugin action 2014-08-11 19:27:24 +09:00
Naoki Takezoe
dd809896c8 (refs #464)Add extension point to inject JavaScript instead of adding button 2014-08-11 00:45:58 +09:00
Naoki Takezoe
93536d3365 (refs #464)Add new extension point to add buttons 2014-08-10 05:42:06 +09:00
Naoki Takezoe
098b18fe6d (refs #464)Experimental implementation of Scala based plugin 2014-08-10 04:33:57 +09:00
tanacasino
66efdac757 Merge pull request #449 from jparound30/fix_423
Change blob view's table-layout property.
2014-08-07 23:08:54 +09:00
Tomofumi Tanaka
45545d3815 Revert "(refs #458)Skip unexpected commit message"
This reverts commit be79ac2eb2.
2014-08-05 23:02:31 +09:00
Tomofumi Tanaka
b65d41731b (refs #458)Correct commit message in activity info 2014-08-05 23:00:56 +09:00
Tomofumi Tanaka
be19e97518 Merge branch '2.2-update' 2014-08-05 08:53:51 +09:00
Naoki Takezoe
2ebf2b99bd Update README.md 2014-08-05 02:15:36 +09:00
Tomofumi Tanaka
be79ac2eb2 (refs #458)Skip unexpected commit message
This patch is temporary measures.
MUST create AutoUpdate before release 2.3.
2014-08-05 00:43:20 +09:00
Tomofumi Tanaka
05afec3236 (refs#458)Correct short and full message
Swaped short and full message in commit info by accident.
2014-08-05 00:07:21 +09:00
Mike Slinn
193a312b22 Made gitbucket run on system startup and stop on shutdown 2014-07-29 15:49:48 -07:00
Mike Slinn
6a2d2ebfd1 Added help info for user about making iptables changes persistent 2014-07-29 15:44:43 -07:00
Mike Slinn
6d200aa340 Works under Ubuntu and Mac OS/X 2014-07-29 07:28:44 -07:00
Mike Slinn
a0fbb90048 Works on Mac, need to retest on Ubuntu 2014-07-29 00:18:09 -07:00
Mike Slinn
08e29e7077 Added install script, made existing RedHat init script also work with Ubuntu 2014-07-28 10:10:27 -07:00
jparound30
3bef71f5f2 (refs #423)Change blob view's table-layout property. 2014-07-29 00:22:22 +09:00
103 changed files with 2498 additions and 1407 deletions

View File

@@ -80,6 +80,24 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.4.1 - 6 Oct 2014
- Bug fix
### 2.4 - 6 Oct 2014
- New UI is applied to Issues and Pull requests
- Side-by-side diff is available
- Fix relative path problem in Markdown links and images
- Plugin System is disabled in default
- Some bug fix and improvements
### 2.3 - 1 Sep 2014
- Scala based plugin system
- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`
- Some bug fix and improvements
### 2.2.1 - 5 Aug 2014
- Bug fix
### 2.2 - 4 Aug 2014 ### 2.2 - 4 Aug 2014
- Plug-in system is available - Plug-in system is available
- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1 - Move to Scala 2.11, Scalatra 2.3 and Slick 2.1

13
contrib/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Contrib Notes #
The configuration script adapts according to the OS.
The `linux` directory contains scripts for Ubuntu and RedHat.
The Mac scripts have been folded in as well.
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`

62
contrib/gitbucket.conf Normal file
View File

@@ -0,0 +1,62 @@
# Configuration section is below. Ignore this part
function isUbuntu {
if [ -f /etc/lsb-release ]; then
grep -i ubuntu /etc/lsb-release | head -n 1 | cut -d \ -f 1 | cut -d = -f 2
fi
}
function isRedHat {
if [ -d "/etc/rc.d/init.d" ]; then echo yes; fi
}
function isMac {
if [[ "$(uname -a | cut -d \ -f 1 )" == "Darwin" ]]; then echo yes; fi
}
#
# Configuration section start
#
# Bind host
GITBUCKET_HOST=0.0.0.0
# Other Java option
GITBUCKET_JVM_OPTS=-Dmail.smtp.starttls.enable=true
# Data directory, holds repositories
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_LOG_DIR=/var/log/gitbucket
# Server port
GITBUCKET_PORT=8080
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
GITBUCKET_PREFIX=
# Directory where GitBucket is installed
# Configuration is stored here:
GITBUCKET_DIR=/usr/share/gitbucket
GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
# Path to the WAR file
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
# GitBucket version to fetch when installing
GITBUCKET_VERSION=2.1
#
# End of configuration section. Ignore this part
#
if [ `isUbuntu` ]; then
GITBUCKET_SERVICE=/etc/init.d/gitbucket
elif [ `isRedHat` ]; then
GITBUCKET_SERVICE=/etc/rc.d/init.d
elif [ `isMac` ]; then
GITBUCKET_SERVICE=/Library/StartupItems/GitBucket/GitBucket
else
echo "Don't know how to install onto this OS"
exit -2
fi

View File

@@ -1,6 +1,8 @@
#!/bin/bash #!/bin/bash
# #
# /etc/rc.d/init.d/gitbucket # RedHat: /etc/rc.d/init.d/gitbucket
# Ubuntu: /etc/init.d/gitbucket
# Mac OS/X: /Library/StartupItems/GitBucket
# #
# Starts the GitBucket server # Starts the GitBucket server
# #
@@ -8,28 +10,44 @@
# description: Run GitBucket server # description: Run GitBucket server
# processname: java # processname: java
# Source function library set -e
. /etc/rc.d/init.d/functions
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
[ -f /etc/rc.common ] && source /etc/rc.common # Mac OS/X
# Default values # Default values
GITBUCKET_HOME=/var/lib/gitbucket GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# Pull in cq settings # Pull in cq settings
[ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket [ -f /etc/sysconfig/gitbucket ] && source /etc/sysconfig/gitbucket # RedHat
[ -f gitbucket.conf ] && source gitbucket.conf # For all systems
# Location of the log and PID file # Location of the log and PID file
LOG_FILE=/var/log/gitbucket/run.log LOG_FILE=$GITBUCKET_LOG_DIR/run.log
PID_FILE=/var/run/gitbucket.pid PID_FILE=/var/run/gitbucket.pid
# Default return value RED='\033[1m\E[37;41m'
RETVAL=0 GREEN='\033[1m\E[37;42m'
OFF='\E[0m'
if [ -z "$(which success)" ]; then
function success {
printf "%b\n" "$GREEN $* $OFF"
}
fi
if [ -z "$(which failure)" ]; then
function failure {
printf "%b\n" "$RED $* $OFF"
}
fi
RETVAL=0
start() { start() {
echo -n $"Starting GitBucket server: " echo -n $"Starting GitBucket server: "
# Compile statup parameters START_OPTS=
if [ $GITBUCKET_PORT ]; then if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}" START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi fi
@@ -40,17 +58,15 @@ start() {
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}" START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi fi
# Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 & GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
RETVAL=$? RETVAL=$?
# Store PID of the Java process into a file
echo $! > $PID_FILE echo $! > $PID_FILE
if [ $RETVAL -eq 0 ] ; then if [ $RETVAL -eq 0 ] ; then
success "GitBucket startup" success "Success"
else else
failure "GitBucket startup" failure "Exit code $RETVAL"
fi fi
echo echo
@@ -82,25 +98,41 @@ restart() {
start start
} }
## MacOS proxies for System V service hooks:
StartService() {
start
}
case "$1" in StopService() {
start) stop
}
RestartService() {
restart
}
if [ `isMac` ]; then
RunService "$1"
else
case "$1" in
start)
start start
;; ;;
stop) stop)
stop stop
;; ;;
restart) restart)
restart restart
;; ;;
status) status)
status -p $PID_FILE java status -p $PID_FILE java
RETVAL=$? RETVAL=$?
;; ;;
*) *)
echo $"Usage: $0 [start|stop|restart|status]" echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2 RETVAL=2
esac esac
exit $RETVAL
fi
exit $RETVAL

69
contrib/install Executable file
View File

@@ -0,0 +1,69 @@
#!/bin/bash
# Only tested on Ubuntu 14.04
# Uses information stored in GitBucket git repo on GitHub as defaults.
# Edit gitbucket.conf before running this
set -e
GITBUCKET_VERSION=2.1
if [ ! -f gitbucket.conf ]; then
echo "gitbucket.conf not found, aborting"
exit -3
fi
source gitbucket.conf
function createDir {
if [ ! -d "$1" ]; then
echo "Making $1 directory."
sudo mkdir -p "$1"
fi
}
if [ "$(which iptables)" ]; then
echo "Opening port $GITBUCKET_PORT in firewall."
sudo iptables -A INPUT -p tcp --dport $GITBUCKET_PORT -j ACCEPT
echo "Please use iptables-persistent:"
echo " sudo apt-get install iptables-persistent"
echo "After installed, you can save/reload iptables rules anytime:"
echo " sudo /etc/init.d/iptables-persistent save"
echo " sudo /etc/init.d/iptables-persistent reload"
fi
createDir "$GITBUCKET_HOME"
createDir "$GITBUCKET_WAR_DIR"
createDir "$GITBUCKET_DIR"
createDir "$GITBUCKET_LOG_DIR"
echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
echo "Copying gitbucket.conf to $GITBUCKET_DIR"
sudo cp gitbucket.conf $GITBUCKET_DIR
if [ `isUbuntu` ] || [ `isRedHat` ]; then
sudo cp gitbucket.init "$GITBUCKET_SERVICE"
# Install gitbucket as a service that starts when system boots
sudo chown root:root $GITBUCKET_SERVICE
sudo chmod 755 $GITBUCKET_SERVICE
sudo update-rc.d "$(basename $GITBUCKET_SERVICE)" defaults 98 02
echo "Starting GitBucket service"
sudo $GITBUCKET_SERVICE start
elif [ `isMac` ]; then
sudo macosx/makePlist
echo "Starting GitBucket service"
sudo cp gitbucket.conf "$GITBUCKET_SERVICE"
sudo cp gitbucket.init "$GITBUCKET_SERVICE"
sudo chmod a+x "$GITBUCKET_SERVICE"
sudo "$GITBUCKET_SERVICE" start
else
echo "Don't know how to install this OS"
exit -2
fi
if [ $? != 0 ]; then
less "$GITBUCKET_LOG_DIR/run.log"
fi

View File

@@ -1,3 +1,10 @@
#!/bin/bash
# From http://docstore.mik.ua/orelly/unix3/mac/ch02_02.htm
source gitbucket.conf
GITBUCKET_SERVICE_DIR=`dirname "$GITBUCKET_SERVICE"`
mkdir -p "$GITBUCKET_SERVICE_DIR"
cat << EOF > "$GITBUCKET_SERVICE_DIR/gitbucket.plist"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
@@ -7,14 +14,15 @@
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/usr/bin/java</string> <string>/usr/bin/java</string>
<string>-Dmail.smtp.starttls.enable=true</string> <string>$GITBUCKET_JVM_OPTS</string>
<string>-jar</string> <string>-jar</string>
<string>gitbucket.war</string> <string>gitbucket.war</string>
<string>--host=127.0.0.1</string> <string>--host=$GITBUCKET_HOST</string>
<string>--port=8080</string> <string>--port=$GITBUCKET_PORT</string>
<string>--https=true</string> <string>--https=true</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
</dict> </dict>
</plist> </plist>
EOF

View File

@@ -1,17 +0,0 @@
# Bind host
#GITBUCKET_HOST=0.0.0.0
# Server port
#GITBUCKET_PORT=8080
# Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket
# Path to the WAR file
#GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
#GITBUCKET_PREFIX=
# Other Java option
#GITBUCKET_JVM_OPTS=

View File

@@ -34,9 +34,9 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="0.7" inkscape:zoom="0.98994949"
inkscape:cx="482.58197" inkscape:cx="174.78739"
inkscape:cy="-83.92636" inkscape:cy="-195.96338"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1-9" inkscape:current-layer="layer1-9"
showgrid="false" showgrid="false"
@@ -1583,7 +1583,7 @@
<path <path
id="path2991-7-1-4-1" id="path2991-7-1-4-1"
transform="translate(-154.10522,1432.0357)" transform="translate(-154.10522,1432.0357)"
d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z" d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571" sodipodi:ry="104.28571"
sodipodi:rx="104.28571" sodipodi:rx="104.28571"
sodipodi:cy="290.93362" sodipodi:cy="290.93362"
@@ -1592,7 +1592,7 @@
sodipodi:type="arc" /> sodipodi:type="arc" />
<path <path
id="path2993-4-5-8-7" id="path2993-4-5-8-7"
d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z" d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571" sodipodi:ry="104.28571"
sodipodi:rx="104.28571" sodipodi:rx="104.28571"
sodipodi:cy="290.93362" sodipodi:cy="290.93362"
@@ -1643,7 +1643,7 @@
sodipodi:cy="812.36218" sodipodi:cy="812.36218"
sodipodi:rx="10" sodipodi:rx="10"
sodipodi:ry="10" sodipodi:ry="10"
d="m 710,812.36218 a 10,10 0 1 1 -20,0 10,10 0 1 1 20,0 z" d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-1490.7493,1534.7336)" /> transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-1490.7493,1534.7336)" />
<rect <rect
style="fill:#ffffff;fill-opacity:1;stroke:#bebeff;stroke-width:10.80681515000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" style="fill:#ffffff;fill-opacity:1;stroke:#bebeff;stroke-width:10.80681515000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
@@ -1670,7 +1670,7 @@
style="fill:#ffffff;stroke:#bebefa;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> style="fill:#ffffff;stroke:#bebefa;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path <path
transform="matrix(1.0049237,0,0,0.61497516,302.39116,1664.7945)" transform="matrix(1.0049237,0,0,0.61497516,302.39116,1664.7945)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z" d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915" sodipodi:ry="35.140915"
sodipodi:rx="21.718279" sodipodi:rx="21.718279"
sodipodi:cy="230.89374" sodipodi:cy="230.89374"
@@ -1680,7 +1680,7 @@
sodipodi:type="arc" /> sodipodi:type="arc" />
<path <path
transform="matrix(1.0049237,0,0,0.61497516,300.85563,1514.4712)" transform="matrix(1.0049237,0,0,0.61497516,300.85563,1514.4712)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z" d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915" sodipodi:ry="35.140915"
sodipodi:rx="21.718279" sodipodi:rx="21.718279"
sodipodi:cy="230.89374" sodipodi:cy="230.89374"
@@ -1690,7 +1690,7 @@
sodipodi:type="arc" /> sodipodi:type="arc" />
<path <path
transform="matrix(1.0049237,0,0,0.61497516,401.70879,1561.5007)" transform="matrix(1.0049237,0,0,0.61497516,401.70879,1561.5007)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z" d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915" sodipodi:ry="35.140915"
sodipodi:rx="21.718279" sodipodi:rx="21.718279"
sodipodi:cy="230.89374" sodipodi:cy="230.89374"
@@ -1698,6 +1698,147 @@
id="path3795-8-4-8-2-1" id="path3795-8-4-8-2-1"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" /> sodipodi:type="arc" />
<g
id="g3992">
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
style="fill:#3c3c3c;fill-opacity:1;stroke:none"
width="34.635483"
height="158.96587"
x="1836.6243"
y="-1788.4895"
id="rect2995-0-8-4-1" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
style="fill:#3c3c3c;fill-opacity:1;stroke:none"
width="33.538391"
height="96.944809"
x="1628.6003"
y="1772.8655"
id="rect2995-0-8-4-1-4" />
</g>
<g
id="g4112"
transform="translate(88.611046,-13.773858)">
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
style="fill:#a0a0a0;fill-opacity:1;stroke:none"
width="34.635483"
height="158.96587"
x="1527.2657"
y="-1466.7803"
id="rect2995-0-8-4-1-5" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
style="fill:#a0a0a0;fill-opacity:1;stroke:none"
width="33.538391"
height="96.944809"
x="1306.8911"
y="1463.507"
id="rect2995-0-8-4-1-4-5" />
</g>
<path
style="fill:#b3b3b3;stroke:none"
d="m 2185.2705,373.3859 -109.47,85.45235 29.4727,-89.94984 z"
id="path3894-1-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:49.97417831;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="rect3088-5-5-7"
width="174.36192"
height="89.170021"
x="2060.0393"
y="293.00055" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#dcdcdc;stroke-width:2.10925268999999990;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4170"
width="35.913948"
height="206.36755"
x="2110.2112"
y="507.8555" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#ffffff;stroke-width:15.12008381000000100;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4166"
width="174.5864"
height="76.446434"
x="2035.1414"
y="548.66016" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#dcdcdc;stroke-width:1.42725468000000010;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4174"
width="43.442127"
height="43.442127"
x="1928.0846"
y="-1122.7543"
transform="matrix(0.72181305,0.69208809,-0.72181305,0.69208809,0,0)" />
<path
sodipodi:type="arc"
style="fill:#ffffe6;fill-opacity:1;stroke:#ffffff;stroke-width:10.1960001;stroke-linejoin:miter;stroke-miterlimit:4.30000019;stroke-opacity:1;stroke-dasharray:none"
id="path4364"
sodipodi:cx="1418.2542"
sodipodi:cy="434.14883"
sodipodi:rx="11.111678"
sodipodi:ry="11.111678"
d="m 1429.3658,434.14883 c 0,6.13681 -4.9748,11.11168 -11.1116,11.11168 -6.1369,0 -11.1117,-4.97487 -11.1117,-11.11168 0,-6.13681 4.9748,-11.11167 11.1117,-11.11167 6.1368,0 11.1116,4.97486 11.1116,11.11167 z"
transform="matrix(1.2783369,0,0,1.2783369,315.0834,31.171302)" />
<path
style="fill:#dcdcdc;stroke:none;fill-opacity:1"
d="m 2533.6893,373.6989 -109.47,85.45235 29.4727,-89.94984 z"
id="path3894-1-1-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#dcdcdc;stroke:#dcdcdc;stroke-width:49.97417449999999700;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="rect3088-5-5-7-7"
width="174.36192"
height="89.170021"
x="2408.458"
y="293.31354" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#888888;stroke-width:73.08132935000000400;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3220"
width="104.54597"
height="104.54597"
x="45.94949"
y="1925.303" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1"
width="117.84303"
height="30.608574"
x="1271.0641"
y="-1484.6459"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-7"
width="117.84303"
height="30.608574"
x="1408.8896"
y="1314.712"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
style="fill:#0088cc;fill-opacity:1;stroke:#0088cc;stroke-width:73.08132935000000400;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3220-4"
width="104.54597"
height="104.54597"
x="337.49615"
y="1924.5376" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-0"
width="117.84303"
height="30.608574"
x="1064.3683"
y="-1690.2594"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-7-9"
width="117.84303"
height="30.608574"
x="1614.5032"
y="1108.0162"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1 +1 @@
sbt.version=0.13.1 sbt.version=0.13.5

View File

@@ -1,8 +1,9 @@
import sbt._ import sbt._
import Keys._ import Keys._
import org.scalatra.sbt._ import org.scalatra.sbt._
import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
import play.twirl.sbt.SbtTwirl
import play.twirl.sbt.Import.TwirlKeys._
object MyBuild extends Build { object MyBuild extends Build {
val Organization = "jp.sf.amateras" val Organization = "jp.sf.amateras"
@@ -13,8 +14,10 @@ object MyBuild extends Build {
lazy val project = Project ( lazy val project = Project (
"gitbucket", "gitbucket",
file("."), file(".")
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq( )
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
.settings(
sourcesInBase := false, sourcesInBase := false,
organization := Organization, organization := Organization,
name := Name, name := Name,
@@ -39,20 +42,19 @@ object MyBuild extends Build {
"org.apache.commons" % "commons-email" % "1.3.1", "org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3", "org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.11.0", "org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.1.0-RC3", "com.typesafe.slick" %% "slick" % "2.1.0",
"org.mozilla" % "rhino" % "1.7R4",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1", "org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.4.180", "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" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"), "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.11" % "test" "junit" % "junit" % "4.11" % "test",
"com.typesafe.play" %% "twirl-compiler" % "1.0.2"
), ),
EclipseKeys.withSource := true, EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "6", "-source", "6"), javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"), testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
packageOptions += Package.MainClass("JettyLauncher") packageOptions += Package.MainClass("JettyLauncher")
) ++ seq(Twirl.settings: _*) ).enablePlugins(SbtTwirl)
)
} }

View File

@@ -4,8 +4,6 @@ addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5") addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
resolvers += "spray repo" at "http://repo.spray.io" addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")

View File

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

3
sbt.sh
View File

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

View File

@@ -1,10 +1,8 @@
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.webapp.WebAppContext;
import java.io.IOException; import java.io.File;
import java.net.URL; import java.net.URL;
import java.security.ProtectionDomain; import java.security.ProtectionDomain;
@@ -44,6 +42,14 @@ public class JettyLauncher {
server.addConnector(connector); server.addConnector(connector);
WebAppContext context = new WebAppContext(); WebAppContext context = new WebAppContext();
File tmpDir = new File(getGitBucketHome(), "tmp");
if(tmpDir.exists()){
deleteDirectory(tmpDir);
}
tmpDir.mkdirs();
context.setTempDirectory(tmpDir);
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain(); ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation(); URL location = domain.getCodeSource().getLocation();
@@ -59,4 +65,27 @@ public class JettyLauncher {
server.start(); server.start();
server.join(); server.join();
} }
private static File getGitBucketHome(){
String home = System.getProperty("gitbucket.home");
if(home != null && home.length() > 0){
return new File(home);
}
home = System.getenv("GITBUCKET_HOME");
if(home != null && home.length() > 0){
return new File(home);
}
return new File(System.getProperty("user.home"), ".gitbucket");
}
private static void deleteDirectory(File dir){
for(File file: dir.listFiles()){
if(file.isFile()){
file.delete();
} else if(file.isDirectory()){
deleteDirectory(file);
}
}
dir.delete();
}
} }

View File

@@ -0,0 +1,6 @@
CREATE TABLE PLUGIN (
PLUGIN_ID VARCHAR(100) NOT NULL,
VERSION VARCHAR(100) NOT NULL
);
ALTER TABLE PLUGIN ADD CONSTRAINT IDX_PLUGIN_PK PRIMARY KEY (PLUGIN_ID);

View File

@@ -335,7 +335,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
builder.finish() builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit") Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
} }
} }

View File

@@ -50,20 +50,20 @@ trait DashboardControllerBase extends ControllerBase {
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) //val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
dashboard.html.issues( dashboard.html.issues(
issues.html.listparts( dashboard.html.issueslist(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page, page,
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), countIssue(condition.copy(state = "closed"), false, userRepos: _*),
condition), condition),
countIssue(condition, Map.empty, false, userRepos: _*), countIssue(condition.copy(assigned = None, author = None), false, userRepos: _*),
countIssue(condition, Map("assigned" -> userName), false, userRepos: _*), countIssue(condition.copy(assigned = Some(userName), author = None), false, userRepos: _*),
countIssue(condition, Map("created_by" -> userName), false, userRepos: _*), countIssue(condition.copy(assigned = None, author = Some(userName)), false, userRepos: _*),
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*), countIssueGroupByRepository(condition, false, userRepos: _*),
condition, condition,
filter) filter)
@@ -80,24 +80,24 @@ trait DashboardControllerBase extends ControllerBase {
}.copy(repo = repository)) }.copy(repo = repository))
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val allRepos = getAllRepositories() val allRepos = getAllRepositories(userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository( val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*) IssueSearchCondition().copy(state = condition.state), true, userRepos: _*)
dashboard.html.pulls( dashboard.html.pulls(
pulls.html.listparts( dashboard.html.pullslist(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*), countIssue(condition.copy(state = "closed"), true, allRepos: _*),
condition, condition,
None, None,
false), false),
getPullRequestCountGroupByUser(condition.state == "closed", None, None), getAllPullRequestCountGroupByUser(condition.state == "closed", userName),
userRepos.map { case (userName, repoName) => userRepos.map { case (userName, repoName) =>
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0)) (userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
}.sortBy(_._3).reverse, }.sortBy(_._3).reverse,

View File

@@ -9,6 +9,7 @@ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue import model.Issue
import plugin.PluginSystem
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -20,7 +21,6 @@ trait IssuesControllerBase extends ControllerBase {
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String) case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String]) case class IssueStateForm(issueId: Int, content: Option[String])
@@ -32,10 +32,12 @@ trait IssuesControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply) )(IssueCreateForm.apply)
val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required)))
)(x => x)
val issueEditForm = mapping( val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueEditForm.apply) )(x => x)
val commentForm = mapping( val commentForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
@@ -47,16 +49,8 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { get("/:owner/:repository/issues")(referrersOnly { repository =>
searchIssues("all", _) searchIssues(repository)
})
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _)
})
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _)
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
@@ -125,14 +119,29 @@ trait IssuesControllerBase extends ControllerBase {
} }
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditable(owner, name, issue.openedUserName)){
// update issue // update issue
updateIssue(owner, name, issue.issueId, form.title, form.content) updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue.copy(title = title), title)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment
createReferComment(owner, name, issue, content.getOrElse(""))
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized
@@ -180,7 +189,7 @@ trait IssuesControllerBase extends ControllerBase {
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => issues.html.editissue( case t if t == "html" => issues.html.editissue(
x.title, x.content, x.issueId, x.userName, x.repositoryName) x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
@@ -234,15 +243,17 @@ trait IssuesControllerBase extends ControllerBase {
milestoneId("milestoneId").map { milestoneId => milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name) getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false) issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound } getOrElse NotFound
} getOrElse Ok() } getOrElse Ok()
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action => defining(params.get("value")){ action =>
executeBatch(repository) { action match {
handleComment(_, None, repository)( _ => action) case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
case _ => // TODO BadRequest
} }
} }
}) })
@@ -292,7 +303,10 @@ trait IssuesControllerBase extends ControllerBase {
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues") params("from") match {
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
}
} }
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
@@ -318,10 +332,10 @@ trait IssuesControllerBase extends ControllerBase {
val (action, recordActivity) = val (action, recordActivity) =
getAction(issue) getAction(issue)
.collect { .collect {
case "close" => true -> (Some("close") -> case "close" if(!issue.closed) => true ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") -> case "reopen" if(issue.closed) => false ->
Some(recordReopenIssueActivity _)) (Some("reopen") -> Some(recordReopenIssueActivity _))
} }
.map { case (closed, t) => .map { case (closed, t) =>
updateClosed(owner, name, issueId, closed) updateClosed(owner, name, issueId, closed)
@@ -336,7 +350,7 @@ trait IssuesControllerBase extends ControllerBase {
case (content, action) => createComment(owner, name, userName, issueId, content, action) case (content, action) => createComment(owner, name, userName, issueId, content, action)
} }
// record activity // record comment activity if comment is entered
content foreach { content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _) (owner, name, userName, issueId, _)
@@ -369,9 +383,8 @@ trait IssuesControllerBase extends ControllerBase {
} }
} }
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName) val sessionKey = Keys.Session.Issues(owner, repoName)
@@ -382,19 +395,15 @@ trait IssuesControllerBase extends ControllerBase {
) )
issues.html.list( issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), "issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted, (getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), countIssue(condition.copy(state = "closed"), false, owner -> repoName),
countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition, condition,
filter,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))
} }

View File

@@ -2,51 +2,67 @@ package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.CollaboratorsAuthenticator import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import util.Implicits._ import util.Implicits._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok
class LabelsController extends LabelsControllerBase class LabelsController extends LabelsControllerBase
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator with LabelsService with IssuesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait LabelsControllerBase extends ControllerBase { trait LabelsControllerBase extends ControllerBase {
self: LabelsService with RepositoryService with CollaboratorsAuthenticator => self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
val newForm = mapping( val labelForm = mapping(
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), "labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"newColor" -> trim(label("Color", text(required, color))) "labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
val editForm = mapping( get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), issues.labels.html.list(
"editColor" -> trim(label("Color", text(required, color))) getLabels(repository.owner, repository.name),
)(LabelForm.apply) countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) => hasWritePermission(repository.owner, repository.name, context.loginAccount))
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect(s"/${repository.owner}/${repository.name}/issues")
}) })
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) issues.labels.html.edit(None, repository)
}) })
ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { 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(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
issues.labels.html.edit(Some(label), repository) issues.labels.html.edit(Some(label), repository)
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(collaboratorsOnly { (form, repository) => 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)) updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) issues.labels.html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt) deleteLabel(repository.owner, repository.name, params("labelId").toInt)
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) Ok()
}) })
/** /**

View File

@@ -62,10 +62,6 @@ trait PullRequestsControllerBase extends ControllerBase {
searchPullRequests(None, repository) searchPullRequests(None, repository)
}) })
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
@@ -453,7 +449,6 @@ trait PullRequestsControllerBase extends ControllerBase {
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName) val sessionKey = Keys.Session.Pulls(owner, repoName)
@@ -463,14 +458,15 @@ trait PullRequestsControllerBase extends ControllerBase {
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
) )
pulls.html.list( issues.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), "pulls",
getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
userName,
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName), (getCollaborators(owner, repoName) :+ owner).sorted,
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName), getMilestones(owner, repoName),
countIssue(condition, Map.empty, true, owner -> repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition, condition,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))

View File

@@ -2,10 +2,8 @@ package app
import service._ import service._
import util.Directory._ import util.Directory._
import util.ControlUtil._
import util.Implicits._ import util.Implicits._
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator} import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
import util.JGitUtil.CommitInfo
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
@@ -13,6 +11,7 @@ import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import util.ControlUtil._ import util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
class RepositorySettingsController extends RepositorySettingsControllerBase class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService with RepositoryService with AccountService with WebHookService
@@ -71,11 +70,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Save the repository options. * Save the repository options.
*/ */
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
saveRepositoryOptions( saveRepositoryOptions(
repository.owner, repository.owner,
repository.name, repository.name,
form.description, form.description,
if(repository.branchList.isEmpty) "master" else form.defaultBranch, defaultBranch,
repository.repository.parentUserName.map { _ => repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate repository.repository.isPrivate
} getOrElse form.isPrivate } getOrElse form.isPrivate
@@ -93,6 +93,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
} }
} }
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
}
flash += "info" -> "Repository settings has been updated." flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
}) })
@@ -131,7 +135,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook page. * Display the web hook page.
*/ */
get("/:owner/:repository/settings/hooks")(ownerOnly { repository => get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info")) settings.html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
}) })
/** /**
@@ -153,7 +157,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** /**
* Send the test request to registered web hook URLs. * Send the test request to registered web hook URLs.
*/ */
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
val commits = git.log val commits = git.log
@@ -161,15 +165,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
.setMaxCount(3) .setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_)) .call.iterator.asScala.map(new CommitInfo(_))
getWebHookURLs(repository.owner, repository.name) match { getAccountByUserName(repository.owner).foreach { ownerAccount =>
case webHookURLs if(webHookURLs.nonEmpty) => callWebHook(repository.owner, repository.name,
for(ownerAccount <- getAccountByUserName(repository.owner)){ List(model.WebHook(repository.owner, repository.name, form.url)),
callWebHook(repository.owner, repository.name, webHookURLs, WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)) )
} }
case _ => flash += "url" -> form.url
}
flash += "info" -> "Test payload deployed!" flash += "info" -> "Test payload deployed!"
} }
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")

View File

@@ -191,6 +191,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
getPathObjectId(git, path, revCommit).map { objectId => getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){ if(raw){
// Download // Download
@@ -200,7 +201,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
} else { } else {
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount)) new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
} }
} getOrElse NotFound } getOrElse NotFound
} }
@@ -311,10 +312,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} else { } else {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
//val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit // get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
// get files // get files
val files = JGitUtil.getFileList(git, revision, path) val files = JGitUtil.getFileList(git, revision, path)
val parentPath = if (path == ".") Nil else path.split("/").toList val parentPath = if (path == ".") Nil else path.split("/").toList
@@ -329,7 +330,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.files(revision, repository, repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount)) files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} }
} getOrElse NotFound } getOrElse NotFound
@@ -350,7 +351,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val builder = DirCache.newInCore.builder() val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter() val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}" val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(s"refs/heads/${branch}") val headTip = git.getRepository.resolve(headName)
JGitUtil.processTree(git, headTip){ (path, tree) => JGitUtil.processTree(git, headTip){ (path, tree) =>
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){ if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
@@ -365,7 +366,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
builder.finish() builder.finish()
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter), val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, message) headName, loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush() inserter.flush()
inserter.release() inserter.release()

View File

@@ -11,6 +11,7 @@ import org.apache.commons.io.FileUtils
import java.io.FileInputStream import java.io.FileInputStream
import plugin.{Plugin, PluginSystem} import plugin.{Plugin, PluginSystem}
import org.scalatra.Ok import org.scalatra.Ok
import util.Implicits._
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator with AccountService with AdminAuthenticator
@@ -84,42 +85,56 @@ trait SystemSettingsControllerBase extends ControllerBase {
}) })
get("/admin/plugins")(adminOnly { get("/admin/plugins")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins val installedPlugins = plugin.PluginSystem.plugins
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable") val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins) admin.plugins.html.installed(installedPlugins, updatablePlugins)
} else NotFound
}) })
post("/admin/plugins/_update", pluginForm)(adminOnly { form => post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds) deletePlugins(form.pluginIds)
installPlugins(form.pluginIds) installPlugins(form.pluginIds)
redirect("/admin/plugins") redirect("/admin/plugins")
} else NotFound
}) })
post("/admin/plugins/_delete", pluginForm)(adminOnly { form => post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds) deletePlugins(form.pluginIds)
redirect("/admin/plugins") redirect("/admin/plugins")
} else NotFound
}) })
get("/admin/plugins/available")(adminOnly { get("/admin/plugins/available")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins val installedPlugins = plugin.PluginSystem.plugins
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available") val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
admin.plugins.html.available(availablePlugins) admin.plugins.html.available(availablePlugins)
} else NotFound
}) })
post("/admin/plugins/_install", pluginForm)(adminOnly { form => post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
installPlugins(form.pluginIds) installPlugins(form.pluginIds)
redirect("/admin/plugins") redirect("/admin/plugins")
} else NotFound
}) })
// get("/admin/plugins/console")(adminOnly { get("/admin/plugins/console")(adminOnly {
// admin.plugins.html.console() if(enablePluginSystem){
// }) admin.plugins.html.console()
// } else NotFound
// post("/admin/plugins/console")(adminOnly { })
// val script = request.getParameter("script")
// val result = plugin.JavaScriptPlugin.evaluateJavaScript(script) post("/admin/plugins/console")(adminOnly {
// Ok(result) if(enablePluginSystem){
// }) val script = request.getParameter("script")
val result = plugin.ScalaPlugin.eval(script)
Ok()
} else NotFound
})
// TODO Move these methods to PluginSystem or Service? // TODO Move these methods to PluginSystem or Service?
private def deletePlugins(pluginIds: List[String]): Unit = { private def deletePlugins(pluginIds: List[String]): Unit = {
@@ -138,9 +153,10 @@ trait SystemSettingsControllerBase extends ControllerBase {
val installedPlugins = plugin.PluginSystem.plugins val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin => getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id) val pluginDir = new java.io.File(PluginHome, plugin.id)
if(!pluginDir.exists){ if(pluginDir.exists){
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir) FileUtils.deleteDirectory(pluginDir)
} }
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
PluginSystem.installPlugin(plugin.id) PluginSystem.installPlugin(plugin.id)
} }
} }

View File

@@ -31,7 +31,7 @@ case class Label(
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000" "000000"
} else { } else {
"FFFFFF" "ffffff"
} }
} }
} }

View File

@@ -0,0 +1,19 @@
package model
trait PluginComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val Plugins = TableQuery[Plugins]
class Plugins(tag: Tag) extends Table[Plugin](tag, "PLUGIN"){
val pluginId = column[String]("PLUGIN_ID", O PrimaryKey)
val version = column[String]("VERSION")
def * = (pluginId, version) <> (Plugin.tupled, Plugin.unapply)
}
}
case class Plugin(
pluginId: String,
version: String
)

View File

@@ -31,7 +31,8 @@ object Profile extends {
with PullRequestComponent with PullRequestComponent
with RepositoryComponent with RepositoryComponent
with SshKeyComponent with SshKeyComponent
with WebHookComponent with Profile { with WebHookComponent
with PluginComponent with Profile {
/** /**
* Returns system date. * Returns system date.

View File

@@ -1,117 +0,0 @@
package plugin
import org.mozilla.javascript.{Context => JsContext}
import org.mozilla.javascript.{Function => JsFunction}
import scala.collection.mutable.ListBuffer
import plugin.PluginSystem._
import util.ControlUtil._
import plugin.PluginSystem.GlobalMenu
import plugin.PluginSystem.RepositoryAction
import plugin.PluginSystem.Action
import plugin.PluginSystem.RepositoryMenu
class JavaScriptPlugin(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]()
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 addRepositoryMenu(label: String, name: String, url: String, icon: String, condition: JsFunction): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, (context) => {
val context = JsContext.enter()
try {
condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean]
} finally {
JsContext.exit()
}
})
}
def addGlobalMenu(label: String, url: String, icon: String, condition: JsFunction): Unit = {
globalMenuList += GlobalMenu(label, url, icon, (context) => {
val context = JsContext.enter()
try {
condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean]
} finally {
JsContext.exit()
}
})
}
def addGlobalAction(path: String, function: JsFunction): Unit = {
globalActionList += Action(path, (request, response) => {
val context = JsContext.enter()
try {
function.call(context, function, function, Array(request, response))
} finally {
JsContext.exit()
}
})
}
def addRepositoryAction(path: String, function: JsFunction): Unit = {
repositoryActionList += RepositoryAction(path, (request, response, repository) => {
val context = JsContext.enter()
try {
function.call(context, function, function, Array(request, response, repository))
} finally {
JsContext.exit()
}
})
}
object db {
// TODO Use JavaScript Map instead of java.util.Map
def select(sql: String): Array[java.util.Map[String, String]] = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
using(stmt.executeQuery()){ rs =>
val list = new java.util.ArrayList[java.util.Map[String, String]]()
while(rs.next){
defining(rs.getMetaData){ meta =>
val map = new java.util.HashMap[String, String]()
Range(1, meta.getColumnCount).map { i =>
val name = meta.getColumnName(i)
map.put(name, rs.getString(name))
}
list.add(map)
}
}
list.toArray(new Array[java.util.Map[String, String]](list.size))
}
}
}
}
}
}
object JavaScriptPlugin {
def define(id: String, version: String, author: String, url: String, description: String)
= new JavaScriptPlugin(id, version, author, url, description)
def evaluateJavaScript(script: String, vars: Map[String, Any] = Map.empty): Any = {
val context = JsContext.enter()
try {
val scope = context.initStandardObjects()
scope.put("PluginSystem", scope, PluginSystem)
scope.put("JavaScriptPlugin", scope, this)
vars.foreach { case (key, value) =>
scope.put(key, scope, value)
}
val result = context.evaluateString(scope, script, "<cmd>", 1, null)
result
} finally {
JsContext.exit
}
}
}

View File

@@ -14,6 +14,7 @@ trait Plugin {
def globalMenus : List[GlobalMenu] def globalMenus : List[GlobalMenu]
def repositoryActions : List[RepositoryAction] def repositoryActions : List[RepositoryAction]
def globalActions : List[Action] def globalActions : List[Action]
def javaScripts : List[JavaScript]
} }
object PluginConnectionHolder { object PluginConnectionHolder {

View File

@@ -1,20 +1,24 @@
package plugin package plugin
import app.Context
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import org.apache.commons.io.FileUtils import org.apache.commons.io.{IOUtils, FileUtils}
import util.JGitUtil import Security._
import org.eclipse.jgit.api.Git import service.PluginService
import model.Profile._
import profile.simple._
import java.io.FileInputStream
import java.sql.Connection
import app.Context
import service.RepositoryService.RepositoryInfo import service.RepositoryService.RepositoryInfo
/** /**
* Provides extension points to plug-ins. * Provides extension points to plug-ins.
*/ */
object PluginSystem { object PluginSystem extends PluginService {
private val logger = LoggerFactory.getLogger(PluginSystem.getClass) private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
@@ -28,8 +32,21 @@ object PluginSystem {
def plugins: List[Plugin] = pluginsMap.values.toList def plugins: List[Plugin] = pluginsMap.values.toList
def uninstall(id: String): Unit = { def uninstall(id: String)(implicit session: Session): Unit = {
pluginsMap.remove(id) 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 def repositories: List[PluginRepository] = repositoriesList.toList
@@ -37,7 +54,7 @@ object PluginSystem {
/** /**
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins. * Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
*/ */
def init(): Unit = { def init()(implicit session: Session): Unit = {
if(initialized.compareAndSet(false, true)){ if(initialized.compareAndSet(false, true)){
// Load installed plugins // Load installed plugins
val pluginDir = new java.io.File(PluginHome) val pluginDir = new java.io.File(PluginHome)
@@ -52,42 +69,107 @@ object PluginSystem {
} }
// TODO Method name seems to not so good. // TODO Method name seems to not so good.
def installPlugin(id: String): Unit = { def installPlugin(id: String)(implicit session: Session): Unit = {
val pluginDir = new java.io.File(PluginHome) val pluginHome = new java.io.File(PluginHome)
val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js") val pluginDir = new java.io.File(pluginHome, id)
if(javaScriptFile.exists && javaScriptFile.isFile){ val scalaFile = new java.io.File(pluginDir, "plugin.scala")
if(scalaFile.exists && scalaFile.isFile){
val properties = new java.util.Properties() val properties = new java.util.Properties()
using(new java.io.FileInputStream(new java.io.File(pluginDir, id + "/plugin.properties"))){ in => using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
properties.load(in) properties.load(in)
} }
val script = FileUtils.readFileToString(javaScriptFile, "UTF-8") 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 { try {
JavaScriptPlugin.evaluateJavaScript(script, Map( // Compile and eval Scala source code
"id" -> properties.getProperty("id"), ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
"version" -> properties.getProperty("version"), ScalaPlugin.compileTemplate(
"author" -> properties.getProperty("author"), id.replaceAll("-", ""),
"url" -> properties.getProperty("url"), file.getName.replaceAll("\\.scala\\.html$", ""),
"description" -> properties.getProperty("description") 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 { } catch {
case e: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e) 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 repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).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 classes to hold plug-ins information internally in GitBucket
case class PluginRepository(id: String, url: String) case class PluginRepository(id: String, url: String)
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean) 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 RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any) case class Action(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any)
case class RepositoryAction(path: String, function: (HttpServletRequest, HttpServletResponse, RepositoryInfo) => 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. * Checks whether the plugin is updatable.
@@ -109,17 +191,4 @@ object PluginSystem {
} }
} }
// TODO This is a test
// addGlobalMenu("Google", "http://www.google.co.jp/", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
// { context => context.loginAccount.isDefined }
//
// addRepositoryMenu("Board", "board", "/board", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
// { context => true}
//
// addGlobalAction("/hello"){ (request, response) =>
// "Hello World!"
// }
} }

View File

@@ -1,10 +1,16 @@
package plugin package plugin
import app.Context
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import plugin.PluginSystem._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import app.Context
import plugin.PluginSystem._
import plugin.PluginSystem.RepositoryMenu
import plugin.Security._
import service.RepositoryService.RepositoryInfo 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. // TODO This is a sample implementation for Scala based plug-ins.
class ScalaPlugin(val id: String, val version: String, class ScalaPlugin(val id: String, val version: String,
@@ -14,11 +20,13 @@ class ScalaPlugin(val id: String, val version: String,
private val globalMenuList = ListBuffer[GlobalMenu]() private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[RepositoryAction]() private val repositoryActionList = ListBuffer[RepositoryAction]()
private val globalActionList = ListBuffer[Action]() private val globalActionList = ListBuffer[Action]()
private val javaScriptList = ListBuffer[JavaScript]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.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 = { def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition) repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
@@ -28,12 +36,42 @@ class ScalaPlugin(val id: String, val version: String,
globalMenuList += GlobalMenu(label, url, icon, condition) globalMenuList += GlobalMenu(label, url, icon, condition)
} }
def addGlobalAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = { def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
globalActionList += Action(path, function) globalActionList += Action(method, path, security, function)
} }
def addRepositoryAction(path: String)(function: (HttpServletRequest, HttpServletResponse, RepositoryInfo) => Any): Unit = { def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = {
repositoryActionList += RepositoryAction(path, function) 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

@@ -0,0 +1,36 @@
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
}

View File

@@ -0,0 +1,56 @@
import java.sql.PreparedStatement
import play.twirl.api.Html
import util.ControlUtil._
import scala.collection.mutable.ListBuffer
package object plugin {
case class Redirect(path: String)
case class Fragment(html: Html)
case class RawData(contentType: String, content: Array[Byte])
object db {
// TODO labelled place holder support
def select(sql: String, params: Any*): Seq[Map[String, String]] = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
setParams(stmt, params: _*)
using(stmt.executeQuery()){ rs =>
val list = new ListBuffer[Map[String, String]]()
while(rs.next){
defining(rs.getMetaData){ meta =>
val map = Range(1, meta.getColumnCount + 1).map { i =>
val name = meta.getColumnName(i)
(name, rs.getString(name))
}.toMap
list += map
}
}
list
}
}
}
}
// TODO labelled place holder support
def update(sql: String, params: Any*): Int = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
setParams(stmt, params: _*)
stmt.executeUpdate()
}
}
}
private def setParams(stmt: PreparedStatement, params: Any*): Unit = {
params.zipWithIndex.foreach { case (p, i) =>
p match {
case x: String => stmt.setString(i + 1, x)
case x: Int => stmt.setInt(i + 1, x)
case x: Boolean => stmt.setBoolean(i + 1, x)
}
}
}
}
}

View File

@@ -43,14 +43,13 @@ trait IssuesService {
* Returns the count of the search result against issues. * Returns the count of the search result against issues.
* *
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name * @param repos Tuple of the repository owner and the repository name
* @return the count of the search result * @return the count of the search result
*/ */
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean, repos: (String, String)*)
repos: (String, String)*)(implicit s: Session): Int = (implicit s: Session): Int =
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first
/** /**
* Returns the Map which contains issue count for each labels. * Returns the Map which contains issue count for each labels.
@@ -58,13 +57,12 @@ trait IssuesService {
* @param owner the repository owner * @param owner the repository owner
* @param repository the repository name * @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @return the Map which contains issue count for each labels (key is label name, value is issue count) * @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/ */
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, def countIssueGroupByLabels(owner: String, repository: String,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { condition: IssueSearchCondition)(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false) searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
.innerJoin(IssueLabels).on { (t1, t2) => .innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
} }
@@ -84,15 +82,14 @@ trait IssuesService {
* If the issue does not exist, its repository is not included in the result. * If the issue does not exist, its repository is not included in the result.
* *
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name * @param repos Tuple of the repository owner and the repository name
* @return list which contains issue count for each repository * @return list which contains issue count for each repository
*/ */
def countIssueGroupByRepository( def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, condition: IssueSearchCondition, onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) searchIssueQuery(repos, condition.copy(repo = None), onlyPullRequest)
.groupBy { t => .groupBy { t =>
t.userName -> t.repositoryName t.userName -> t.repositoryName
} }
@@ -107,19 +104,18 @@ trait IssuesService {
* Returns the search result against issues. * Returns the search result against issues.
* *
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) * @param pullRequest if true then returns only pull requests, false then returns only issues.
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param offset the offset for pagination * @param offset the offset for pagination
* @param limit the limit for pagination * @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name * @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count) * @return the search result (list of tuples which contain issue, labels and comment count)
*/ */
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean,
offset: Int, limit: Int, repos: (String, String)*) offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, List[Label], Int)] = { (implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser, onlyPullRequest) searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) => .sortBy { case (t1, t2) =>
(condition.sort match { (condition.sort match {
@@ -136,8 +132,9 @@ trait IssuesService {
.drop(offset).take(limit) .drop(offset).take(limit)
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.map { case (((t1, t2), t3), t4) => .leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) .map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
} }
.list .list
.splitWith { (c1, c2) => .splitWith { (c1, c2) =>
@@ -146,11 +143,12 @@ trait IssuesService {
c1._1.issueId == c2._1.issueId c1._1.issueId == c2._1.issueId
} }
.map { issues => issues.head match { .map { issues => issues.head match {
case (issue, commentCount, _,_,_) => case (issue, commentCount, _, _, _, milestone) =>
(issue, IssueInfo(issue,
issues.flatMap { t => t._3.map ( issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList, )} toList,
milestone,
commentCount) commentCount)
}} toList }} toList
} }
@@ -159,7 +157,7 @@ trait IssuesService {
* Assembles query for conditional issue searching. * Assembles query for conditional issue searching.
*/ */
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) = pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 => Issues filter { t1 =>
condition.repo condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
@@ -169,10 +167,9 @@ trait IssuesService {
(t1.closed === (condition.state == "closed").bind) && (t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && (t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) && (t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
(t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) && (t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) && (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && (t1.pullRequest === pullRequest.bind) &&
(t1.pullRequest === true.bind, onlyPullRequest) &&
(IssueLabels filter { t2 => (IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in (t2.labelId in
@@ -337,11 +334,20 @@ object IssuesService {
case class IssueSearchCondition( case class IssueSearchCondition(
labels: Set[String] = Set.empty, labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None, milestoneId: Option[Option[Int]] = None,
author: Option[String] = None,
assigned: Option[String] = None,
repo: Option[String] = None, repo: Option[String] = None,
state: String = "open", state: String = "open",
sort: String = "created", sort: String = "created",
direction: String = "desc"){ direction: String = "desc"){
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
state == "open" && sort == "created" && direction == "desc"
}
def nonEmpty: Boolean = !isEmpty
def toURL: String = def toURL: String =
"?" + List( "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
@@ -349,6 +355,8 @@ object IssuesService {
case Some(x) => x.toString case Some(x) => x.toString
case None => "none" case None => "none"
})}, })},
author .map(x => "author=" + urlEncode(x)),
assigned.map(x => "assigned=" + urlEncode(x)),
repo.map("for=" + urlEncode(_)), repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)), Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)), Some("sort=" + urlEncode(sort)),
@@ -370,6 +378,8 @@ object IssuesService {
case "none" => None case "none" => None
case x => x.toIntOpt case x => x.toIntOpt
}, },
param(request, "author"),
param(request, "assigned"),
param(request, "for"), param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
@@ -383,4 +393,6 @@ object IssuesService {
} }
} }
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int)
} }

View File

@@ -12,8 +12,8 @@ trait LabelsService {
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] = def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit = def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
Labels insert Label( Labels returning Labels.map(_.labelId) += Label(
userName = owner, userName = owner,
repositoryName = repository, repositoryName = repository,
labelName = labelName, labelName = labelName,

View File

@@ -0,0 +1,24 @@
package service
import model.Profile._
import profile.simple._
import model.Plugin
trait PluginService {
def getPlugins()(implicit s: Session): List[Plugin] =
Plugins.sortBy(_.pluginId).list
def registerPlugin(plugin: Plugin)(implicit s: Session): Unit =
Plugins.insert(plugin)
def updatePlugin(plugin: Plugin)(implicit s: Session): Unit =
Plugins.filter(_.pluginId === plugin.pluginId.bind).map(_.version).update(plugin.version)
def deletePlugin(pluginId: String)(implicit s: Session): Unit =
Plugins.filter(_.pluginId === pluginId.bind).delete
def getPlugin(pluginId: String)(implicit s: Session): Option[Plugin] =
Plugins.filter(_.pluginId === pluginId.bind).firstOption
}

View File

@@ -36,6 +36,24 @@ trait PullRequestService { self: IssuesService =>
.list .list
.map { x => PullRequestCount(x._1, x._2) } .map { x => PullRequestCount(x._1, x._2) }
def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] =
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) }
.filter { case ((t1, t2), t3) =>
(t2.closed === closed.bind) &&
(
(t3.isPrivate === false.bind) ||
(t3.userName === userName.bind) ||
(Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists)
)
}
.groupBy { case ((t1, t2), t3) => t2.openedUserName }
.map { case (userName, t) => userName -> t.length }
.sortBy(_._2 desc)
.list
.map { x => PullRequestCount(x._1, x._2) }
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit = commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit =

View File

@@ -166,8 +166,19 @@ trait RepositoryService { self: AccountService =>
} }
} }
def getAllRepositories()(implicit s: Session): List[(String, String)] = { /**
Repositories.sortBy(_.lastActivityDate desc).map{ t => * Returns the repositories without private repository that user does not have access right.
* Include public repository, private own repository and private but collaborator repository.
*
* @param userName the user name of collaborator
* @return the repository infomation list
*/
def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = {
Repositories.filter { t1 =>
(t1.isPrivate === false.bind) ||
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).map{ t =>
(t.userName, t.repositoryName) (t.userName, t.repositoryName)
}.list }.list
} }

View File

@@ -191,4 +191,7 @@ object SystemSettingsService {
else value else value
} }
// TODO temporary flag
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
} }

View File

@@ -182,7 +182,8 @@ trait WikiService {
} }
builder.finish() builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer.fullName, committer.mailAddress,
pageName match { pageName match {
case Some(x) => s"Revert ${from} ... ${to} on ${x}" case Some(x) => s"Revert ${from} ... ${to} on ${x}"
case None => s"Revert ${from} ... ${to}" case None => s"Revert ${from} ... ${to}"
@@ -229,7 +230,8 @@ trait WikiService {
if(created || updated || removed){ if(created || updated || removed){
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish() builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer.fullName, committer.mailAddress,
if(message.trim.length == 0) { if(message.trim.length == 0) {
if(removed){ if(removed){
s"Rename ${currentPageName} to ${newPageName}" s"Rename ${currentPageName} to ${newPageName}"
@@ -269,7 +271,8 @@ trait WikiService {
} }
if(removed){ if(removed){
builder.finish() builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer, mailAddress, message)
} }
} }
} }

View File

@@ -11,6 +11,7 @@ import util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory import util.Directory
import plugin.PluginUpdateJob import plugin.PluginUpdateJob
import service.SystemSettingsService
object AutoUpdate { object AutoUpdate {
@@ -52,6 +53,28 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection): Unit = {
super.update(conn)
using(conn.createStatement.executeQuery("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'")){ rs =>
while(rs.next) {
val info = rs.getString("ADDITIONAL_INFO")
val newInfo = info.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
if (info != newInfo) {
val id = rs.getString("ACTIVITY_ID")
using(conn.prepareStatement("UPDATE ACTIVITY SET ADDITIONAL_INFO=? WHERE ACTIVITY_ID=?")) { sql =>
sql.setString(1, newInfo)
sql.setLong(2, id.toLong)
sql.executeUpdate
}
}
}
}
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
FileUtils.deleteDirectory(new File(Directory.PluginHome))
}
},
new Version(2, 2), new Version(2, 2),
new Version(2, 1), new Version(2, 1),
new Version(2, 0){ new Version(2, 0){
@@ -147,24 +170,23 @@ object AutoUpdate {
*/ */
class AutoUpdateListener extends ServletContextListener { class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory import org.quartz.impl.StdSchedulerFactory
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
import AutoUpdate._ import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private val scheduler = StdSchedulerFactory.getDefaultScheduler private val scheduler = StdSchedulerFactory.getDefaultScheduler
override def contextInitialized(event: ServletContextEvent): Unit = { override def contextInitialized(event: ServletContextEvent): Unit = {
val datadir = event.getServletContext.getInitParameter("gitbucket.home") val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if(datadir != null){ if(dataDir != null){
System.setProperty("gitbucket.home", datadir) System.setProperty("gitbucket.home", dataDir)
} }
org.h2.Driver.load() org.h2.Driver.load()
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
logger.debug("Start schema update") val context = event.getServletContext
context.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
defining(getConnection(event.getServletContext)){ conn => defining(getConnection(event.getServletContext)){ conn =>
logger.debug("Start schema update")
try { try {
defining(getCurrentVersion()){ currentVersion => defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){ if(currentVersion == headVersion){
@@ -174,7 +196,6 @@ class AutoUpdateListener extends ServletContextListener {
} else { } else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit()
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
} }
} }
@@ -185,10 +206,13 @@ class AutoUpdateListener extends ServletContextListener {
conn.rollback() conn.rollback()
} }
} }
}
logger.debug("End schema update") logger.debug("End schema update")
}
if(SystemSettingsService.enablePluginSystem){
getDatabase(context).withSession { implicit session =>
logger.debug("Starting plugin system...") logger.debug("Starting plugin system...")
try {
plugin.PluginSystem.init() plugin.PluginSystem.init()
scheduler.start() scheduler.start()
@@ -196,6 +220,15 @@ class AutoUpdateListener extends ServletContextListener {
logger.debug("PluginUpdateJob is started.") logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.") 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 = { def contextDestroyed(sce: ServletContextEvent): Unit = {
@@ -208,4 +241,10 @@ class AutoUpdateListener extends ServletContextListener {
servletContext.getInitParameter("db.user"), servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password")) 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"))
} }

View File

@@ -134,8 +134,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// Retrieve all issue count in the repository // Retrieve all issue count in the repository
val issueCount = val issueCount =
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) + countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository) countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch

View File

@@ -3,13 +3,13 @@ package servlet
import javax.servlet._ import javax.servlet._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import twirl.api.Html import play.twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService} import service.{AccountService, RepositoryService, SystemSettingsService}
import model.{Account, Session} import model.{Account, Session}
import util.{JGitUtil, Keys} import util.{JGitUtil, Keys}
import plugin.PluginConnectionHolder import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
import service.RepositoryService.RepositoryInfo import service.RepositoryService.RepositoryInfo
import service.SystemSettingsService.SystemSettings import plugin.Security._
class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService { class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
@@ -21,7 +21,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
(req, res) match { (req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => { case (request: HttpServletRequest, response: HttpServletResponse) => {
Database(req.getServletContext) withTransaction { implicit session => Database(req.getServletContext) withTransaction { implicit session =>
val path = req.asInstanceOf[HttpServletRequest].getRequestURI val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){ if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res) chain.doFilter(req, res)
} }
@@ -30,18 +30,25 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} }
} }
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse): Boolean = { private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
plugin.PluginSystem.globalActions.find(_.path == path).map { action => (implicit session: Session): Boolean = {
val result = action.function(request, response) plugin.PluginSystem.globalActions.find(x =>
x.method.toLowerCase == request.getMethod.toLowerCase && path.matches(x.path)
).map { action =>
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings() val systemSettings = loadSystemSettings()
result match { implicit val context = app.Context(systemSettings, Option(loginAccount), request)
case x: String => renderGlobalHtml(request, response, systemSettings, x)
case x: org.mozilla.javascript.NativeObject => { if(authenticate(action.security, context)){
x.get("format") match { val result = try {
case "html" => renderGlobalHtml(request, response, systemSettings, x.get("body").toString) PluginConnectionHolder.threadLocal.set(session.conn)
case "json" => renderJson(request, response, x.get("body").toString) action.function(request, response, context)
} } finally {
PluginConnectionHolder.threadLocal.remove()
} }
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
} }
true true
} getOrElse false } getOrElse false
@@ -54,23 +61,23 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
val owner = elements(1) val owner = elements(1)
val name = elements(2) val name = elements(2)
val remain = elements.drop(3).mkString("/", "/", "") val remain = elements.drop(3).mkString("/", "/", "")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings() val systemSettings = loadSystemSettings()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository => getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository =>
plugin.PluginSystem.repositoryActions.find(_.path == remain).map { action => plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action =>
if(authenticate(action.security, context, repository)){
val result = try { val result = try {
PluginConnectionHolder.threadLocal.set(session.conn) PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, repository) action.function(request, response, context, repository)
} finally { } finally {
PluginConnectionHolder.threadLocal.remove() PluginConnectionHolder.threadLocal.remove()
} }
result match { processActionResult(result, request, response, context)
case x: String => renderRepositoryHtml(request, response, systemSettings, repository, x) } else {
case x: org.mozilla.javascript.NativeObject => { // TODO NotFound or Error?
x.get("format") match {
case "html" => renderRepositoryHtml(request, response, systemSettings, repository, x.get("body").toString)
case "json" => renderJson(request, response, x.get("body").toString)
}
}
} }
true true
} }
@@ -78,27 +85,108 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} else false } else false
} }
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
systemSettings: SystemSettings, body: String): Unit = { context: app.Context): Unit = {
result match {
case null|None => renderError(request, response, context, 404)
case x: String => renderGlobalHtml(request, response, context, x)
case Some(x: String) => renderGlobalHtml(request, response, context, x)
case x: Html => renderGlobalHtml(request, response, context, x.toString)
case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString)
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString)
case x: RawData => renderRawData(request, response, context, x)
case Some(x: RawData) => renderRawData(request, response, context, x)
case x: Redirect => response.sendRedirect(x.path)
case Some(x: Redirect) => response.sendRedirect(x.path)
case x: AnyRef => renderJson(request, response, x)
}
}
/**
* Authentication for global action
*/
private def authenticate(security: Security, context: app.Context)(implicit session: Session): Boolean = {
// Global Action
security match {
case All() => true
case Login() => context.loginAccount.isDefined
case Admin() => context.loginAccount.exists(_.isAdmin)
case _ => false // TODO throw Exception?
}
}
/**
* Authenticate for repository action
*/
private def authenticate(security: Security, context: app.Context, repository: RepositoryInfo)(implicit session: Session): Boolean = {
if(repository.repository.isPrivate){
// Private Repository
security match {
case Admin() => context.loginAccount.exists(_.isAdmin)
case Owner() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
}
case _ => context.loginAccount.exists { account =>
account.isAdmin || account.userName == repository.owner ||
getCollaborators(repository.owner, repository.name).contains(account.userName)
}
}
} else {
// Public Repository
security match {
case All() => true
case Login() => context.loginAccount.isDefined
case Owner() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
}
case Member() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getCollaborators(repository.owner, repository.name).contains(account.userName)
}
case Admin() => context.loginAccount.exists(_.isAdmin)
}
}
}
private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = {
response.sendError(error)
}
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8") response.setContentType("text/html; charset=UTF-8")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] val html = _root_.html.main("GitBucket", None)(Html(body))(context)
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
val html = _root_.html.main("GitBucket", None)(Html(body))
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream) IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
} }
private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = {
systemSettings: SystemSettings, repository: RepositoryInfo, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8") response.setContentType("text/html; charset=UTF-8")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))) // TODO specify active side menu
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream) IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
} }
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, body: String): Unit = { private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("application/json; charset=UTF-8") response.setContentType("text/html; charset=UTF-8")
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream) IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
} }
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
response.setContentType(rawData.contentType)
IOUtils.write(rawData.content, response.getOutputStream)
}
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write
implicit val formats = Serialization.formats(NoTypeHints)
val json = write(obj)
response.setContentType("application/json; charset=UTF-8")
IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream)
}
} }

View File

@@ -4,6 +4,7 @@ import org.eclipse.jgit.api.Git
import util.Directory._ import util.Directory._
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import scala.annotation.tailrec
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._ import org.eclipse.jgit.revwalk._
@@ -77,8 +78,8 @@ object JGitUtil {
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this( def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
rev.getName, rev.getName,
rev.getFullMessage,
rev.getShortMessage, rev.getShortMessage,
rev.getFullMessage,
rev.getParents().map(_.name).toList, rev.getParents().map(_.name).toList,
rev.getAuthorIdent.getWhen, rev.getAuthorIdent.getWhen,
rev.getAuthorIdent.getName, rev.getAuthorIdent.getName,
@@ -190,38 +191,23 @@ object JGitUtil {
* @return HTML of the file list * @return HTML of the file list
*/ */
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new RevWalk(git.getRepository)){ revWalk => using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision) val objectId = git.getRepository.resolve(revision)
val revCommit = revWalk.parseCommit(objectId) val revCommit = revWalk.parseCommit(objectId)
using(new TreeWalk(git.getRepository)){ treeWalk => val treeWalk = if (path == ".") {
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
if(path != "."){ treeWalk
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
var stopRecursive = false
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf('/') < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else { } else {
false val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
} treeWalk.enterSubtree()
treeWalk
} }
def shouldBeRecursive(): Boolean = !stopRecursive using(treeWalk) { treeWalk =>
override def clone: TreeFilter = return this
})
}
while (treeWalk.next()) { while (treeWalk.next()) {
// submodule // submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
@@ -230,6 +216,31 @@ object JGitUtil {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
} }
list = list.map(tuple =>
if (tuple._2 != FileMode.TREE)
tuple
else
simplifyPath(tuple)
)
@tailrec
def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new TreeWalk(git.getRepository)) { walk =>
walk.addTree(tuple._1)
while (walk.next() && list.size < 2) {
val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url)
} else None
list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl))
}
}
if (list.size != 1 || list.exists(_._2 != FileMode.TREE))
tuple
else
simplifyPath(list(0))
}
} }
} }
@@ -504,7 +515,7 @@ object JGitUtil {
} }
def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId, def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
fullName: String, mailAddress: String, message: String): ObjectId = { ref: String, fullName: String, mailAddress: String, message: String): ObjectId = {
val newCommit = new CommitBuilder() val newCommit = new CommitBuilder()
newCommit.setCommitter(new PersonIdent(fullName, mailAddress)) newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
newCommit.setAuthor(new PersonIdent(fullName, mailAddress)) newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
@@ -518,7 +529,7 @@ object JGitUtil {
inserter.flush() inserter.flush()
inserter.release() inserter.release()
val refUpdate = git.getRepository.updateRef(Constants.HEAD) val refUpdate = git.getRepository.updateRef(ref)
refUpdate.setNewObjectId(newHeadId) refUpdate.setNewObjectId(newHeadId)
refUpdate.update() refUpdate.update()
@@ -652,4 +663,15 @@ object JGitUtil {
}.head.id }.head.id
} }
/**
* Returns the last modified commit of specified path
* @param git the Git object
* @param startCommit the search base commit id
* @param path the path of target file or directory
* @return the last modified commit of specified path
*/
def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = {
return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
}
} }

View File

@@ -1,7 +1,7 @@
package view package view
import service.RequestCache import service.RequestCache
import twirl.api.Html import play.twirl.api.Html
import util.StringUtil import util.StringUtil
trait AvatarImageProvider { self: RequestCache => trait AvatarImageProvider { self: RequestCache =>

View File

@@ -88,9 +88,10 @@ class GitBucketHtmlSerializer(
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache { ) with LinkConverter with RequestCache {
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = override protected def printImageTag(imageNode: SuperNode, url: String): Unit = {
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url)).print("\">") printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url, true)).print("\">")
.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>") .print("<img src=\"").print(fixUrl(url, true)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
}
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = { override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
printer.print('<').print('a') printer.print('<').print('a')
@@ -101,9 +102,21 @@ class GitBucketHtmlSerializer(
printer.print('>').print(rendering.text).print("</a>") printer.print('>').print(rendering.text).print("</a>")
} }
private def fixUrl(url: String): String = { private def fixUrl(url: String, isImage: Boolean = false): String = {
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){ if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
url url
} else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "")
} else if(context.currentPath.contains("/tree/")){
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
} else {
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
}
} else { } else {
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
} }

View File

@@ -1,7 +1,7 @@
package view package view
import java.util.Date import java.util.{Date, TimeZone}
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import twirl.api.Html import play.twirl.api.Html
import util.StringUtil import util.StringUtil
import service.RequestCache import service.RequestCache
@@ -18,7 +18,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/** /**
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'". * Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
*/ */
def datetimeRFC3339(date: Date): String = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'").format(date).replaceAll("(\\d\\d)(\\d\\d)$","$1:$2") def datetimeRFC3339(date: Date): String = {
val sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
sf.setTimeZone(TimeZone.getTimeZone("UTC"))
sf.format(date)
}
/** /**
* Format java.util.Date to "yyyy-MM-dd". * Format java.util.Date to "yyyy-MM-dd".

View File

@@ -11,9 +11,11 @@
<li@if(active=="system"){ class="active"}> <li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a> <a href="@path/admin/system">System Settings</a>
</li> </li>
@if(service.SystemSettingsService.enablePluginSystem){
<li@if(active=="plugins"){ class="active"}> <li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a> <a href="@path/admin/plugins">Plugins</a>
</li> </li>
}
<li> <li>
<a href="@path/console/login.jsp">H2 Console</a> <a href="@path/console/login.jsp">H2 Console</a>
</li> </li>

View File

@@ -1,4 +1,4 @@
@(listparts: twirl.api.Html, @(listparts: play.twirl.api.Html,
allCount: Int, allCount: Int,
assignedCount: Int, assignedCount: Int,
createdByCount: Int, createdByCount: Int,

View File

@@ -0,0 +1,184 @@
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
collaborators: List[String] = Nil,
milestones: List[model.Milestone] = Nil,
labels: List[model.Label] = Nil,
repository: Option[service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
</a>
}
@if(condition.repo.isDefined){
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</div>
<div class="btn-group">
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
@helper.html.dropdown(
value = (condition.sort, condition.direction) match {
case ("created" , "desc") => "Newest"
case ("created" , "asc" ) => "Oldest"
case ("comments", "desc") => "Most commented"
case ("comments", "asc" ) => "Least commented"
case ("updated" , "desc") => "Recently updated"
case ("updated" , "asc" ) => "Least recently updated"
},
prefix = "Sort",
mini = false
){
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
}
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
</div>
@helper.html.dropdown("Label") {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Assignee") {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
@helper.html.dropdown("Milestone") {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.map { milestone =>
<li>
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
} else {
<span class="muted">Due by @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</td>
</tr>
}
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td>
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
}
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
#@issue.issueId
</span>
<div class="small muted" style="margin-left: 20px;">
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
@if(hasWritePermission){
</label>
}
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>
</div>

View File

@@ -1,4 +1,4 @@
@(listparts: twirl.api.Html, @(listparts: play.twirl.api.Html,
counts: List[service.PullRequestService.PullRequestCount], counts: List[service.PullRequestService.PullRequestCount],
repositories: List[(String, String, Int)], repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,

View File

@@ -1,4 +1,4 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(issues: List[service.IssuesService.IssueInfo],
page: Int, page: Int,
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
@@ -7,6 +7,7 @@
hasWritePermission: Boolean)(implicit context: app.Context) hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9"> <div class="span9">
@repository.map { repository => @repository.map { repository =>
@if(hasWritePermission){ @if(hasWritePermission){
@@ -71,7 +72,7 @@
</td> </td>
</tr> </tr>
} }
@issues.map { case (issue, labels, commentCount) => @issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr> <tr>
<td> <td>
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/> <img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>

View File

@@ -6,6 +6,24 @@
<script> <script>
// copy to clipboard // copy to clipboard
(function() { (function() {
// Check flash availablibity
var flashAvailable = false;
try {
var flashObject = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
if(flashObject) flashAvailable = true;
} catch (e) {
if (navigator.mimeTypes
&& navigator.mimeTypes['application/x-shockwave-flash'] != undefined
&& navigator.mimeTypes['application/x-shockwave-flash'].enabledPlugin) {
flashAvailable = true;
}
}
// if flash is not available, remove the copy button.
if(!flashAvailable) {
$('#@id').remove();
return
}
// Find ZeroClipboard.swf file URI from ZeroClipboard JavaScript file path. // Find ZeroClipboard.swf file URI from ZeroClipboard JavaScript file path.
// NOTE(tanacasino) I think this way is wrong... but i don't know correct way. // NOTE(tanacasino) I think this way is wrong... but i don't know correct way.
var moviePath = (function() { var moviePath = (function() {

View File

@@ -9,9 +9,12 @@
@if(showIndex){ @if(showIndex){
<div> <div>
<div class="pull-right" style="margin-bottom: 10px;"> <div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/> <div class="btn-group" data-toggle="buttons-radio">
<input type="button" id="btn-unified" class="btn btn-default btn-small active" value="Unified">
<input type="button" id="btn-split" class="btn btn-default btn-small" value="Split">
</div> </div>
Showing @diffs.size changed @plural(diffs.size, "file") </div>
Showing <a href="javascript:void(0);" id="toggle-file-list">@diffs.size changed @plural(diffs.size, "file")</a>
</div> </div>
<ul id="commit-file-list" style="display: none;"> <ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
@@ -38,7 +41,7 @@
<a name="diff-@i"></a> <a name="diff-@i"></a>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th style="font-weight: normal;" class="box-header"> <th style="font-weight: normal; line-height: 27px;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@diff.oldPath -> @diff.newPath @diff.oldPath -> @diff.newPath
@if(newCommitId.isDefined){ @if(newCommitId.isDefined){
@@ -66,7 +69,7 @@
</th> </th>
</tr> </tr>
<tr> <tr>
<td> <td style="padding: 0;">
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i"></div> <div id="diffText-@i"></div>
<textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea> <textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea>
@@ -94,10 +97,25 @@ $(function(){
}); });
} }
// Render diffs as unified mode initially
renderDiffs(1);
$('#btn-unified').click(function(){
$('.container-wide').removeClass('container-wide').addClass('container');
renderDiffs(1);
});
$('#btn-split').click(function(){
$('.container').removeClass('container').addClass('container-wide');
renderDiffs(0);
});
function renderDiffs(viewType){
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){ if($('#oldText-@i').length > 0){
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i'); diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i', viewType);
}
} }
} }
} }

View File

@@ -1,6 +1,13 @@
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html) @(value : String = "",
<div class="btn-group"@if(style.nonEmpty){ style="@style"}> prefix: String = "",
<button class="btn dropdown-toggle@if(mini){ btn-mini} else { btn-small}" data-toggle="dropdown"> mini : Boolean = true,
style : String = "",
right : Boolean = false,
flat : Boolean = false)(body: Html)
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
<button
@if(flat){style="border: none; background-color: #eee;"}
class="dropdown-toggle @if(!flat){btn} else {flat} @if(mini){btn-mini} else {btn-small}" data-toggle="dropdown">
@if(value.isEmpty){ @if(value.isEmpty){
<i class="icon-cog"></i> <i class="icon-cog"></i>
} else { } else {

View File

@@ -5,6 +5,7 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<hr/><br/>
<form method="POST" validate="true"> <form method="POST" validate="true">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box"> <div class="box issue-comment-box">

View File

@@ -5,20 +5,36 @@
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context) pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-comment-box">
<div class="box-header-small">
@user(issue.openedUserName, styleClass="username strong") <span class="muted">commented on @datetime(issue.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.issueId"><i class="icon-pencil"></i></a>
}
</span>
</div>
<div class="box-content issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description provided.", repository, false, true)
</div>
</div>
@comments.map { comment => @comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){ @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div> <div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId"> <div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small"> <div class="box-header-small">
<i class="icon-comment"></i>
@user(comment.commentedUserName, styleClass="username strong") @user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
@if(comment.action == "comment"){ @if(comment.action == "comment"){
commented commented
} else { } else {
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
} }
on @datetime(comment.registeredDate)
</span>
<span class="pull-right"> <span class="pull-right">
@datetime(comment.registeredDate)
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp; <a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
@@ -87,12 +103,21 @@
$(function(){ $(function(){
$('i.icon-pencil').click(function(){ $('i.icon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id'); var id = $(this).closest('a').data('comment-id');
$.get('@url(repository)/issue_comments/_data/' + id, var url = '@url(repository)/issue_comments/_data/' + id;
var $content = $('#commentContent-' + id);
if(!id){
id = $(this).closest('a').data('issue-id');
url = '@url(repository)/issues/_data/' + id;
$content = $('#issueContent');
}
$.get(url,
{ {
dataType : 'html' dataType : 'html'
}, },
function(data){ function(data){
$('#commentContent-' + id).empty().html(data); $content.empty().html(data);
}); });
return false; return false;
}); });

View File

@@ -7,7 +7,8 @@
@import view.helpers._ @import view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("", true, repository) @tab("issues", false, repository)
<br/><br/><hr style="margin-bottom: 10px;">
<form action="@url(repository)/issues/new" method="POST" validate="true"> <form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span9"> <div class="span9">
@@ -32,7 +33,7 @@
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/> <input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown() { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li> <li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone => @milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li> <li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title"> <a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@@ -40,9 +41,9 @@
<div class="small" style="padding-left: 20px;"> <div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate) <img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
} else { } else {
<span class="muted">Due in @date(dueDate)</span> <span class="muted">Due by @date(dueDate)</span>
} }
}.getOrElse { }.getOrElse {
<span class="muted">No due date</span> <span class="muted">No due date</span>
@@ -65,7 +66,7 @@
</div> </div>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){
<span class="strong">Add Labels</span> <span class="strong">Labels</span>
<div> <div>
<div id="label-list"> <div id="label-list">
<ul class="label-list nav nav-pills nav-stacked"> <ul class="label-list nav nav-pills nav-stacked">
@@ -112,7 +113,7 @@ $(function(){
if(milestoneId == ''){ if(milestoneId == ''){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title))); $('#label-milestone').html($('<span class="strong">').text(title));
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok'); $('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
} }
$('input[name=milestoneId]').val(milestoneId); $('input[name=milestoneId]').val(milestoneId);

View File

@@ -5,8 +5,8 @@
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea> <textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
} }
<div> <div>
<input type="button" id="update-comment-@commentId" class="btn btn-small" value="Update Comment"/> <input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger" value="Cancel"/>
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger pull-right" value="Cancel"/> <input type="button" id="update-comment-@commentId" class="btn btn-small pull-right" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){

View File

@@ -1,42 +1,35 @@
@(title: String, content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context) @(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context)
@import context._ @import context._
<span id="error-edit-title" class="error"></span>
<input type="text" style="width: 635px;" id="edit-title" value="@title"/>
@helper.html.attached(owner, repository){ @helper.html.attached(owner, repository){
<textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea> <textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
} }
<div> <div>
<input type="button" id="update" class="btn btn-small" value="Update Issue"/> <input type="button" id="cancel-issue" class="btn btn-small btn-danger" value="Cancel"/>
<input type="button" id="cancel" class="btn btn-small btn-danger pull-right" value="Cancel"/> <input type="button" id="update-issue" class="btn btn-small pull-right" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){
$('#edit-content').elastic();
var callback = function(data){ var callback = function(data){
$('#update, #cancel').removeAttr('disabled'); $('#update, #cancel').removeAttr('disabled');
$('#issueTitle').empty().text(data.title);
$('#issueContent').empty().html(data.content); $('#issueContent').empty().html(data.content);
}; };
$('#update').click(function(){ $('#update-issue').click(function(){
$('#update, #cancel').attr('disabled', 'disabled'); $('#update, #cancel').attr('disabled', 'disabled');
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issues/edit/@issueId', url: '@path/@owner/@repository/issues/edit/@issueId',
type: 'POST', type: 'POST',
data: { data: {
title : $('#edit-title').val(),
content : $('#edit-content').val() content : $('#edit-content').val()
} }
}).done( }).done(
callback callback
).fail(function(req) { ).fail(function(req) {
$('#update, #cancel').removeAttr('disabled'); $('#update, #cancel').removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
}); });
}); });
$('#cancel').click(function(){ $('#cancel-issue').click(function(){
$('#update, #cancel').attr('disabled', 'disabled'); $('#update, #cancel').attr('disabled', 'disabled');
$.get('@path/@owner/@repository/issues/_data/@issueId', callback); $.get('@path/@owner/@repository/issues/_data/@issueId', callback);
return false; return false;

View File

@@ -10,31 +10,86 @@
@import view.helpers._ @import view.helpers._
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("issues", false, repository)
<ul class="nav nav-tabs pull-left fill-width"> <ul class="nav nav-tabs pull-left fill-width">
<li class="pull-left"><a href="@url(repository)/issues"><i class="icon-arrow-left"></i> Back to issue list</a></li> <li class="pull-left">
<li class="pull-right">Issue #@issue.issueId</li> <h1>
</ul> <span class="show-title">
<div class="row-fluid"> <span id="show-title">@issue.title</span>
<div class="span10"> <span class="muted">#@issue.issueId</span>
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository) </span>
@commentlist(issue, comments, hasWritePermission, repository) <span class="edit-title" style="display: none;">
@commentform(issue, true, hasWritePermission, repository) <span id="error-edit-title" class="error"></span>
</div> <input type="text" class="span9" id="edit-title" value="@issue.title"/>
<div class="span2"> </span>
</h1>
@if(issue.closed) { @if(issue.closed) {
<span class="label label-important issue-status">Closed</span> <span class="label label-important issue-status">Closed</span>
} else { } else {
<span class="label label-success issue-status">Open</span> <span class="label label-success issue-status">Open</span>
} }
<div class="small" style="text-align: center;"> <span class="muted">
@defining(comments.filter( _.action.contains("comment") ).size){ count => @user(issue.openedUserName, styleClass="username strong") opened this issue on @datetime(issue.registeredDate) - @defining(
<span class="strong">@count</span> @plural(count, "comment") comments.filter( _.action.contains("comment") ).size
){ count =>
@count @plural(count, "comment")
} }
</span>
<br/><br/>
</li>
<li class="pull-right">
<div class="show-title">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
</div> </div>
<hr/> <div class="edit-title" style="display: none;">
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository) <a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
</div>
</li>
</ul>
<div class="row-fluid">
<div class="span10">
@commentlist(issue, comments, hasWritePermission, repository)
@commentform(issue, true, hasWritePermission, repository)
</div>
<div class="span2">
@issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
</div> </div>
} }
} }
<script>
$(function(){
$('#edit').click(function(){
$('.edit-title').show();
$('.show-title').hide();
return false;
});
$('#update').click(function(){
$(this).attr('disabled', 'disabled');
$.ajax({
url: '@url(repository)/issues/edit_title/@issue.issueId',
type: 'POST',
data: {
title : $('#edit-title').val()
}
}).done(function(data){
$('#show-title').empty().text(data.title);
$('#cancel').click();
$(this).removeAttr('disabled');
}).fail(function(req){
$(this).removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
});
return false;
});
$('#cancel').click(function(){
$('.edit-title').hide();
$('.show-title').show();
return false;
});
});
</script>

View File

@@ -1,145 +0,0 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-box">
<div class="box-content" style="padding: 0px;">
<div class="issue-header">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
}
<div class="small muted">
@user(issue.openedUserName, styleClass="username strong") opened this issue @datetime(issue.registeredDate)
</div>
<h4 id="issueTitle">@issue.title</h4>
</div>
<div class="issue-info">
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) @user(userName, styleClass="username strong") is assigned
}.getOrElse("No one is assigned")
</span>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
}
<div class="pull-right">
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <span class="strong">@milestone.title</span>
}
}.getOrElse("No milestone")
</span>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}
}
</div>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
</div>
</div>
<div class="issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description given.", repository, false, true)
</div>
</div>
</div>
<div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<span class="strong">@participants.size</span> @plural(participants.size, "participant")
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
}
</div>
<script>
$(function(){
$('#edit').click(function(){
$.get('@url(repository)/issues/_data/@issue.issueId',
{
dataType : 'html'
},
function(data){
$('#issueContent').empty().html(data);
});
return false;
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').text('No one is assigned');
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned');
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
});
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});
});
});
</script>

View File

@@ -0,0 +1,173 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import view.helpers._
<div style="margin-bottom: 8px;">
<span class="muted small strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
<hr/>
<div style="margin-bottom: 8px;">
<span class="muted small strong">Milestone</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
} else {
<span class="muted">Due by @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</div>
}
</div>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount)
}
}
</div>
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
<span class="strong small">@milestone.title</span>
}
}.getOrElse {
<span class="muted small">No milestone</span>
}
</span>
<hr/>
<div style="margin-bottom: 8px;">
<span class="muted small strong">Assignee</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
</div>
}
</div>
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) @user(userName, styleClass="username strong small")
}.getOrElse{
<span class="muted small">No one</span>
}
</span>
<hr/>
<div style="margin-bottom: 8px;">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
}
</div>
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span class="strong small">').text(title));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').html($('<span class="muted small">').text('No one'));
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar-mini').clone(false)).append(' ')
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
});
});
});
</script>

View File

@@ -1,4 +1,7 @@
@(issueLabels: List[model.Label]) @(issueLabels: List[model.Label])
@if(issueLabels.isEmpty){
<li><span class="muted small">None yet</span></li>
}
@issueLabels.map { label => @issueLabels.map { label =>
<li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li> <li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li>
} }

View File

@@ -1,51 +0,0 @@
@(issue: model.Issue,
issueLabels: List[model.Label],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import view.helpers._
<div style="margin-bottom: 8px;">
<span class="strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
});
</script>

View File

@@ -1,45 +1,61 @@
@(label: Option[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(label: Option[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@defining((if(label.isEmpty) ("new", 190, 4) else ("edit", 180, 8))){ case (mode, width, margin) => @defining(label.map(_.labelId).getOrElse("new")){ labelId =>
<div id="@(mode)LabelArea"> <div id="edit-label-area-@labelId">
<form method="POST" id="edit-label-form" validate="true" style="margin-bottom: 8px;" <form style="margin-bottom: 0px;">
action="@url(repository)/issues/label/@{if(mode == "new") "new" else label.get.labelId + "/edit"}"> <input type="text" id="labelName-@labelId" style="width: 300px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
<span id="error-@(mode)LabelName" class="error"></span> <div id="label-color-@labelId" class="input-append color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; margin-bottom: 0px;">
<input type="text" name="@(mode)LabelName" id="@(mode)LabelName" style="width: @(width)px; margin-left: @(margin)px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(mode == "new"){ placeholder="New label name"}/> <input type="text" class="span3" id="labelColor-@labelId" value="#@label.map(_.color)" readonly style="width: 100px;">
<span id="error-@(mode)Color" class="error"></span>
<div class="input-append color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" id="@(mode)Color" style="width: @(width)px; margin-bottom: 0px;">
<input type="text" class="span3" name="@(mode)Color" value="#@label.map(_.color)" readonly style="width: @(width - 12)px; margin-left: @(margin)px;">
<span class="add-on"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span> <span class="add-on"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
</div> </div>
<input type="submit" class="btn" style="margin-left: @(margin)px; margin-bottom: 0px;" value="@if(mode == "new"){Create} else {Save}"/>
@if(mode == "edit"){
<input type="hidden" name="editLabelId" value="@label.map(_.labelId)"/>
}
</form>
<script> <script>
$(function(){ $('div#label-color-@labelId').colorpicker();
@if(mode == "new"){ </script>
$('#newColor').colorpicker(); <span id="label-error-@labelId" class="error" style="padding-left: 40px;"></span>
<span class="pull-right">
<input type="button" id="cancel-@labelId" class="btn label-edit-cancel" value="Cancel">
<input type="button" id="submit-@labelId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
</span>
</form>
</div>
<script>
$(function(){
$('#submit-@labelId').click(function(e){
$.post('@url(repository)/issues/labels/@{if(labelId == "new") "new" else labelId + "/edit"}', {
'labelName' : $('#labelName-@labelId').val(),
'labelColor': $('#labelColor-@labelId').val()
}, function(data, status){
$('div#edit-label-area-@labelId').remove();
@if(labelId == "new"){
$('#new-label-table').hide();
// Insert row into the top of table
$('#label-row-header').after(data);
} else { } else {
$('#editColor').colorpicker(); // Replace table row
$('#label-row-@labelId').after(data).remove();
$('#edit-label-form').submit(function(e){ }
$.ajax($(this).attr('action'), { }).fail(function(xhr, status, error){
type: 'POST', var errors = JSON.parse(xhr.responseText);
data: $(this).serialize() if(errors.labelName){
}) $('span#label-error-@labelId').text(errors.labelName);
.done(function(data){ } else if(errors.labelColor){
$('#label-edit').parent().empty().html(data); $('span#label-error-@labelId').text(errors.labelColor);
}) } else {
.fail(function(data, status){ $('span#label-error-@labelId').text('error');
displayErrors($.parseJSON(data.responseText)); }
}); });
return false; return false;
}); });
$('#cancel-@labelId').click(function(e){
$('div#edit-label-area-@labelId').remove();
@if(labelId == "new"){
$('#new-label-table').hide();
} else {
$('#label-@labelId').show();
} }
}); });
</script> });
</div> </script>
} }

View File

@@ -1,47 +0,0 @@
@(labels: List[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div id="label-edit">
<ul class="label-list nav nav-pills nav-stacked">
@labels.map { label =>
<li style="border: 1px solid white;">
<a href="javascript:void(0);" class="label-edit-link" data-label-id="@label.labelId">
<span class="count-right"><i class="icon-remove-circle"></i></span>
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
</ul>
<script>
$(function(){
$('i.icon-remove-circle').click(function(e){
e.stopPropagation();
if(confirm('Are you sure you want to delete this?')){
$.get('@url(repository)/issues/label/' + $(this).parents('a').data('label-id') + '/delete',
function(data){
$('#label-edit').parent().empty().html(data);
}
);
}
});
$('a.label-edit-link').click(function(e){
if($('input[name=editLabelId]').val() != $(this).data('label-id')){
$('#editLabelArea').remove();
var element = this;
$.get('@url(repository)/issues/label/' + $(this).data('label-id') + '/edit',
function(data){
$(element).parent().append(data);
$('div#label-edit li').css('border', '1px solid white');
$(element).parent().css('border', '1px solid #eee');
}
);
} else {
$('#editLabelArea').remove();
$('div#label-edit li').css('border', '1px solid white');
}
});
});
</script>
</div>

View File

@@ -0,0 +1,36 @@
@(label: model.Label,
counts: Map[String, Int],
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
<tr id="label-row-@label.labelId">
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row-fluid" id="label-@label.labelId">
<div class="span8">
<div style="margin-top: 6px">
<a href="@url(repository)/issues?labels=@urlEncode(label.labelName)" id="label-row-content-@label.labelId">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
<img src="@assets/common/images/label_@(if(label.fontColor == "ffffff") "white" else "black").png" style="width: 12px;"/>
@label.labelName
</span>
</a>
</div>
</div>
<div class="@if(hasWritePermission){span2} else {span4}">
<div class="pull-right">
<span class="muted">@counts.get(label.labelName).getOrElse(0) open issues</span>
</div>
</div>
@if(hasWritePermission){
<div class="span2">
<div class="pull-right">
<a href="javascript:void(0);" onclick="editLabel(@label.labelId)">Edit</a>
&nbsp;&nbsp;
<a href="javascript:void(0);" onclick="deleteLabel(@label.labelId)">Delete</a>
</div>
</div>
}
</div>
</td>
</tr>

View File

@@ -0,0 +1,66 @@
@(labels: List[model.Label],
counts: Map[String, Int],
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){
@issues.html.tab("labels", hasWritePermission, repository)
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
<tr><td></td></tr>
</table>
<table class="table table-bordered table-hover table-issues">
<tr id="label-row-header">
<th style="background-color: #eee;">
<span class="small">@labels.size labels</span>
</th>
</tr>
@labels.map { label =>
@_root_.issues.labels.html.label(label, counts, repository, hasWritePermission)
}
@if(labels.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No labels to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/labels/new">Create a new label.</a>
}
</td>
</tr>
}
</table>
}
}
<script>
$(function(){
$('#new-label-button').click(function(e){
if($('#new-label-area').size() != 0){
$('#new-label-table').hide();
$('#new-label-area').remove();
} else {
$.get('@url(repository)/issues/labels/new',
function(data){
$('#new-label-table').show().find('tr td').append(data);
}
);
}
});
});
function deleteLabel(labelId){
if(confirm('Once you delete this label, there is no going back.\nAre you sure?')){
$.post('@url(repository)/issues/labels/' + labelId + '/delete', function(){
$('tr#label-row-' + labelId).remove();
});
}
}
function editLabel(labelId){
$.get('@url(repository)/issues/labels/' + labelId + '/edit',
function(data){
$('#label-' + labelId).hide().parent().append(data);
}
);
}
</script>

View File

@@ -1,143 +1,25 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(target: String,
issues: List[service.IssuesService.IssueInfo],
page: Int, page: Int,
collaborators: List[String], collaborators: List[String],
milestones: List[model.Milestone], milestones: List[model.Milestone],
labels: List[model.Label], labels: List[model.Label],
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
allCount: Int,
assignedCount: Option[Int],
createdByCount: Option[Int],
labelCounts: Map[String, Int],
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
filter: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context) hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){ @html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu(target, repository){
@tab("issues", false, repository) @tab(target, true, repository)
<div class="row-fluid"> @listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "all"){ class="active"}>
<a href="@url(repository)/issues@condition.toURL">
<span class="count-right">@allCount</span>
Everyone's Issues
</a>
</li>
@if(loginAccount.isDefined){
<li@if(filter == "assigned"){ class="active"}>
<a href="@url(repository)/issues/assigned/@loginAccount.map(_.userName)@condition.toURL">
<span class="count-right">@assignedCount</span>
Assigned to you
</a>
</li>
<li@if(filter == "created_by"){ class="active"}>
<a href="@url(repository)/issues/created_by/@loginAccount.map(_.userName)@condition.toURL">
<span class="count-right">@createdByCount</span>
Created by you
</a>
</li>
}
</ul>
<hr/>
@if(condition.milestoneId.isEmpty){
<span class="muted small">No milestone selected</span>
} else {
@if(condition.milestoneId.get.isEmpty){
<span class="muted small">Issues with no milestone</span>
} else {
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
}
}
@helper.html.dropdown() {
@if(condition.milestoneId.isDefined){
<li>
<a href="@condition.copy(milestoneId = None).toURL">
<i class="icon-remove-circle"></i> Clear milestone filter
</a>
</li>
}
<li>
<a href="@condition.copy(milestoneId = Some(None)).toURL">
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
</a>
</li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
<div style="margin-top: 4px;">
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
</div>
<span class="muted small">@openCount open issues</span>
@if(milestone.closedDate.isDefined){
@milestone.closedDate.map { closedDate =>
<span class="small">Closed in @date(closedDate)</span>
}
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="small">Due in @date(dueDate)</span>
}
}
}
}
}
<hr/>
<span class="strong">Labels</span>
<div>
<div id="label-list">
<ul class="label-list nav nav-pills nav-stacked">
@labels.map { label =>
<li>
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL"
@if(condition.labels.contains(label.labelName)){style="background-color: #@label.color; color: #@label.fontColor;"}>
<span class="count-right">@labelCounts.getOrElse(label.labelName, 0)</span>
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
</ul>
</div>
</div>
@if(hasWritePermission){
<hr/>
<input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/>
<br/>
<span class="strong">New label</span>
@_root_.issues.labels.html.edit(None, repository)
}
</div>
@***** show issue list *****@
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
</div>
@if(hasWritePermission){ @if(hasWritePermission){
<form id="batcheditForm" method="POST"> <form id="batcheditForm" method="POST">
<input type="hidden" name="value"/> <input type="hidden" name="value"/>
<input type="hidden" name="checked"/> <input type="hidden" name="checked"/>
<input type="hidden" name="from" value="@target"/>
</form> </form>
} }
} }
@@ -145,20 +27,34 @@
@if(hasWritePermission){ @if(hasWritePermission){
<script> <script>
$(function(){ $(function(){
$('#manageLabel').click(function(){ $('a.header-link').mouseover(function(e){
if($(this).data('toggle-state')){ var target = e.target;
location.href = '@url(repository)/issues'; if(e.target.tagName != 'A'){
} else { target = e.target.parentElement;
$(this).data('toggle-state', 'on');
$.get('@url(repository)/issues/label/edit', function(data){
$('#label-list').parent().empty().html(data);
});
} }
$(target).children('strong' ).css('color', '#0088cc');
$(target).children('img.header-icon-hover').css('display', 'inline');
$(target).children('img.header-icon' ).css('display', 'none');
});
$('a.header-link').mouseout(function(e){
var target = e.target;
if(e.target.tagName != 'A'){
target = e.target.parentElement;
}
$(target).children('strong' ).css('color', 'black');
$(target).children('img.header-icon-hover').css('display', 'none');
$(target).children('img.header-icon' ).css('display', 'inline');
}); });
$('.table-issues input[type=checkbox]').change(function(){ $('.table-issues input[type=checkbox]').change(function(){
$('.table-issues button').prop('disabled', if($('.table-issues input[type=checkbox]').filter(':checked').length == 0){
!$('.table-issues input[type=checkbox]').filter(':checked').length); $('#table-issues-control').show();
$('#table-issues-batchedit').hide();
} else {
$('#table-issues-control').hide();
$('#table-issues-batchedit').show();
}
}).filter(':first').change(); }).filter(':first').change();
var submitBatchEdit = function(action, value) { var submitBatchEdit = function(action, value) {
@@ -170,8 +66,8 @@ $(function(){
form.submit(); form.submit();
}; };
$('#state').click(function(){ $('a.toggle-state').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).text().toLowerCase()); submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).data('id'));
}); });
$('a.toggle-label').click(function(){ $('a.toggle-label').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id')); submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id'));

View File

@@ -1,4 +1,5 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(target: String,
issues: List[service.IssuesService.IssueInfo],
page: Int, page: Int,
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
@@ -10,37 +11,78 @@
hasWritePermission: Boolean = false)(implicit context: app.Context) hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9"> @if(condition.nonEmpty){
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ <div>
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter"> <a href="@service.IssuesService.IssueSearchCondition().toURL" class="header-link">
<i class="icon-remove-circle"></i> Clear milestone and label filters <img src="@assets/common/images/clear.png" class="header-icon"/>
<img src="@assets/common/images/clear_hover.png" class="header-icon-hover" style="display: none;"/>
<span class="strong">Clear current search query, filters, and sorts</span>
</a> </a>
} </div>
@if(condition.repo.isDefined){ }
<a href="@condition.copy(repo = None).toURL" id="clear-filter"> &nbsp;
<i class="icon-remove-circle"></i> Clear filter on @condition.repo <table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
<input type="checkbox"/>
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
@closedCount Closed
</a> </a>
</span>
<div class="pull-right" id="table-issues-control">
@helper.html.dropdown("Author", flat = true) {
@collaborators.map { collaborator =>
<li>
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
@helper.html.checkicon(condition.author == Some(collaborator))
@avatar(collaborator, 20) @collaborator
</a>
</li>
} }
<div class="pull-right"> }
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL) @helper.html.dropdown("Label", flat = true) {
</div> @labels.map { label =>
<div class="btn-group"> <li>
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a> <a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a> @helper.html.checkicon(condition.labels.contains(label.labelName))
</div> <span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@helper.html.dropdown( @label.labelName
value = (condition.sort, condition.direction) match { </a>
case ("created" , "desc") => "Newest" </li>
case ("created" , "asc" ) => "Oldest" }
case ("comments", "desc") => "Most commented" }
case ("comments", "asc" ) => "Least commented" @helper.html.dropdown("Milestone", flat = true) {
case ("updated" , "desc") => "Recently updated" <li>
case ("updated" , "asc" ) => "Least recently updated" <a href="@condition.copy(milestoneId = Some(None)).toURL">
}, @helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
prefix = "Sort", </a>
mini = false </li>
){ @milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
</a>
</li>
}
}
@helper.html.dropdown("Assignee", flat = true) {
@collaborators.map { collaborator =>
<li>
<a href="@condition.copy(assigned = Some(collaborator)).toURL">
@helper.html.checkicon(condition.assigned == Some(collaborator))
@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
@helper.html.dropdown("Sort", flat = true){
<li> <li>
<a href="@condition.copy(sort="created", direction="desc").toURL"> <a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
@@ -72,28 +114,14 @@
</a> </a>
</li> </li>
} }
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
</div> </div>
@helper.html.dropdown("Label") { @if(hasWritePermission){
<div class="pull-right" id="table-issues-batchedit">
@helper.html.dropdown("Mark as", flat = true) {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
}
@helper.html.dropdown("Label", flat = true) {
@labels.map { label => @labels.map { label =>
<li> <li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId"> <a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
@@ -104,81 +132,87 @@
</li> </li>
} }
} }
@helper.html.dropdown("Assignee") { @helper.html.dropdown("Milestone", flat = true) {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
}
}
@helper.html.dropdown("Assignee", flat = true) {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li> <li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li> <li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
} }
} }
@helper.html.dropdown("Milestone") {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.map { milestone =>
<li>
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div> </div>
</a> }
</li> </th>
</tr>
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
@if(target == "issues"){
No issues to show.
} else {
No pull requests to show.
}
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
@if(target == "issues"){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
} else {
<a href="@url(repository.get)/compare">Create a new pull request.</a>
}
} }
} }
</td> </td>
</tr> </tr>
} }
} @issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
@issues.map { case (issue, labels, commentCount) =>
<tr> <tr>
<td> <td style="padding-top: 15px; padding-bottom: 15px;">
@if(hasWritePermission){ @if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/> <input type="checkbox" value="@issue.issueId"/>
} }
@if(issue.isPullRequest){ <img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png" style="margin-right: 20px;"/>
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
@if(repository.isEmpty){ @if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65; <a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
} }
@if(issue.isPullRequest){ @if(target == "issues"){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a> <a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} }
@labels.map { label => @labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span> <span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
} }
<span class="pull-right muted"> <span class="pull-right small">
@issue.assignedUserName.map { userName => @issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true) @avatar(userName, 20, tooltip = true)
} }
#@issue.issueId
</span>
<div class="small muted" style="margin-left: 20px;">
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){ @if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a> <a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<img src="@assets/common/images/comment-active.png"> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<img src="@assets/common/images/comment.png"> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 40px; margin-top: 5px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
} }
</div> </div>
@if(hasWritePermission){
</label>
}
</td> </td>
</tr> </tr>
} }
</table> </table>
<div class="pull-right"> <div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL) @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div> </div>
</div>

View File

@@ -2,12 +2,18 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("milestones", repository){ @html.menu("issues", repository){
@if(milestone.isEmpty){
<h4>New milestone</h4>
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
} else {
@issues.html.tab("milestones", false, repository) @issues.html.tab("milestones", false, repository)
<br><br>
}
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true"> <form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
<fieldset> <fieldset>
<label for="title"><string>Title</string></label> <input type="text" id="title" name="title" style="width: 500px;" value="@milestone.map(_.title)" placeholder="Title"/>
<input type="text" id="title" name="title" style="width: 400px;" value="@milestone.map(_.title)"/>
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>

View File

@@ -6,70 +6,71 @@
@import view.helpers._ @import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){ @html.menu("issues", repository){
@issues.html.tab("milestones", false, repository) @issues.html.tab("milestones", hasWritePermission, repository)
<div class="row-fluid"> <table class="table table-bordered table-hover table-issues">
<div class="span3"> <tr>
<ul class="nav nav-pills nav-stacked"> <th style="background-color: #eee;">
<li@if(state == "open"){ class="active"}> <span class="small">
<a href="?state=open"> <a class="button-link@if(state == "open"){ selected}" href="?state=open">
<span class="count-right">@milestones.filter(_._1.closedDate.isEmpty).size</span> <img src="@assets/common/images/milestone@(if(state == "open"){"-active"}).png"/>
Open Milestones @milestones.filter(_._1.closedDate.isEmpty).size Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
<img src="@assets/common/images/milestone@(if(state == "closed"){"-active"}).png"/>
@milestones.filter(_._1.closedDate.isDefined).size Closed
</a> </a>
</li> </span>
<li@if(state == "closed"){ class="active"}> </th>
<a href="?state=closed"> </tr>
<span class="count-right">@milestones.filter(_._1.closedDate.isDefined).size</span>
Closed Milestones
</a>
</li>
</ul>
@if(hasWritePermission){
<hr>
<a href="@url(repository)/issues/milestones/new" class="btn btn-block">Create a new milestone</a>
}
</div>
<div class="span9">
<table class="table table-bordered table-hover">
@defining(milestones.filter { case (milestone, _, _) => @defining(milestones.filter { case (milestone, _, _) =>
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open") milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
}){ milestones => }){ milestones =>
@milestones.map { case (milestone, openCount, closedCount) => @milestones.map { case (milestone, openCount, closedCount) =>
<tr> <tr>
<td> <td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row-fluid"> <div class="milestone row-fluid">
<div class="span4"> <div class="span4">
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a><br> <a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a>
<div style="margin-top: 6px">
@if(milestone.closedDate.isDefined){ @if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span> <span class="muted">Closed @datetime(milestone.closedDate.get)</span>
} else { } else {
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due in @date(dueDate)</span> <img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due by @date(dueDate)</span>
} else { } else {
<span class="muted">Due in @date(dueDate)</span> <span class="muted">Due by @date(dueDate)</span>
} }
}.getOrElse { }.getOrElse {
<span class="muted">No due date</span> <span class="muted">No due date</span>
} }
} }
</div> </div>
</div>
<div class="span8"> <div class="span8">
<div class="milestone-menu"> @progress(openCount + closedCount, closedCount)
<div class="pull-right"> <div>
@if(hasWritePermission){ <div>
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit @if(closedCount == 0){
@if(milestone.closedDate.isDefined){ 0%
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a>
} else { } else {
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a> @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
} <span class="muted">complete</span> &nbsp;&nbsp;
@openCount <span class="muted">open</span> &nbsp;&nbsp;
@closedCount <span class="muted">closed</span>
</div>
<div class="milestone-menu">
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit</a> &nbsp;&nbsp;
@if(milestone.closedDate.isDefined){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a> &nbsp;&nbsp;
} else {
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a> &nbsp;&nbsp;
} }
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a> <a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
} }
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open">Browse issues</a>
</div> </div>
<span class="muted">@closedCount closed - @openCount open</span>
</div> </div>
@progress(openCount + closedCount, closedCount, true)
</div> </div>
</div> </div>
@if(milestone.description.isDefined){ @if(milestone.description.isDefined){
@@ -92,8 +93,6 @@
} }
} }
</table> </table>
</div>
</div>
} }
} }
<script> <script>

View File

@@ -1,15 +1,6 @@
@(total: Int, progress: Int, showPercentage: Boolean) @(total: Int, progress: Int)
<div class="milestone-progress"> <div class="milestone-progress">
@if(progress > 0){ @if(progress > 0){
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span> <span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
} }
@if(showPercentage){
<span class="milestone-percentage">
@if(progress == 0){
0%
} else {
@((progress.toDouble / total.toDouble * 100).toInt)%
}
</span>
}
</div> </div>

View File

@@ -1,16 +1,28 @@
@(active: String, create: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(active: String, newButton: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<ul class="nav nav-tabs pull-left fill-width"> <ul class="nav nav-pills-group pull-left fill-width">
<li@if(active == "issues"){ class="active"}><a href="@url(repository)/issues">Browse Issues</a></li> <li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
<li@if(active == "milestones"){ class="active"}><a href="@url(repository)/issues/milestones">Milestones</a></li> <li class="@if(active == "pulls" ){active}"><a href="@url(repository)/pulls">Pull requests</a></li>
<li class="@if(active == "labels" ){active}"><a href="@url(repository)/issues/labels">Labels</a></li>
<li class="@if(active == "milestones"){active} last"><a href="@url(repository)/issues/milestones">Milestones</a></li>
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div class="btn-group">
@if(create){ @if(newButton){
<a class="btn btn-small btn-success" href="#" disabled="disabled">New Issue</a> @if(active == "issues"){
} else { <a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New Issue</a> }
@if(active == "pulls"){
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
}
@if(active == "labels"){
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button">New label</a>
}
@if(active == "milestones"){
<a class="btn btn-success" href="@url(repository)/issues/milestones/new">New milestone</a>
}
} }
</div> </div>
</li> </li>

View File

@@ -88,6 +88,9 @@
$('#search').submit(function(){ $('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != ''; return $.trim($(this).find('input[name=query]').val()) != '';
}); });
@plugin.PluginSystem.javaScripts.filter(_.filter(context.currentPath)).map { js =>
@Html(js.script)
}
}); });
</script> </script>
</body> </body>

View File

@@ -11,7 +11,6 @@
@import view.helpers._ @import view.helpers._
<div class="row-fluid"> <div class="row-fluid">
<div class="span10"> <div class="span10">
@issues.html.issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq)) @issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
@defining(comments.exists(_.action == "merge")){ merged => @defining(comments.exists(_.action == "merge")){ merged =>
@if(hasWritePermission && !issue.closed){ @if(hasWritePermission && !issue.closed){
@@ -67,7 +66,7 @@
</div> </div>
} }
<hr/> <hr/>
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository) @issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
</div> </div>
<script> <script>

View File

@@ -1,51 +0,0 @@
@(issues: List[(model.Issue, List[model.Label], Int)],
counts: List[service.PullRequestService.PullRequestCount],
filter: Option[String],
page: Int,
openCount: Int,
closedCount: Int,
allCount: Int,
condition: service.IssuesService.IssueSearchCondition,
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("pulls", repository){
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter.isEmpty){ class="active"}>
<a href="@url(repository)/pulls">
<span class="count-right">@allCount</span>
All Requests
</a>
</li>
@if(loginAccount.isDefined){
<li@if(filter.map(_ == loginAccount.get.userName).getOrElse(false)){ class="active"}>
<a href="@url(repository)/pulls/@loginAccount.map(_.userName)">
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
Yours
</a>
</li>
}
</ul>
<hr>
<ul class="nav nav-pills nav-stacked small">
@counts.map { user =>
@if(loginAccount.isEmpty || loginAccount.get.userName != user.userName){
<li@if(filter.map(_ == user.userName).getOrElse(false)){ class="active"}>
<a href="@url(repository)/pulls/@user.userName">
<span class="count-right">@user.count</span>
@user.userName
</a>
</li>
}
}
</ul>
</div>
@listparts(issues, page, openCount, closedCount, condition, Some(repository), hasWritePermission)
</div>
}
}

View File

@@ -28,7 +28,7 @@
} }
</div> </div>
<table class="table table-bordered"> <table class="table table-bordered blobview">
<tr> <tr>
<th style="font-weight: normal;"> <th style="font-weight: normal;">
<div class="pull-left"> <div class="pull-left">

View File

@@ -56,6 +56,6 @@
<link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" /> <link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script> <script>
$(function(){ $(function(){
diffUsingJS('oldText', 'newText', 'diffText'); diffUsingJS('oldText', 'newText', 'diffText', 1);
}); });
</script> </script>

View File

@@ -139,7 +139,7 @@ $(function(){
.append($('<div id="diffText">')) .append($('<div id="diffText">'))
.append($('<textarea id="newText" style="display: none;">').html(editor.getValue())) .append($('<textarea id="newText" style="display: none;">').html(editor.getValue()))
.append($('<textarea id="oldText" style="display: none;">').html($('#initial').val())); .append($('<textarea id="oldText" style="display: none;">').html($('#initial').val()));
diffUsingJS('oldText', 'newText', 'diffText'); diffUsingJS('oldText', 'newText', 'diffText', 1);
} }
}); });
}); });

View File

@@ -84,9 +84,19 @@
<td> <td>
@if(file.isDirectory){ @if(file.isDirectory){
@if(file.linkUrl.isDefined){ @if(file.linkUrl.isDefined){
<a href="@file.linkUrl">@file.name</a> <a href="@file.linkUrl">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
} else { } else {
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a> <a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
} }
} else { } else {
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a> <a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>

View File

@@ -11,7 +11,7 @@
<th width="20%">Commit</th> <th width="20%">Commit</th>
<th width="20%">Download</th> <th width="20%">Download</th>
</tr> </tr>
@repository.tags.map { tag => @repository.tags.reverse.map { tag =>
<tr> <tr>
<td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td> <td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td>
<td>@datetime(tag.time)</td> <td>@datetime(tag.time)</td>

View File

@@ -1,4 +1,7 @@
@(webHooks: List[model.WebHook], repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context) @(webHooks: List[model.WebHook],
enteredUrl: Option[Any],
repository: service.RepositoryService.RepositoryInfo,
info: Option[Any])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Settings", Some(repository)){ @html.main("Settings", Some(repository)){
@@ -11,13 +14,13 @@
<li>@webHook.url <a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="remove">(remove)</a></li> <li>@webHook.url <a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="remove">(remove)</a></li>
} }
</ul> </ul>
<form method="POST" action="@url(repository)/settings/hooks/add" validate="true"> <form method="POST" validate="true">
<div> <div>
<span class="error" id="error-url"></span> <span class="error" id="error-url"></span>
</div> </div>
<input type="text" name="url" id="url" style="width: 300px; margin-bottom: 0px;"/> <input type="text" name="url" id="url" value="@enteredUrl" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/> <input type="submit" class="btn" formaction="@url(repository)/settings/hooks/add" value="Add"/>
<a href="@url(repository)/settings/hooks/test" class="btn">Test Hook</a> <input type="submit" class="btn" formaction="@url(repository)/settings/hooks/test" value="Test Hook"/>
</form> </form>
} }
} }

View File

@@ -36,9 +36,6 @@ h6 {
font-size: 18px; font-size: 18px;
} }
h7 {
font-size: 14px;
}
/* ======================================================================== */ /* ======================================================================== */
/* Global Header */ /* Global Header */
@@ -122,6 +119,11 @@ div.container {
width: 920px; width: 920px;
} }
div.container-wide {
padding-left: 10px;
padding-right: 10px;
}
div.pagination { div.pagination {
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
@@ -510,6 +512,10 @@ a.header-link:hover {
text-decoration: none; text-decoration: none;
} }
table.blobview {
table-layout: fixed;
}
table.table-file-list { table.table-file-list {
margin-bottom: 0px; margin-bottom: 0px;
border: 1px solid #ddd; border: 1px solid #ddd;
@@ -592,6 +598,7 @@ pre.blob {
border: none; border: none;
background-color: white; background-color: white;
padding-left: 20px; padding-left: 20px;
font-size: 12px;
} }
#readme { #readme {
@@ -606,11 +613,85 @@ li.highlight {
background-color: #ffb; background-color: #ffb;
} }
span.simplified-path {
color: #888;
}
/****************************************************************************/
/* nav pulls group */
/****************************************************************************/
.nav-pills-group:after {
display: table;
line-height: 0;
content: "";
}
.nav-pills-group:after {
clear: both;
}
.nav-pills-group > li {
float: left;
}
.nav-pills-group > li > a {
padding-right: 12px;
padding-left: 12px;
line-height: 14px;
color: #666;
font-weight: bold;
}
.nav-pills-group > li > a {
padding-top: 10px;
padding-bottom: 10px;
border-left : 1px solid #e5e5e5;
border-top : 1px solid #e5e5e5;
border-bottom : 1px solid #e5e5e5;
}
.nav-pills-group > .first > a {
-webkit-border-radius: 4px 0 0 4px;
-moz-border-radius: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
.nav-pills-group > .last > a {
-webkit-border-radius: 0 4px 4px 0;
-moz-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
border-right : 1px solid #e5e5e5;
}
.nav-pills-group > .active > a,
.nav-pills-group > .active > a:hover,
.nav-pills-group > .active > a:focus {
color: #ffffff;
background-color: #0088cc;
border-color: #0088cc;
}
/****************************************************************************/ /****************************************************************************/
/* Issues */ /* Issues */
/****************************************************************************/ /****************************************************************************/
.btn-group.open .dropdown-toggle.flat {
background-image: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
a.button-link {
font-weight: normal;
color: gray;
}
a.selected {
font-weight: bold;
color: black;
}
span.issue-status { span.issue-status {
display: block;
font-size: large; font-size: large;
text-align: center; text-align: center;
padding: 8px; padding: 8px;
@@ -623,7 +704,7 @@ table.table-issues {
a.issue-title { a.issue-title {
color: #333; color: #333;
font-weight: bold; font-weight: bold;
size: 110%; font-size: 120%;
} }
ul.label-list { ul.label-list {
@@ -660,8 +741,7 @@ span.milestone-alert {
} }
a.milestone-title { a.milestone-title {
font-size: 120%; font-size: 180%;
font-weight: bold;
} }
div.milestone-description { div.milestone-description {
@@ -669,13 +749,12 @@ div.milestone-description {
color: #666; color: #666;
} }
div.milestone-menu { a.milestone-title {
font-size: 80%; color: #333;
} }
div.milestone-menu a { div.milestone-menu {
margin-left: 8px; margin-top: 8px;
font-weight: bold;
} }
div.milestone-menu a.delete { div.milestone-menu a.delete {
@@ -687,13 +766,13 @@ div#milestone-progress-area {
} }
div#milestone-progress-area div.milestone-progress { div#milestone-progress-area div.milestone-progress {
width: 150px; width: 130px;
margin-bottom: -6px; margin-bottom: -6px;
} }
div.milestone-progress { div.milestone-progress {
position: relative; position: relative;
height: 20px; height: 10px;
color: white; color: white;
margin-bottom: 4px; margin-bottom: 4px;
font-weight: bold; font-weight: bold;
@@ -714,11 +793,6 @@ span.milestone-progress {
-moz-border-radius: 4px; -moz-border-radius: 4px;
} }
span.milestone-percentage {
position: absolute;
padding-left: 8px;
}
div.issue-header { div.issue-header {
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
@@ -836,18 +910,24 @@ div.author-info div.committer {
/****************************************************************************/ /****************************************************************************/
/* Diff */ /* Diff */
/****************************************************************************/ /****************************************************************************/
table.inlinediff { table.diff {
font-size: 12px;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
width: 100%; width: 100%;
} }
table.inlinediff thead { table.diff thead {
display: none; display: none;
} }
td.insert, td.equal, td.delete { table.inlinediff td.insert, table.inlinediff td.equal, table.inlinediff td.delete {
width: 100%; width: 100%;
} }
td.insert, td.equal, td.delete, td.empty {
width: 50%;
}
/****************************************************************************/ /****************************************************************************/
/* Repository Settings */ /* Repository Settings */
/****************************************************************************/ /****************************************************************************/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

View File

@@ -17,7 +17,7 @@ $(function(){
}); });
$('.markdown-head').mouseleave(function(e){ $('.markdown-head').mouseleave(function(e){
var anchorLink = $(e.target).children('a.markdown-anchor-link'); var anchorLink = $(e.target).children('a.markdown-anchor-link');
if(anchorLink.data('active') != true){ if(anchorLink.data('active') !== true){
anchorLink.hide(); anchorLink.hide();
} }
}); });
@@ -39,7 +39,7 @@ function displayErrors(data){
var i = 0; var i = 0;
$.each(data, function(key, value){ $.each(data, function(key, value){
$('#error-' + key.split(".").join("_")).text(value); $('#error-' + key.split(".").join("_")).text(value);
if(i == 0){ if(i === 0){
$('#' + key).focus(); $('#' + key).focus();
} }
i++; i++;
@@ -68,20 +68,26 @@ function displayErrors(data){
}; };
})(jQuery); })(jQuery);
function diffUsingJS(oldTextId, newTextId, outputId) { /**
* Render diff using jsdifflib.
*
* @param oldTextId {String} element id of old text
* @param newTextId {String} element id of new text
* @param outputId {String} element id of output element
* @param viewType {Number} 0: split, 1: unified
*/
function diffUsingJS(oldTextId, newTextId, outputId, viewType) {
// get the baseText and newText values from the two textboxes, and split them into lines // get the baseText and newText values from the two textboxes, and split them into lines
var oldText = document.getElementById(oldTextId).value; var oldText = document.getElementById(oldTextId).value;
if(oldText == ''){
var oldLines = []; var oldLines = [];
} else { if(oldText !== ''){
var oldLines = difflib.stringAsLines(oldText); oldLines = difflib.stringAsLines(oldText);
} }
var newText = document.getElementById(newTextId).value var newText = document.getElementById(newTextId).value;
if(newText == ''){
var newLines = []; var newLines = [];
} else { if(newText !== ''){
var newLines = difflib.stringAsLines(newText); newLines = difflib.stringAsLines(newText);
} }
// create a SequenceMatcher instance that diffs the two sets of lines // create a SequenceMatcher instance that diffs the two sets of lines
@@ -100,7 +106,7 @@ function diffUsingJS(oldTextId, newTextId, outputId) {
newTextLines: newLines, newTextLines: newLines,
opcodes: opcodes, opcodes: opcodes,
contextSize: 4, contextSize: 4,
viewType: 1 viewType: viewType
})); }));
} }

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