mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-09 11:47:08 +02:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d56d72611 | ||
|
|
527c91ff9d | ||
|
|
c58c2d6700 | ||
|
|
5518eca952 | ||
|
|
6e2b67ec0b | ||
|
|
837b1e44a7 | ||
|
|
e04c230c6e | ||
|
|
a01b5a4a59 | ||
|
|
427b6ce846 | ||
|
|
b7b5af2b72 | ||
|
|
39fec57f72 | ||
|
|
238dedb6df | ||
|
|
af091117b7 | ||
|
|
ddea4e12f0 | ||
|
|
9767903252 | ||
|
|
bc75f9f8a2 | ||
|
|
63627fc1d0 | ||
|
|
c23985c1a7 | ||
|
|
af58e99dcf | ||
|
|
676670e9e3 | ||
|
|
823c52e941 | ||
|
|
7f42007648 | ||
|
|
7214ef21d2 | ||
|
|
18a4492975 | ||
|
|
99f73b1016 | ||
|
|
0c1ce6a088 | ||
|
|
ae6291ab83 | ||
|
|
617fcf7c99 | ||
|
|
9df4a74837 | ||
|
|
966d4251be | ||
|
|
84b2e9cdcd | ||
|
|
e29d63c91a | ||
|
|
805d2b8e79 | ||
|
|
9983fd1292 | ||
|
|
1de202e927 | ||
|
|
4eb9f4a485 | ||
|
|
a8801e4e41 | ||
|
|
ee1c84dbf2 | ||
|
|
e40e1fa6cd | ||
|
|
055f648ea2 | ||
|
|
37a399c3a2 | ||
|
|
bc0b11b60a | ||
|
|
65a1ca7146 | ||
|
|
2293030d4e | ||
|
|
c83fab611e | ||
|
|
29baf1223c | ||
|
|
2a60f607ff | ||
|
|
78f4d26aa0 | ||
|
|
f59e86f5ca | ||
|
|
1c2af36c92 | ||
|
|
badbe73f4e | ||
|
|
a9d58698cd | ||
|
|
bb3f086aa6 | ||
|
|
2db674bb03 | ||
|
|
4bc4a16a80 | ||
|
|
d88a105628 | ||
|
|
15d0c5b506 | ||
|
|
dbde79d2f2 | ||
|
|
e6e3786b47 | ||
|
|
4c1b8004fc | ||
|
|
ff4052f097 | ||
|
|
13c206d068 | ||
|
|
5b875d7c73 | ||
|
|
e33dd9008b | ||
|
|
8764910553 | ||
|
|
4c89c40944 | ||
|
|
0f0986afcf | ||
|
|
5d5f1f8bdd | ||
|
|
03e386b3ce | ||
|
|
435eac7ae6 | ||
|
|
bd5df3977d | ||
|
|
ba218053f9 | ||
|
|
1fe448a83b | ||
|
|
26a45d0117 | ||
|
|
320585a530 | ||
|
|
ca0f888a99 | ||
|
|
3b08dc2e41 | ||
|
|
cc128a49c1 | ||
|
|
e0148695f2 | ||
|
|
afe0b1dd71 | ||
|
|
353852d6da | ||
|
|
28585d1a3d | ||
|
|
9d69a48c65 | ||
|
|
2f95c76634 | ||
|
|
eac9f0e6ff | ||
|
|
043fc21e05 | ||
|
|
5854a75615 | ||
|
|
7b02946496 | ||
|
|
70f0ffd4f4 | ||
|
|
91b82c2652 | ||
|
|
b1017140aa | ||
|
|
fc806b8813 | ||
|
|
836913482b | ||
|
|
b3df3f44c6 | ||
|
|
4ffbf89e74 | ||
|
|
9851c7d93d | ||
|
|
2201f2b202 | ||
|
|
c92e71bb7a | ||
|
|
d271fac350 | ||
|
|
ce4522fc30 | ||
|
|
a178c48de6 | ||
|
|
9d1323a044 | ||
|
|
43babfed94 | ||
|
|
6fa7ea30fb | ||
|
|
d78315695b | ||
|
|
16021865cb | ||
|
|
b516be242d | ||
|
|
0124f7cc3c | ||
|
|
f3eec35287 | ||
|
|
fb396a33b0 | ||
|
|
3370499421 | ||
|
|
d847e27cf9 | ||
|
|
9684b158ce | ||
|
|
8456808a8e | ||
|
|
9747899a19 | ||
|
|
099304605e | ||
|
|
30994d0465 | ||
|
|
71fdbe7b71 | ||
|
|
86432c5ffe | ||
|
|
4dfa1fb0f8 | ||
|
|
db59a7652f | ||
|
|
417470a81c | ||
|
|
cc639da17e | ||
|
|
f619f4a9bc | ||
|
|
5dffc2a64e | ||
|
|
bb63a8d14c | ||
|
|
c1263cc16d | ||
|
|
49f2e7d70f | ||
|
|
f93b535f70 | ||
|
|
e16d3c823b | ||
|
|
7a6fdbcf50 | ||
|
|
46041a3762 | ||
|
|
20b0553f7f | ||
|
|
5870cacf44 | ||
|
|
cb512cd98d | ||
|
|
90487eb7b7 | ||
|
|
706fa77de3 | ||
|
|
26b14ded58 | ||
|
|
3b1367dd8e | ||
|
|
e1f310317d | ||
|
|
937814ec5d | ||
|
|
b55fc649a6 | ||
|
|
f4e4506517 | ||
|
|
287a0b6669 | ||
|
|
5bddd352af | ||
|
|
9c6ea8fb9d | ||
|
|
32e8bf46a7 | ||
|
|
d61fe1bf84 | ||
|
|
47dbea947d | ||
|
|
97c6b0495e | ||
|
|
a602ece8e9 | ||
|
|
cf6dca84d8 | ||
|
|
79432ff8ad | ||
|
|
b8613431de | ||
|
|
698eafa562 | ||
|
|
d33886db89 | ||
|
|
cde09d3a59 | ||
|
|
5674f0e980 | ||
|
|
b9ade60eb2 | ||
|
|
96303723fa | ||
|
|
0f5dbc5788 | ||
|
|
8df0c3a439 | ||
|
|
ca6a86816a | ||
|
|
3ea939798f | ||
|
|
d947410e3c | ||
|
|
db59bc08ac | ||
|
|
95a8649f79 | ||
|
|
ffd10122ed | ||
|
|
c4c39f36e9 | ||
|
|
96900c3cbf | ||
|
|
69fa370d12 | ||
|
|
7496437d11 | ||
|
|
33b7d09af7 | ||
|
|
53d0974760 | ||
|
|
a87399f223 | ||
|
|
975dfb17e1 | ||
|
|
8b8bd0289b | ||
|
|
3bb69c623b | ||
|
|
dd427bdbef | ||
|
|
b40657a14a | ||
|
|
21ca5b2eec | ||
|
|
b78d584d8a | ||
|
|
e6b666a66a | ||
|
|
bab93ea4f5 | ||
|
|
7fe98253ae | ||
|
|
13385cbced | ||
|
|
3f20cec7b2 | ||
|
|
a0e4b020ca | ||
|
|
ea5d898b27 | ||
|
|
4e652b5ccd | ||
|
|
dd809896c8 | ||
|
|
93536d3365 | ||
|
|
098b18fe6d | ||
|
|
66efdac757 | ||
|
|
45545d3815 | ||
|
|
b65d41731b | ||
|
|
be19e97518 | ||
|
|
2ebf2b99bd | ||
|
|
be79ac2eb2 | ||
|
|
05afec3236 | ||
|
|
193a312b22 | ||
|
|
6a2d2ebfd1 | ||
|
|
6d200aa340 | ||
|
|
a0fbb90048 | ||
|
|
08e29e7077 | ||
|
|
3bef71f5f2 | ||
|
|
6175eb7c08 | ||
|
|
ebb9d9329a | ||
|
|
843722f82e | ||
|
|
ce79eaada8 |
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
language: scala
|
||||||
|
scala:
|
||||||
|
- 2.11.2
|
||||||
40
README.md
40
README.md
@@ -1,4 +1,4 @@
|
|||||||
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
|
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://travis-ci.org/takezoe/gitbucket)
|
||||||
=========
|
=========
|
||||||
|
|
||||||
GitBucket is the easily installable Github clone written with Scala.
|
GitBucket is the easily installable Github clone written with Scala.
|
||||||
@@ -23,7 +23,6 @@ The current version of GitBucket provides a basic features below:
|
|||||||
|
|
||||||
Following features are not implemented, but we will make them in the future release!
|
Following features are not implemented, but we will make them in the future release!
|
||||||
|
|
||||||
- Comment for the changeset
|
|
||||||
- Network graph
|
- Network graph
|
||||||
- Statistics
|
- Statistics
|
||||||
- Watch / Star
|
- Watch / Star
|
||||||
@@ -80,6 +79,43 @@ Run the following commands in `Terminal` to
|
|||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
--------
|
--------
|
||||||
|
### 2.7 - 29 Dec 2014
|
||||||
|
- Comment for commit and diff
|
||||||
|
- Fix security issue in markdown rendering
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.6 - 24 Nov 2014
|
||||||
|
- Search box at issues and pull requests
|
||||||
|
- Information from administrator
|
||||||
|
- Pull request UI has been updated
|
||||||
|
- Move to TravisCI from Buildhive
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.5 - 4 Nov 2014
|
||||||
|
- New Dashboard
|
||||||
|
- Change datetime format
|
||||||
|
- Create branch from Web UI
|
||||||
|
- Task list in Markdown
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 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
13
contrib/README.md
Normal 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
62
contrib/gitbucket.conf
Normal 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
|
||||||
|
|
||||||
@@ -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:
|
||||||
case "$1" in
|
StartService() {
|
||||||
start)
|
|
||||||
start
|
start
|
||||||
;;
|
}
|
||||||
stop)
|
|
||||||
|
StopService() {
|
||||||
stop
|
stop
|
||||||
;;
|
}
|
||||||
restart)
|
|
||||||
|
RestartService() {
|
||||||
restart
|
restart
|
||||||
;;
|
}
|
||||||
status)
|
|
||||||
status -p $PID_FILE java
|
|
||||||
RETVAL=$?
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo $"Usage: $0 [start|stop|restart|status]"
|
|
||||||
RETVAL=2
|
|
||||||
esac
|
|
||||||
|
|
||||||
|
|
||||||
exit $RETVAL
|
if [ `isMac` ]; then
|
||||||
|
RunService "$1"
|
||||||
|
else
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status -p $PID_FILE java
|
||||||
|
RETVAL=$?
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo $"Usage: $0 [start|stop|restart|status]"
|
||||||
|
RETVAL=2
|
||||||
|
esac
|
||||||
|
exit $RETVAL
|
||||||
|
fi
|
||||||
|
|
||||||
69
contrib/install
Executable file
69
contrib/install
Executable 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
|
||||||
15
contrib/linux/redhat/README.md
Normal file
15
contrib/linux/redhat/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Contrib Notes #
|
||||||
|
|
||||||
|
RPM spec file and init script for Red Hat Enterprise Linux 6.x.
|
||||||
|
|
||||||
|
To create RPM:
|
||||||
|
1. Edit `../../gitbucket.conf` to suit.
|
||||||
|
2. Edit `gitbucket.init` to suit.
|
||||||
|
3. Edit `gitbucket.spec` to suit.
|
||||||
|
4. Place `gitbucket.spec` to rpm/SPECS/.
|
||||||
|
5. Place `gitbucket.init` and `gitbucket.war` to rpm/SOURCES/.
|
||||||
|
6. Execute `rpmbuild -ba rpm/SPECS/gitbucket.spec`
|
||||||
|
|
||||||
|
This rpm runs gitbucket not as root user but as gitbucket user.
|
||||||
|
This rpm creates user and group named `gitbucket` at installation.
|
||||||
|
This rpm make chkconfig of gitbucket to be on.
|
||||||
108
contrib/linux/redhat/gitbucket.init
Normal file
108
contrib/linux/redhat/gitbucket.init
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# RedHat: /etc/rc.d/init.d/gitbucket
|
||||||
|
#
|
||||||
|
# Starts the GitBucket server
|
||||||
|
#
|
||||||
|
# chkconfig: 345 60 40
|
||||||
|
# description: Run GitBucket server
|
||||||
|
# processname: java
|
||||||
|
|
||||||
|
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||||
|
|
||||||
|
# Pull in cq settings
|
||||||
|
[ -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
|
||||||
|
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
|
||||||
|
|
||||||
|
RED='\033[1m\E[37;41m'
|
||||||
|
GREEN='\033[1m\E[37;42m'
|
||||||
|
OFF='\E[0m'
|
||||||
|
|
||||||
|
RETVAL=0
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo -n $"Starting GitBucket server: "
|
||||||
|
|
||||||
|
START_OPTS=
|
||||||
|
if [ $GITBUCKET_PORT ]; then
|
||||||
|
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_PREFIX ]; then
|
||||||
|
START_OPTS="${START_OPTS} --prefix=${GITBUCKET_PREFIX}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_HOST ]; then
|
||||||
|
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITBUCKET_HOME="${GITBUCKET_HOME}" daemon --user=gitbucket java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
|
||||||
|
sleep 3
|
||||||
|
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "Success"
|
||||||
|
else
|
||||||
|
failure "Exit code $RETVAL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
echo -n $"Stopping GitBucket server: "
|
||||||
|
|
||||||
|
# Run the Java process
|
||||||
|
pkill -f $GITBUCKET_WAR_FILE >>$LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "GitBucket stopping"
|
||||||
|
else
|
||||||
|
failure "GitBucket stopping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
if [ $RETVAL -eq 0 ]; then
|
||||||
|
echo $"GitBucket is running...."
|
||||||
|
else
|
||||||
|
echo $"GitBucket is stopped"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo $"Usage: $0 [start|stop|restart|status]"
|
||||||
|
RETVAL=2
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit $RETVAL
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Name: gitbucket
|
Name: gitbucket
|
||||||
Summary: GitHub clone written with Scala.
|
Summary: GitHub clone written with Scala.
|
||||||
Version: 1.7
|
Version: 2.6
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
License: Apache
|
License: Apache
|
||||||
URL: https://github.com/takezoe/gitbucket
|
URL: https://github.com/takezoe/gitbucket
|
||||||
@@ -26,6 +26,25 @@ GitBucket is the easily installable GitHub clone written with Scala.
|
|||||||
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
|
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
|
||||||
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
||||||
|
|
||||||
|
%pre
|
||||||
|
/usr/sbin/groupadd -r gitbucket &> /dev/null || :
|
||||||
|
/usr/sbin/useradd -g gitbucket -s /bin/false -r -c "GitBucket GitHub clone" -d %{_sharedstatedir}/%{name} gitbucket &> /dev/null || :
|
||||||
|
|
||||||
|
%post
|
||||||
|
/sbin/chkconfig --add gitbucket
|
||||||
|
|
||||||
|
%preun
|
||||||
|
if [ "$1" = 0 ]; then
|
||||||
|
/sbin/service gitbucket stop > /dev/null 2>&1
|
||||||
|
/sbin/chkconfig --del gitbucket
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
%postun
|
||||||
|
if [ "$1" -ge 1 ]; then
|
||||||
|
/sbin/service gitbucket restart > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
||||||
%clean
|
%clean
|
||||||
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
||||||
@@ -34,12 +53,28 @@ touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
|||||||
%files
|
%files
|
||||||
%defattr(-,root,root,-)
|
%defattr(-,root,root,-)
|
||||||
%{_datarootdir}/%{name}/lib/%{name}.war
|
%{_datarootdir}/%{name}/lib/%{name}.war
|
||||||
%{_sysconfdir}/init.d/%{name}
|
%config %{_sysconfdir}/init.d/%{name}
|
||||||
%config %{_sysconfdir}/sysconfig/%{name}
|
%config(noreplace) %{_sysconfdir}/sysconfig/%{name}
|
||||||
%{_localstatedir}/log/%{name}/run.log
|
%attr(0755,gitbucket,gitbucket) %{_sharedstatedir}/%{name}
|
||||||
|
%attr(0750,gitbucket,gitbucket) %{_localstatedir}/log/%{name}
|
||||||
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Mon Nov 24 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.6
|
||||||
|
|
||||||
|
* Sun Nov 09 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.5
|
||||||
|
|
||||||
|
* Sun Oct 26 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.4.1
|
||||||
|
|
||||||
|
* Mon Jul 21 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- execute as gitbucket user
|
||||||
|
|
||||||
|
* Sun Jul 20 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.1.
|
||||||
|
|
||||||
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
||||||
- Version bump to v1.7.
|
- Version bump to v1.7.
|
||||||
|
|
||||||
14
contrib/macosx/gitbucket.plist → contrib/macosx/makePlist
Normal file → Executable file
14
contrib/macosx/gitbucket.plist → contrib/macosx/makePlist
Normal file → Executable 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
|
||||||
@@ -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=
|
|
||||||
2451
etc/icons.svg
2451
etc/icons.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 78 KiB |
@@ -1 +1 @@
|
|||||||
sbt.version=0.13.1
|
sbt.version=0.13.5
|
||||||
|
|||||||
@@ -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,46 +14,47 @@ object MyBuild extends Build {
|
|||||||
|
|
||||||
lazy val project = Project (
|
lazy val project = Project (
|
||||||
"gitbucket",
|
"gitbucket",
|
||||||
file("."),
|
file(".")
|
||||||
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
|
|
||||||
sourcesInBase := false,
|
|
||||||
organization := Organization,
|
|
||||||
name := Name,
|
|
||||||
version := Version,
|
|
||||||
scalaVersion := ScalaVersion,
|
|
||||||
resolvers ++= Seq(
|
|
||||||
Classpaths.typesafeReleases,
|
|
||||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
|
||||||
),
|
|
||||||
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
|
||||||
libraryDependencies ++= Seq(
|
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r",
|
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r",
|
|
||||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
|
||||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
|
||||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
|
||||||
"org.json4s" %% "json4s-jackson" % "3.2.10",
|
|
||||||
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
|
|
||||||
"commons-io" % "commons-io" % "2.4",
|
|
||||||
"org.pegdown" % "pegdown" % "1.4.1",
|
|
||||||
"org.apache.commons" % "commons-compress" % "1.5",
|
|
||||||
"org.apache.commons" % "commons-email" % "1.3.1",
|
|
||||||
"org.apache.httpcomponents" % "httpclient" % "4.3",
|
|
||||||
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
|
||||||
"com.typesafe.slick" %% "slick" % "2.1.0-RC3",
|
|
||||||
"org.mozilla" % "rhino" % "1.7R4",
|
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
|
||||||
"org.quartz-scheduler" % "quartz" % "2.2.1",
|
|
||||||
"com.h2database" % "h2" % "1.4.180",
|
|
||||||
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
|
||||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
|
||||||
"junit" % "junit" % "4.11" % "test"
|
|
||||||
),
|
|
||||||
EclipseKeys.withSource := true,
|
|
||||||
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
|
|
||||||
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
|
|
||||||
packageOptions += Package.MainClass("JettyLauncher")
|
|
||||||
) ++ seq(Twirl.settings: _*)
|
|
||||||
)
|
)
|
||||||
|
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
|
||||||
|
.settings(
|
||||||
|
sourcesInBase := false,
|
||||||
|
organization := Organization,
|
||||||
|
name := Name,
|
||||||
|
version := Version,
|
||||||
|
scalaVersion := ScalaVersion,
|
||||||
|
resolvers ++= Seq(
|
||||||
|
Classpaths.typesafeReleases,
|
||||||
|
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||||
|
),
|
||||||
|
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||||
|
libraryDependencies ++= Seq(
|
||||||
|
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r",
|
||||||
|
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r",
|
||||||
|
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||||
|
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||||
|
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||||
|
"org.json4s" %% "json4s-jackson" % "3.2.10",
|
||||||
|
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
|
||||||
|
"commons-io" % "commons-io" % "2.4",
|
||||||
|
"org.pegdown" % "pegdown" % "1.4.1",
|
||||||
|
"org.apache.commons" % "commons-compress" % "1.5",
|
||||||
|
"org.apache.commons" % "commons-email" % "1.3.1",
|
||||||
|
"org.apache.httpcomponents" % "httpclient" % "4.3",
|
||||||
|
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||||
|
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||||
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
|
"org.quartz-scheduler" % "quartz" % "2.2.1",
|
||||||
|
"com.h2database" % "h2" % "1.4.180",
|
||||||
|
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||||
|
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
||||||
|
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
||||||
|
"junit" % "junit" % "4.11" % "test",
|
||||||
|
"com.typesafe.play" %% "twirl-compiler" % "1.0.2"
|
||||||
|
),
|
||||||
|
EclipseKeys.withSource := true,
|
||||||
|
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
|
||||||
|
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
|
||||||
|
packageOptions += Package.MainClass("JettyLauncher")
|
||||||
|
).enablePlugins(SbtTwirl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Binary file not shown.
2
sbt.bat
2
sbt.bat
@@ -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
3
sbt.sh
@@ -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 "$@"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/main/resources/update/2_3.sql
Normal file
6
src/main/resources/update/2_3.sql
Normal 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);
|
||||||
18
src/main/resources/update/2_7.sql
Normal file
18
src/main/resources/update/2_7.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE COMMIT_COMMENT (
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
COMMIT_ID VARCHAR(100) NOT NULL,
|
||||||
|
COMMENT_ID INT AUTO_INCREMENT,
|
||||||
|
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
CONTENT TEXT NOT NULL,
|
||||||
|
FILE_NAME NVARCHAR(100),
|
||||||
|
OLD_LINE_NUMBER INT,
|
||||||
|
NEW_LINE_NUMBER INT,
|
||||||
|
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||||
|
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||||
|
PULL_REQUEST BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, COMMENT_ID);
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.{UsersAuthenticator, Keys}
|
import util.{StringUtil, UsersAuthenticator, Keys}
|
||||||
import util.Implicits._
|
import util.Implicits._
|
||||||
|
import service.IssuesService.IssueSearchCondition
|
||||||
|
|
||||||
class DashboardController extends DashboardControllerBase
|
class DashboardController extends DashboardControllerBase
|
||||||
with IssuesService with PullRequestService with RepositoryService with AccountService
|
with IssuesService with PullRequestService with RepositoryService with AccountService
|
||||||
with UsersAuthenticator
|
with UsersAuthenticator
|
||||||
|
|
||||||
trait DashboardControllerBase extends ControllerBase {
|
trait DashboardControllerBase extends ControllerBase {
|
||||||
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
|
self: IssuesService with PullRequestService with RepositoryService with AccountService
|
||||||
|
with UsersAuthenticator =>
|
||||||
|
|
||||||
get("/dashboard/issues/repos")(usersOnly {
|
get("/dashboard/issues")(usersOnly {
|
||||||
searchIssues("all")
|
val q = request.getParameter("q")
|
||||||
|
val account = context.loginAccount.get
|
||||||
|
Option(q).map { q =>
|
||||||
|
val condition = IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
q match {
|
||||||
|
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
|
||||||
|
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
|
||||||
|
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
|
||||||
|
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
|
||||||
|
case _ => searchIssues("created_by")
|
||||||
|
}
|
||||||
|
} getOrElse {
|
||||||
|
searchIssues("created_by")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/issues/assigned")(usersOnly {
|
get("/dashboard/issues/assigned")(usersOnly {
|
||||||
@@ -23,87 +38,99 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
searchIssues("created_by")
|
searchIssues("created_by")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/dashboard/issues/mentioned")(usersOnly {
|
||||||
|
searchIssues("mentioned")
|
||||||
|
})
|
||||||
|
|
||||||
get("/dashboard/pulls")(usersOnly {
|
get("/dashboard/pulls")(usersOnly {
|
||||||
searchPullRequests("created_by", None)
|
val q = request.getParameter("q")
|
||||||
|
val account = context.loginAccount.get
|
||||||
|
Option(q).map { q =>
|
||||||
|
val condition = IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
q match {
|
||||||
|
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
|
||||||
|
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
|
||||||
|
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
|
||||||
|
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
|
||||||
|
case _ => searchPullRequests("created_by")
|
||||||
|
}
|
||||||
|
} getOrElse {
|
||||||
|
searchPullRequests("created_by")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/pulls/owned")(usersOnly {
|
get("/dashboard/pulls/created_by")(usersOnly {
|
||||||
searchPullRequests("created_by", None)
|
searchPullRequests("created_by")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/pulls/public")(usersOnly {
|
get("/dashboard/pulls/assigned")(usersOnly {
|
||||||
searchPullRequests("not_created_by", None)
|
searchPullRequests("assigned")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
|
get("/dashboard/pulls/mentioned")(usersOnly {
|
||||||
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
|
searchPullRequests("mentioned")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private def getOrCreateCondition(key: String, filter: String, userName: String) = {
|
||||||
|
val condition = session.putAndGet(key, if(request.hasQueryString){
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
if(q == null){
|
||||||
|
IssueSearchCondition(request)
|
||||||
|
} else {
|
||||||
|
IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
}
|
||||||
|
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
|
||||||
|
|
||||||
|
filter match {
|
||||||
|
case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
|
||||||
|
case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
|
||||||
|
case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def searchIssues(filter: String) = {
|
private def searchIssues(filter: String) = {
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
|
|
||||||
// condition
|
val userName = context.loginAccount.get.userName
|
||||||
val condition = session.putAndGet(Keys.Session.DashboardIssues,
|
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
|
||||||
if(request.hasQueryString) IssueSearchCondition(request)
|
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
||||||
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
|
val page = IssueSearchCondition.page(request)
|
||||||
)
|
|
||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
|
||||||
val filterUser = Map(filter -> userName)
|
|
||||||
val page = IssueSearchCondition.page(request)
|
|
||||||
|
|
||||||
dashboard.html.issues(
|
dashboard.html.issues(
|
||||||
issues.html.listparts(
|
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
||||||
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
page,
|
||||||
page,
|
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
|
||||||
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*),
|
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
|
||||||
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
|
filter match {
|
||||||
condition),
|
case "assigned" => condition.copy(assigned = Some(userName))
|
||||||
countIssue(condition, Map.empty, false, userRepos: _*),
|
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||||
countIssue(condition, Map("assigned" -> userName), false, userRepos: _*),
|
case _ => condition.copy(author = Some(userName))
|
||||||
countIssue(condition, Map("created_by" -> userName), false, userRepos: _*),
|
},
|
||||||
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
|
filter,
|
||||||
condition,
|
getGroupNames(userName))
|
||||||
filter)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchPullRequests(filter: String, repository: Option[String]) = {
|
private def searchPullRequests(filter: String) = {
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
import PullRequestService._
|
import PullRequestService._
|
||||||
|
|
||||||
// condition
|
val userName = context.loginAccount.get.userName
|
||||||
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
|
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
|
||||||
if(request.hasQueryString) IssueSearchCondition(request)
|
val allRepos = getAllRepositories(userName)
|
||||||
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
|
val page = IssueSearchCondition.page(request)
|
||||||
}.copy(repo = repository))
|
|
||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
val allRepos = getAllRepositories()
|
|
||||||
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
|
||||||
val filterUser = Map(filter -> userName)
|
|
||||||
val page = IssueSearchCondition.page(request)
|
|
||||||
|
|
||||||
val counts = countIssueGroupByRepository(
|
|
||||||
IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*)
|
|
||||||
|
|
||||||
dashboard.html.pulls(
|
dashboard.html.pulls(
|
||||||
pulls.html.listparts(
|
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
||||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
page,
|
||||||
page,
|
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
|
||||||
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*),
|
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
|
||||||
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
|
filter match {
|
||||||
condition,
|
case "assigned" => condition.copy(assigned = Some(userName))
|
||||||
None,
|
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||||
false),
|
case _ => condition.copy(author = Some(userName))
|
||||||
getPullRequestCountGroupByUser(condition.state == "closed", None, None),
|
},
|
||||||
userRepos.map { case (userName, repoName) =>
|
filter,
|
||||||
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
|
getGroupNames(userName))
|
||||||
}.sortBy(_._3).reverse,
|
|
||||||
condition,
|
|
||||||
filter)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,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 +31,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()))
|
)(x => x)
|
||||||
)(IssueEditForm.apply)
|
|
||||||
|
|
||||||
val commentForm = mapping(
|
val commentForm = mapping(
|
||||||
"issueId" -> label("Issue Id", number()),
|
"issueId" -> label("Issue Id", number()),
|
||||||
@@ -47,16 +48,13 @@ 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", _)
|
val q = request.getParameter("q")
|
||||||
})
|
if(Option(q).exists(_.contains("is:pr"))){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
|
||||||
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
|
} else {
|
||||||
searchIssues("assigned", _)
|
searchIssues(repository)
|
||||||
})
|
}
|
||||||
|
|
||||||
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 +123,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,13 +193,13 @@ 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(
|
||||||
Map("title" -> x.title,
|
Map("title" -> x.title,
|
||||||
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
|
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
|
||||||
repository, false, true)
|
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else Unauthorized
|
} else Unauthorized
|
||||||
@@ -203,7 +216,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
contentType = formats("json")
|
contentType = formats("json")
|
||||||
org.json4s.jackson.Serialization.write(
|
org.json4s.jackson.Serialization.write(
|
||||||
Map("content" -> view.Markdown.toHtml(x.content,
|
Map("content" -> view.Markdown.toHtml(x.content,
|
||||||
repository, false, true)
|
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else Unauthorized
|
} else Unauthorized
|
||||||
@@ -234,15 +247,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 +307,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,15 +336,15 @@ 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)
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
.getOrElse(None -> None)
|
.getOrElse(None -> None)
|
||||||
|
|
||||||
val commentId = content
|
val commentId = content
|
||||||
@@ -336,7 +354,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,32 +387,33 @@ 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)
|
||||||
|
|
||||||
// retrieve search condition
|
// retrieve search condition
|
||||||
val condition = session.putAndGet(sessionKey,
|
val condition = session.putAndGet(sessionKey,
|
||||||
if(request.hasQueryString) IssueSearchCondition(request)
|
if(request.hasQueryString){
|
||||||
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
val q = request.getParameter("q")
|
||||||
|
if(q == null){
|
||||||
|
IssueSearchCondition(request)
|
||||||
|
} else {
|
||||||
|
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
|
||||||
|
}
|
||||||
|
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||||
)
|
)
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(), Map.empty),
|
||||||
|
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(), Map.empty),
|
||||||
|
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(), Map.empty),
|
||||||
|
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()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
|
import util._
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import util.Implicits._
|
import util.Implicits._
|
||||||
import util.ControlUtil._
|
import util.ControlUtil._
|
||||||
@@ -12,20 +12,21 @@ import scala.collection.JavaConverters._
|
|||||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||||
import service.IssuesService._
|
import service.IssuesService._
|
||||||
import service.PullRequestService._
|
import service.PullRequestService._
|
||||||
import util.JGitUtil.DiffInfo
|
|
||||||
import util.JGitUtil.CommitInfo
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.eclipse.jgit.merge.MergeStrategy
|
import org.eclipse.jgit.merge.MergeStrategy
|
||||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||||
import service.WebHookService.WebHookPayload
|
import service.WebHookService.WebHookPayload
|
||||||
|
import util.JGitUtil.DiffInfo
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
|
||||||
|
|
||||||
class PullRequestsController extends PullRequestsControllerBase
|
class PullRequestsController extends PullRequestsControllerBase
|
||||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||||
with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with CommitsService with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
trait PullRequestsControllerBase extends ControllerBase {
|
trait PullRequestsControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||||
with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
with CommitsService with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||||
|
|
||||||
@@ -59,11 +60,12 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
case class MergeForm(message: String)
|
case class MergeForm(message: String)
|
||||||
|
|
||||||
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||||
searchPullRequests(None, repository)
|
val q = request.getParameter("q")
|
||||||
})
|
if(Option(q).exists(_.contains("is:issue"))){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
|
||||||
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
|
} else {
|
||||||
searchPullRequests(Some(params("userName")), repository)
|
searchPullRequests(None, repository)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||||
@@ -77,7 +79,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
pulls.html.pullreq(
|
pulls.html.pullreq(
|
||||||
issue, pullreq,
|
issue, pullreq,
|
||||||
getComments(owner, name, issueId),
|
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
|
||||||
|
.sortWith((a, b) => a.registeredDate before b.registeredDate),
|
||||||
getIssueLabels(owner, name, issueId),
|
getIssueLabels(owner, name, issueId),
|
||||||
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
getMilestonesWithIssueCount(owner, name),
|
getMilestonesWithIssueCount(owner, name),
|
||||||
@@ -277,6 +280,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||||
},
|
},
|
||||||
|
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||||
originBranch,
|
originBranch,
|
||||||
forkedBranch,
|
forkedBranch,
|
||||||
oldId.getName,
|
oldId.getName,
|
||||||
@@ -453,7 +457,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 +466,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))
|
||||||
|
|||||||
@@ -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, form.repositoryName))) { 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")
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ import org.eclipse.jgit.revwalk.RevCommit
|
|||||||
import service.WebHookService.WebHookPayload
|
import service.WebHookService.WebHookPayload
|
||||||
|
|
||||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||||
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
|
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The repository viewer.
|
* The repository viewer.
|
||||||
*/
|
*/
|
||||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
|
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||||
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||||
@@ -52,6 +52,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
fileName: String
|
fileName: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case class CommentForm(
|
||||||
|
fileName: Option[String],
|
||||||
|
oldLineNumber: Option[Int],
|
||||||
|
newLineNumber: Option[Int],
|
||||||
|
content: String,
|
||||||
|
pullRequest: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
val editorForm = mapping(
|
val editorForm = mapping(
|
||||||
"branch" -> trim(label("Branch", text(required))),
|
"branch" -> trim(label("Branch", text(required))),
|
||||||
"path" -> trim(label("Path", text())),
|
"path" -> trim(label("Path", text())),
|
||||||
@@ -70,6 +78,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
"fileName" -> trim(label("Filename", text(required)))
|
"fileName" -> trim(label("Filename", text(required)))
|
||||||
)(DeleteForm.apply)
|
)(DeleteForm.apply)
|
||||||
|
|
||||||
|
val commentForm = mapping(
|
||||||
|
"fileName" -> trim(label("Filename", optional(text()))),
|
||||||
|
"oldLineNumber" -> trim(label("Old line number", optional(number()))),
|
||||||
|
"newLineNumber" -> trim(label("New line number", optional(number()))),
|
||||||
|
"content" -> trim(label("Content", text(required))),
|
||||||
|
"pullRequest" -> trim(label("In pull request", boolean()))
|
||||||
|
)(CommentForm.apply)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns converted HTML from Markdown for preview.
|
* Returns converted HTML from Markdown for preview.
|
||||||
*/
|
*/
|
||||||
@@ -77,7 +93,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
contentType = "text/html"
|
contentType = "text/html"
|
||||||
view.helpers.markdown(params("content"), repository,
|
view.helpers.markdown(params("content"), repository,
|
||||||
params("enableWikiLink").toBoolean,
|
params("enableWikiLink").toBoolean,
|
||||||
params("enableRefsLink").toBoolean)
|
params("enableRefsLink").toBoolean,
|
||||||
|
params("enableTaskList").toBoolean,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +130,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
||||||
logs.splitWith{ (commit1, commit2) =>
|
logs.splitWith{ (commit1, commit2) =>
|
||||||
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||||
}, page, hasNext)
|
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
case Left(_) => NotFound
|
case Left(_) => NotFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +209,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 +219,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
|
||||||
}
|
}
|
||||||
@@ -218,12 +237,82 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||||
repository, diffs, oldCommitId)
|
getCommitComments(repository.owner, repository.name, id, false),
|
||||||
|
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
val id = params("id")
|
||||||
|
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
|
||||||
|
form.fileName, form.oldLineNumber, form.newLineNumber, form.pullRequest)
|
||||||
|
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository =>
|
||||||
|
val id = params("id")
|
||||||
|
val fileName = params.get("fileName")
|
||||||
|
val oldLineNumber = params.get("oldLineNumber") flatMap {b => Some(b.toInt)}
|
||||||
|
val newLineNumber = params.get("newLineNumber") flatMap {b => Some(b.toInt)}
|
||||||
|
val pullRequest = params.get("pullRequest")
|
||||||
|
repo.html.commentform(
|
||||||
|
commitId = id,
|
||||||
|
fileName, oldLineNumber, newLineNumber, pullRequest.map(_.toBoolean).getOrElse(false),
|
||||||
|
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||||
|
repository = repository
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
val id = params("id")
|
||||||
|
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
|
||||||
|
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.pullRequest)
|
||||||
|
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
||||||
|
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
|
||||||
|
getCommitComment(repository.owner, repository.name, params("id")) map { x =>
|
||||||
|
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
||||||
|
params.get("dataType") collect {
|
||||||
|
case t if t == "html" => repo.html.editcomment(
|
||||||
|
x.content, x.commentId, x.userName, x.repositoryName)
|
||||||
|
} getOrElse {
|
||||||
|
contentType = formats("json")
|
||||||
|
org.json4s.jackson.Serialization.write(
|
||||||
|
Map("content" -> view.Markdown.toHtml(x.content,
|
||||||
|
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getCommitComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
updateCommitComment(comment.commentId, form.content)
|
||||||
|
redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}")
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit_comments/delete/:id")(readableUsersOnly { repository =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getCommitComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
Ok(deleteCommitComment(comment.commentId))
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays branches.
|
* Displays branches.
|
||||||
*/
|
*/
|
||||||
@@ -238,6 +327,24 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a branch.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/branches")(collaboratorsOnly { repository =>
|
||||||
|
val newBranchName = params.getOrElse("new", halt(400))
|
||||||
|
val fromBranchName = params.getOrElse("from", halt(400))
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
JGitUtil.createBranch(git, fromBranchName, newBranchName)
|
||||||
|
} match {
|
||||||
|
case Right(message) =>
|
||||||
|
flash += "info" -> message
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}")
|
||||||
|
case Left(message) =>
|
||||||
|
flash += "error" -> message
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes branch.
|
* Deletes branch.
|
||||||
*/
|
*/
|
||||||
@@ -311,10 +418,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,8 +436,9 @@ 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),
|
||||||
|
flash.get("info"), flash.get("error"))
|
||||||
}
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
}
|
}
|
||||||
@@ -350,7 +458,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 +473,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()
|
||||||
@@ -439,4 +547,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
file
|
file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||||
|
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -20,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
private val form = mapping(
|
private val form = mapping(
|
||||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||||
|
"information" -> trim(label("Information", optional(text()))),
|
||||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||||
"notification" -> trim(label("Notification", boolean())),
|
"notification" -> trim(label("Notification", boolean())),
|
||||||
@@ -84,42 +86,56 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/plugins")(adminOnly {
|
get("/admin/plugins")(adminOnly {
|
||||||
val installedPlugins = plugin.PluginSystem.plugins
|
if(enablePluginSystem){
|
||||||
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
|
val installedPlugins = plugin.PluginSystem.plugins
|
||||||
admin.plugins.html.installed(installedPlugins, updatablePlugins)
|
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
|
||||||
|
admin.plugins.html.installed(installedPlugins, updatablePlugins)
|
||||||
|
} else NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
|
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
|
||||||
deletePlugins(form.pluginIds)
|
if(enablePluginSystem){
|
||||||
installPlugins(form.pluginIds)
|
deletePlugins(form.pluginIds)
|
||||||
redirect("/admin/plugins")
|
installPlugins(form.pluginIds)
|
||||||
|
redirect("/admin/plugins")
|
||||||
|
} else NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
|
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
|
||||||
deletePlugins(form.pluginIds)
|
if(enablePluginSystem){
|
||||||
redirect("/admin/plugins")
|
deletePlugins(form.pluginIds)
|
||||||
|
redirect("/admin/plugins")
|
||||||
|
} else NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/plugins/available")(adminOnly {
|
get("/admin/plugins/available")(adminOnly {
|
||||||
val installedPlugins = plugin.PluginSystem.plugins
|
if(enablePluginSystem){
|
||||||
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
|
val installedPlugins = plugin.PluginSystem.plugins
|
||||||
admin.plugins.html.available(availablePlugins)
|
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
|
||||||
|
admin.plugins.html.available(availablePlugins)
|
||||||
|
} else NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
|
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
|
||||||
installPlugins(form.pluginIds)
|
if(enablePluginSystem){
|
||||||
redirect("/admin/plugins")
|
installPlugins(form.pluginIds)
|
||||||
|
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 +154,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
|||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
"removed" -> trim(label("Disable" ,boolean()))
|
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
|
||||||
)(EditUserForm.apply)
|
)(EditUserForm.apply)
|
||||||
|
|
||||||
val newGroupForm = mapping(
|
val newGroupForm = mapping(
|
||||||
@@ -190,4 +190,14 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
params.get(paramName).flatMap { userName =>
|
||||||
|
if(userName == context.loginAccount.get.userName)
|
||||||
|
Some("You can't disable your account yourself")
|
||||||
|
else
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,11 @@ protected[model] trait TemplateComponent { self: Profile =>
|
|||||||
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
|
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait CommitTemplate extends BasicTemplate { self: Table[_] =>
|
||||||
|
val commitId = column[String]("COMMIT_ID")
|
||||||
|
|
||||||
|
def byCommit(owner: String, repository: String, commitId: String) =
|
||||||
|
byRepository(owner, repository) && (this.commitId === commitId)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/main/scala/model/Comment.scala
Normal file
78
src/main/scala/model/Comment.scala
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
trait Comment {
|
||||||
|
val commentedUserName: String
|
||||||
|
val registeredDate: java.util.Date
|
||||||
|
}
|
||||||
|
|
||||||
|
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import self._
|
||||||
|
|
||||||
|
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
|
||||||
|
def autoInc = this returning this.map(_.commentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
|
||||||
|
val commentId = column[Int]("COMMENT_ID", O AutoInc)
|
||||||
|
val action = column[String]("ACTION")
|
||||||
|
val commentedUserName = column[String]("COMMENTED_USER_NAME")
|
||||||
|
val content = column[String]("CONTENT")
|
||||||
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
|
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class IssueComment (
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
issueId: Int,
|
||||||
|
commentId: Int = 0,
|
||||||
|
action: String,
|
||||||
|
commentedUserName: String,
|
||||||
|
content: String,
|
||||||
|
registeredDate: java.util.Date,
|
||||||
|
updatedDate: java.util.Date
|
||||||
|
) extends Comment
|
||||||
|
|
||||||
|
trait CommitCommentComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import self._
|
||||||
|
|
||||||
|
lazy val CommitComments = new TableQuery(tag => new CommitComments(tag)){
|
||||||
|
def autoInc = this returning this.map(_.commentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommitComments(tag: Tag) extends Table[CommitComment](tag, "COMMIT_COMMENT") with CommitTemplate {
|
||||||
|
val commentId = column[Int]("COMMENT_ID", O AutoInc)
|
||||||
|
val commentedUserName = column[String]("COMMENTED_USER_NAME")
|
||||||
|
val content = column[String]("CONTENT")
|
||||||
|
val fileName = column[Option[String]]("FILE_NAME")
|
||||||
|
val oldLine = column[Option[Int]]("OLD_LINE_NUMBER")
|
||||||
|
val newLine = column[Option[Int]]("NEW_LINE_NUMBER")
|
||||||
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
|
val pullRequest = column[Boolean]("PULL_REQUEST")
|
||||||
|
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class CommitComment(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
commitId: String,
|
||||||
|
commentId: Int = 0,
|
||||||
|
commentedUserName: String,
|
||||||
|
content: String,
|
||||||
|
fileName: Option[String],
|
||||||
|
oldLine: Option[Int],
|
||||||
|
newLine: Option[Int],
|
||||||
|
registeredDate: java.util.Date,
|
||||||
|
updatedDate: java.util.Date,
|
||||||
|
pullRequest: Boolean
|
||||||
|
) extends Comment
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
|
|
||||||
import profile.simple._
|
|
||||||
import self._
|
|
||||||
|
|
||||||
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
|
|
||||||
def autoInc = this returning this.map(_.commentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
|
|
||||||
val commentId = column[Int]("COMMENT_ID", O AutoInc)
|
|
||||||
val action = column[String]("ACTION")
|
|
||||||
val commentedUserName = column[String]("COMMENTED_USER_NAME")
|
|
||||||
val content = column[String]("CONTENT")
|
|
||||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
|
||||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
|
||||||
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
|
|
||||||
|
|
||||||
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case class IssueComment(
|
|
||||||
userName: String,
|
|
||||||
repositoryName: String,
|
|
||||||
issueId: Int,
|
|
||||||
commentId: Int = 0,
|
|
||||||
action: String,
|
|
||||||
commentedUserName: String,
|
|
||||||
content: String,
|
|
||||||
registeredDate: java.util.Date,
|
|
||||||
updatedDate: java.util.Date
|
|
||||||
)
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main/scala/model/Plugin.scala
Normal file
19
src/main/scala/model/Plugin.scala
Normal 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
|
||||||
|
)
|
||||||
@@ -22,6 +22,7 @@ object Profile extends {
|
|||||||
} with AccountComponent
|
} with AccountComponent
|
||||||
with ActivityComponent
|
with ActivityComponent
|
||||||
with CollaboratorComponent
|
with CollaboratorComponent
|
||||||
|
with CommitCommentComponent
|
||||||
with GroupMemberComponent
|
with GroupMemberComponent
|
||||||
with IssueComponent
|
with IssueComponent
|
||||||
with IssueCommentComponent
|
with IssueCommentComponent
|
||||||
@@ -31,7 +32,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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -10,10 +10,11 @@ trait Plugin {
|
|||||||
val url: String
|
val url: String
|
||||||
val description: String
|
val description: String
|
||||||
|
|
||||||
def repositoryMenus : List[RepositoryMenu]
|
def repositoryMenus : List[RepositoryMenu]
|
||||||
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 {
|
||||||
|
|||||||
@@ -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.replace("-", ""),
|
||||||
"url" -> properties.getProperty("url"),
|
file.getName.stripSuffix(".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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
|
// TODO Should PluginSystem provide a way to migrate resources other than H2?
|
||||||
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
|
private def migrate(conn: Connection, pluginId: String, current: String): Unit = {
|
||||||
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
|
val pluginDir = new java.io.File(PluginHome)
|
||||||
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
|
|
||||||
|
// TODO Is ot possible to use this migration system in GitBucket migration?
|
||||||
|
val dim = current.split("\\.")
|
||||||
|
val currentVersion = Version(dim(0).toInt, dim(1).toInt)
|
||||||
|
|
||||||
|
val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql")
|
||||||
|
if(sqlDir.exists && sqlDir.isDirectory){
|
||||||
|
sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file =>
|
||||||
|
val array = file.getName.replaceFirst("\\.sql", "").split("_")
|
||||||
|
Version(array(0).toInt, array(1).toInt)
|
||||||
|
}
|
||||||
|
.sorted.reverse.takeWhile(_ > currentVersion)
|
||||||
|
.reverse.foreach { version =>
|
||||||
|
val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql")
|
||||||
|
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
|
||||||
|
using(conn.createStatement()){ stmt =>
|
||||||
|
stmt.executeUpdate(sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Version(major: Int, minor: Int) extends Ordered[Version] {
|
||||||
|
|
||||||
|
override def compare(that: Version): Int = {
|
||||||
|
if(major != that.major){
|
||||||
|
major.compare(that.major)
|
||||||
|
} else{
|
||||||
|
minor.compare(that.minor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def displayString: String = major + "." + minor
|
||||||
|
}
|
||||||
|
|
||||||
|
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
|
||||||
|
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
|
||||||
|
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
|
||||||
|
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
|
||||||
|
def javaScripts : List[JavaScript] = pluginsMap.values.flatMap(_.javaScripts).toList
|
||||||
|
|
||||||
// Case classes to hold plug-ins information internally in GitBucket
|
// Case 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!"
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 .*", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
36
src/main/scala/plugin/Security.scala
Normal file
36
src/main/scala/plugin/Security.scala
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
56
src/main/scala/plugin/package.scala
Normal file
56
src/main/scala/plugin/package.scala
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -168,6 +168,11 @@ trait AccountService {
|
|||||||
Repositories.filter(_.userName === userName.bind).delete
|
Repositories.filter(_.userName === userName.bind).delete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getGroupNames(userName: String)(implicit s: Session): List[String] = {
|
||||||
|
List(userName) ++
|
||||||
|
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object AccountService extends AccountService
|
object AccountService extends AccountService
|
||||||
|
|||||||
@@ -95,6 +95,15 @@ trait ActivityService {
|
|||||||
Some(cut(comment, 200)),
|
Some(cut(comment, 200)),
|
||||||
currentDate)
|
currentDate)
|
||||||
|
|
||||||
|
def recordCommentCommitActivity(userName: String, repositoryName: String, activityUserName: String, commitId: String, comment: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"comment_commit",
|
||||||
|
s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]",
|
||||||
|
Some(cut(comment, 200)),
|
||||||
|
currentDate
|
||||||
|
)
|
||||||
|
|
||||||
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
|
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
|
||||||
(implicit s: Session): Unit =
|
(implicit s: Session): Unit =
|
||||||
Activities insert Activity(userName, repositoryName, activityUserName,
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
|||||||
52
src/main/scala/service/CommitsService.scala
Normal file
52
src/main/scala/service/CommitsService.scala
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import scala.slick.jdbc.{StaticQuery => Q}
|
||||||
|
import Q.interpolation
|
||||||
|
|
||||||
|
import model.Profile._
|
||||||
|
import profile.simple._
|
||||||
|
import model.CommitComment
|
||||||
|
import util.Implicits._
|
||||||
|
import util.StringUtil._
|
||||||
|
|
||||||
|
|
||||||
|
trait CommitsService {
|
||||||
|
|
||||||
|
def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) =
|
||||||
|
CommitComments filter {
|
||||||
|
t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest)
|
||||||
|
} list
|
||||||
|
|
||||||
|
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||||
|
if (commentId forall (_.isDigit))
|
||||||
|
CommitComments filter { t =>
|
||||||
|
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
|
||||||
|
} firstOption
|
||||||
|
else
|
||||||
|
None
|
||||||
|
|
||||||
|
def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String,
|
||||||
|
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], pullRequest: Boolean)(implicit s: Session): Int =
|
||||||
|
CommitComments.autoInc insert CommitComment(
|
||||||
|
userName = owner,
|
||||||
|
repositoryName = repository,
|
||||||
|
commitId = commitId,
|
||||||
|
commentedUserName = loginUser,
|
||||||
|
content = content,
|
||||||
|
fileName = fileName,
|
||||||
|
oldLine = oldLine,
|
||||||
|
newLine = newLine,
|
||||||
|
registeredDate = currentDate,
|
||||||
|
updatedDate = currentDate,
|
||||||
|
pullRequest = pullRequest)
|
||||||
|
|
||||||
|
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
|
||||||
|
CommitComments
|
||||||
|
.filter (_.byPrimaryKey(commentId))
|
||||||
|
.map { t =>
|
||||||
|
t.content -> t.updatedDate
|
||||||
|
}.update (content, currentDate)
|
||||||
|
|
||||||
|
def deleteCommitComment(commentId: Int)(implicit s: Session) =
|
||||||
|
CommitComments filter (_.byPrimaryKey(commentId)) delete
|
||||||
|
}
|
||||||
@@ -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)*)(implicit s: Session): Int =
|
repos: (String, String)*)(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, condition: IssueSearchCondition,
|
||||||
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
|
filterUser: Map[String, String])(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)
|
||||||
}
|
}
|
||||||
@@ -79,47 +77,22 @@ trait IssuesService {
|
|||||||
}
|
}
|
||||||
.toMap
|
.toMap
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Returns list which contains issue count for each repository.
|
|
||||||
* If the issue does not exist, its repository is not included in the result.
|
|
||||||
*
|
|
||||||
* @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 repos Tuple of the repository owner and the repository name
|
|
||||||
* @return list which contains issue count for each repository
|
|
||||||
*/
|
|
||||||
def countIssueGroupByRepository(
|
|
||||||
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
|
||||||
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
|
|
||||||
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
|
|
||||||
.groupBy { t =>
|
|
||||||
t.userName -> t.repositoryName
|
|
||||||
}
|
|
||||||
.map { case (repo, t) =>
|
|
||||||
(repo._1, repo._2, t.length)
|
|
||||||
}
|
|
||||||
.sortBy(_._3 desc)
|
|
||||||
.list
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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[IssueInfo] = {
|
||||||
(implicit s: Session): List[(Issue, List[Label], Int)] = {
|
|
||||||
|
|
||||||
// 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,21 +109,23 @@ 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) =>
|
||||||
c1._1.userName == c2._1.userName &&
|
c1._1.userName == c2._1.userName &&
|
||||||
c1._1.repositoryName == c2._1.repositoryName &&
|
c1._1.repositoryName == c2._1.repositoryName &&
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -158,21 +133,18 @@ 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, pullRequest: Boolean)(implicit s: Session) =
|
||||||
filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
|
|
||||||
Issues filter { t1 =>
|
Issues filter { t1 =>
|
||||||
condition.repo
|
repos
|
||||||
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
|
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
||||||
.getOrElse (repos)
|
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
||||||
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
|
||||||
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
|
||||||
(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) &&
|
// Label filter
|
||||||
(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
|
||||||
@@ -180,7 +152,19 @@ trait IssuesService {
|
|||||||
(t3.byRepository(t1.userName, t1.repositoryName)) &&
|
(t3.byRepository(t1.userName, t1.repositoryName)) &&
|
||||||
(t3.labelName inSetBind condition.labels)
|
(t3.labelName inSetBind condition.labels)
|
||||||
} map(_.labelId)))
|
} map(_.labelId)))
|
||||||
} exists, condition.labels.nonEmpty)
|
} exists, condition.labels.nonEmpty) &&
|
||||||
|
// Visibility filter
|
||||||
|
(Repositories filter { t2 =>
|
||||||
|
(t2.byRepository(t1.userName, t1.repositoryName)) &&
|
||||||
|
(t2.isPrivate === (condition.visibility == Some("private")).bind)
|
||||||
|
} exists, condition.visibility.nonEmpty) &&
|
||||||
|
// Organization (group) filter
|
||||||
|
(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
|
||||||
|
// Mentioned filter
|
||||||
|
((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
|
||||||
|
(IssueComments filter { t2 =>
|
||||||
|
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
|
||||||
|
} exists), condition.mentioned.isDefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
||||||
@@ -279,6 +263,7 @@ trait IssuesService {
|
|||||||
|
|
||||||
// Search Issue
|
// Search Issue
|
||||||
val issues = Issues
|
val issues = Issues
|
||||||
|
.filter(_.byRepository(owner, repository))
|
||||||
.innerJoin(IssueOutline).on { case (t1, t2) =>
|
.innerJoin(IssueOutline).on { case (t1, t2) =>
|
||||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||||
}
|
}
|
||||||
@@ -294,6 +279,7 @@ trait IssuesService {
|
|||||||
|
|
||||||
// Search IssueComment
|
// Search IssueComment
|
||||||
val comments = IssueComments
|
val comments = IssueComments
|
||||||
|
.filter(_.byRepository(owner, repository))
|
||||||
.innerJoin(Issues).on { case (t1, t2) =>
|
.innerJoin(Issues).on { case (t1, t2) =>
|
||||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||||
}
|
}
|
||||||
@@ -337,22 +323,64 @@ 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,
|
||||||
repo: Option[String] = None,
|
author: Option[String] = None,
|
||||||
|
assigned: Option[String] = None,
|
||||||
|
mentioned: Option[String] = None,
|
||||||
state: String = "open",
|
state: String = "open",
|
||||||
sort: String = "created",
|
sort: String = "created",
|
||||||
direction: String = "desc"){
|
direction: String = "desc",
|
||||||
|
visibility: Option[String] = None,
|
||||||
|
groups: Set[String] = Set.empty){
|
||||||
|
|
||||||
|
def isEmpty: Boolean = {
|
||||||
|
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
|
||||||
|
state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
def nonEmpty: Boolean = !isEmpty
|
||||||
|
|
||||||
|
def toFilterString: String = (
|
||||||
|
List(
|
||||||
|
Some(s"is:${state}"),
|
||||||
|
author.map(author => s"author:${author}"),
|
||||||
|
assigned.map(assignee => s"assignee:${assignee}"),
|
||||||
|
mentioned.map(mentioned => s"mentions:${mentioned}")
|
||||||
|
).flatten ++
|
||||||
|
labels.map(label => s"label:${label}") ++
|
||||||
|
List(
|
||||||
|
milestoneId.map { _ match {
|
||||||
|
case Some(x) => s"milestone:${milestoneId}"
|
||||||
|
case None => "no:milestone"
|
||||||
|
}},
|
||||||
|
(sort, direction) match {
|
||||||
|
case ("created" , "desc") => None
|
||||||
|
case ("created" , "asc" ) => Some("sort:created-asc")
|
||||||
|
case ("comments", "desc") => Some("sort:comments-desc")
|
||||||
|
case ("comments", "asc" ) => Some("sort:comments-asc")
|
||||||
|
case ("updated" , "desc") => Some("sort:updated-desc")
|
||||||
|
case ("updated" , "asc" ) => Some("sort:updated-asc")
|
||||||
|
},
|
||||||
|
visibility.map(visibility => s"visibility:${visibility}")
|
||||||
|
).flatten ++
|
||||||
|
groups.map(group => s"group:${group}")
|
||||||
|
).mkString(" ")
|
||||||
|
|
||||||
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(","))),
|
||||||
milestoneId.map { id => "milestone=" + (id match {
|
milestoneId.map { _ match {
|
||||||
case Some(x) => x.toString
|
case Some(x) => "milestone=" + x
|
||||||
case None => "none"
|
case None => "milestone=none"
|
||||||
})},
|
}},
|
||||||
repo.map("for=" + urlEncode(_)),
|
author .map(x => "author=" + urlEncode(x)),
|
||||||
|
assigned .map(x => "assigned=" + urlEncode(x)),
|
||||||
|
mentioned.map(x => "mentioned=" + urlEncode(x)),
|
||||||
Some("state=" + urlEncode(state)),
|
Some("state=" + urlEncode(state)),
|
||||||
Some("sort=" + urlEncode(sort)),
|
Some("sort=" + urlEncode(sort)),
|
||||||
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
|
Some("direction=" + urlEncode(direction)),
|
||||||
|
visibility.map(x => "visibility=" + urlEncode(x)),
|
||||||
|
if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(",")))
|
||||||
|
).flatten.mkString("&")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,17 +391,63 @@ object IssuesService {
|
|||||||
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores IssueSearchCondition instance from filter query.
|
||||||
|
*/
|
||||||
|
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
|
||||||
|
val conditions = filter.split("[ \t]+").map { x =>
|
||||||
|
val dim = x.split(":")
|
||||||
|
dim(0) -> dim(1)
|
||||||
|
}.groupBy(_._1).map { case (key, values) =>
|
||||||
|
key -> values.map(_._2).toSeq
|
||||||
|
}
|
||||||
|
|
||||||
|
val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match {
|
||||||
|
case "created-asc" => ("created" , "asc" )
|
||||||
|
case "comments-desc" => ("comments", "desc")
|
||||||
|
case "comments-asc" => ("comments", "asc" )
|
||||||
|
case "updated-desc" => ("comments", "desc")
|
||||||
|
case "updated-asc" => ("comments", "asc" )
|
||||||
|
case _ => ("created" , "desc")
|
||||||
|
}
|
||||||
|
|
||||||
|
IssueSearchCondition(
|
||||||
|
conditions.get("label").map(_.toSet).getOrElse(Set.empty),
|
||||||
|
conditions.get("milestone").flatMap(_.headOption) match {
|
||||||
|
case None => None
|
||||||
|
case Some("none") => Some(None)
|
||||||
|
case Some(x) => milestones.get(x).map(x => Some(x))
|
||||||
|
},
|
||||||
|
conditions.get("author").flatMap(_.headOption),
|
||||||
|
conditions.get("assignee").flatMap(_.headOption),
|
||||||
|
conditions.get("mentions").flatMap(_.headOption),
|
||||||
|
conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"),
|
||||||
|
sort,
|
||||||
|
direction,
|
||||||
|
conditions.get("visibility").flatMap(_.headOption),
|
||||||
|
conditions.get("group").map(_.toSet).getOrElse(Set.empty)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores IssueSearchCondition instance from request parameters.
|
||||||
|
*/
|
||||||
def apply(request: HttpServletRequest): IssueSearchCondition =
|
def apply(request: HttpServletRequest): IssueSearchCondition =
|
||||||
IssueSearchCondition(
|
IssueSearchCondition(
|
||||||
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
|
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
|
||||||
param(request, "milestone").map{
|
param(request, "milestone").map {
|
||||||
case "none" => None
|
case "none" => None
|
||||||
case x => x.toIntOpt
|
case x => x.toIntOpt
|
||||||
},
|
},
|
||||||
param(request, "for"),
|
param(request, "author"),
|
||||||
|
param(request, "assigned"),
|
||||||
|
param(request, "mentioned"),
|
||||||
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"),
|
||||||
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
|
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
|
||||||
|
param(request, "visibility"),
|
||||||
|
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
|
||||||
|
)
|
||||||
|
|
||||||
def page(request: HttpServletRequest) = try {
|
def page(request: HttpServletRequest) = try {
|
||||||
val i = param(request, "page").getOrElse("1").toInt
|
val i = param(request, "page").getOrElse("1").toInt
|
||||||
@@ -383,4 +457,6 @@ object IssuesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
24
src/main/scala/service/PluginService.scala
Normal file
24
src/main/scala/service/PluginService.scala
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 =
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ trait RepositorySearchService { self: IssuesService =>
|
|||||||
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
|
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
|
||||||
IssueSearchResult(
|
IssueSearchResult(
|
||||||
issue.issueId,
|
issue.issueId,
|
||||||
|
issue.isPullRequest,
|
||||||
issue.title,
|
issue.title,
|
||||||
issue.openedUserName,
|
issue.openedUserName,
|
||||||
issue.registeredDate,
|
issue.registeredDate,
|
||||||
@@ -111,6 +112,7 @@ object RepositorySearchService {
|
|||||||
|
|
||||||
case class IssueSearchResult(
|
case class IssueSearchResult(
|
||||||
issueId: Int,
|
issueId: Int,
|
||||||
|
isPullRequest: Boolean,
|
||||||
title: String,
|
title: String,
|
||||||
openedUserName: String,
|
openedUserName: String,
|
||||||
registeredDate: java.util.Date,
|
registeredDate: java.util.Date,
|
||||||
|
|||||||
@@ -46,16 +46,16 @@ trait RepositoryService { self: AccountService =>
|
|||||||
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
|
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
|
||||||
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
|
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
|
||||||
|
|
||||||
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
|
||||||
Repositories.filter { t =>
|
Repositories.filter { t =>
|
||||||
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
|
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
|
||||||
@@ -69,11 +69,18 @@ trait RepositoryService { self: AccountService =>
|
|||||||
t.requestRepositoryName === oldRepositoryName.bind
|
t.requestRepositoryName === oldRepositoryName.bind
|
||||||
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
||||||
|
|
||||||
|
// Updates activity fk before deleting repository because activity is sorted by activityId
|
||||||
|
// and it can't be changed by deleting-and-inserting record.
|
||||||
|
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
|
||||||
|
Activities.filter(_.activityId === activity.activityId.bind)
|
||||||
|
.map(x => (x.userName, x.repositoryName)).update(newUserName, newRepositoryName)
|
||||||
|
}
|
||||||
|
|
||||||
deleteRepository(oldUserName, oldRepositoryName)
|
deleteRepository(oldUserName, oldRepositoryName)
|
||||||
|
|
||||||
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
||||||
|
|
||||||
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
|
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
|
||||||
Issues.insertAll(issues.map { x => x.copy(
|
Issues.insertAll(issues.map { x => x.copy(
|
||||||
@@ -88,7 +95,8 @@ trait RepositoryService { self: AccountService =>
|
|||||||
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
|
||||||
if(account.isGroupAccount){
|
if(account.isGroupAccount){
|
||||||
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
|
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
|
||||||
} else {
|
} else {
|
||||||
@@ -96,12 +104,11 @@ trait RepositoryService { self: AccountService =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update activity messages
|
// Update activity messages
|
||||||
val updateActivities = Activities.filter { t =>
|
Activities.filter { t =>
|
||||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
|
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
|
||||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
|
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") ||
|
||||||
}.map { t => t.activityId -> t.message }.list
|
(t.message like s"%:${oldUserName}/${oldRepositoryName}@%")
|
||||||
|
}.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) =>
|
||||||
updateActivities.foreach { case (activityId, message) =>
|
|
||||||
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
|
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
|
||||||
message
|
message
|
||||||
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
|
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
|
||||||
@@ -109,6 +116,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
|
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
|
||||||
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
|
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
|
||||||
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
|
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
|
||||||
|
.replace(s"[commit:${oldUserName}/${oldRepositoryName}@" ,s"[commit:${newUserName}/${newRepositoryName}@")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,6 +126,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
||||||
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
CommitComments.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
@@ -127,6 +136,30 @@ trait RepositoryService { self: AccountService =>
|
|||||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
|
||||||
|
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
|
||||||
|
Repositories
|
||||||
|
.filter { x => (x.originUserName === userName.bind) && (x.originRepositoryName === repositoryName.bind) }
|
||||||
|
.map { x => (x.userName, x.repositoryName) }
|
||||||
|
.list
|
||||||
|
.foreach { case (userName, repositoryName) =>
|
||||||
|
Repositories
|
||||||
|
.filter(_.byRepository(userName, repositoryName))
|
||||||
|
.map(x => (x.originUserName?, x.originRepositoryName?))
|
||||||
|
.update(None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME
|
||||||
|
Repositories
|
||||||
|
.filter { x => (x.parentUserName === userName.bind) && (x.parentRepositoryName === repositoryName.bind) }
|
||||||
|
.map { x => (x.userName, x.repositoryName) }
|
||||||
|
.list
|
||||||
|
.foreach { case (userName, repositoryName) =>
|
||||||
|
Repositories
|
||||||
|
.filter(_.byRepository(userName, repositoryName))
|
||||||
|
.map(x => (x.parentUserName?, x.parentRepositoryName?))
|
||||||
|
.update(None, None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,7 +190,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
||||||
repository,
|
repository,
|
||||||
issues.size,
|
issues.size,
|
||||||
issues.filter(_ == true).size,
|
issues.count(_ == true),
|
||||||
getForkedCount(
|
getForkedCount(
|
||||||
repository.originUserName.getOrElse(repository.userName),
|
repository.originUserName.getOrElse(repository.userName),
|
||||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||||
@@ -166,8 +199,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
|
||||||
}
|
}
|
||||||
@@ -344,4 +388,4 @@ object RepositoryService {
|
|||||||
|
|
||||||
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
|
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ trait SystemSettingsService {
|
|||||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||||
defining(new java.util.Properties()){ props =>
|
defining(new java.util.Properties()){ props =>
|
||||||
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||||
|
settings.information.foreach(x => props.setProperty(Information, x))
|
||||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||||
props.setProperty(Notification, settings.notification.toString)
|
props.setProperty(Notification, settings.notification.toString)
|
||||||
@@ -39,7 +40,7 @@ trait SystemSettingsService {
|
|||||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||||
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
|
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
|
||||||
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
||||||
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x.toString))
|
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x))
|
||||||
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
||||||
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
||||||
}
|
}
|
||||||
@@ -60,6 +61,7 @@ trait SystemSettingsService {
|
|||||||
}
|
}
|
||||||
SystemSettings(
|
SystemSettings(
|
||||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||||
|
getOptionValue[String](props, Information, None),
|
||||||
getValue(props, AllowAccountRegistration, false),
|
getValue(props, AllowAccountRegistration, false),
|
||||||
getValue(props, Gravatar, true),
|
getValue(props, Gravatar, true),
|
||||||
getValue(props, Notification, false),
|
getValue(props, Notification, false),
|
||||||
@@ -105,6 +107,7 @@ object SystemSettingsService {
|
|||||||
|
|
||||||
case class SystemSettings(
|
case class SystemSettings(
|
||||||
baseUrl: Option[String],
|
baseUrl: Option[String],
|
||||||
|
information: Option[String],
|
||||||
allowAccountRegistration: Boolean,
|
allowAccountRegistration: Boolean,
|
||||||
gravatar: Boolean,
|
gravatar: Boolean,
|
||||||
notification: Boolean,
|
notification: Boolean,
|
||||||
@@ -147,6 +150,7 @@ object SystemSettingsService {
|
|||||||
val DefaultLdapPort = 389
|
val DefaultLdapPort = 389
|
||||||
|
|
||||||
private val BaseURL = "base_url"
|
private val BaseURL = "base_url"
|
||||||
|
private val Information = "information"
|
||||||
private val AllowAccountRegistration = "allow_account_registration"
|
private val AllowAccountRegistration = "allow_account_registration"
|
||||||
private val Gravatar = "gravatar"
|
private val Gravatar = "gravatar"
|
||||||
private val Notification = "notification"
|
private val Notification = "notification"
|
||||||
@@ -191,4 +195,7 @@ object SystemSettingsService {
|
|||||||
else value
|
else value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO temporary flag
|
||||||
|
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import org.apache.commons.io.IOUtils
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import util.ControlUtil._
|
import util.ControlUtil._
|
||||||
|
import util.JDBCUtil._
|
||||||
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 +54,59 @@ 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, 7) {
|
||||||
|
override def update(conn: Connection): Unit = {
|
||||||
|
super.update(conn)
|
||||||
|
conn.select("SELECT * FROM REPOSITORY"){ rs =>
|
||||||
|
// Rename attached files directory from /issues to /comments
|
||||||
|
val userName = rs.getString("USER_NAME")
|
||||||
|
val repoName = rs.getString("REPOSITORY_NAME")
|
||||||
|
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
|
||||||
|
val oldDir = new File(newDir.getParentFile, "issues")
|
||||||
|
if(oldDir.exists && oldDir.isDirectory){
|
||||||
|
oldDir.renameTo(newDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
|
||||||
|
val originalUserName = rs.getString("ORIGIN_USER_NAME")
|
||||||
|
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
|
||||||
|
if(originalUserName != null && originalRepoName != null){
|
||||||
|
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
||||||
|
originalUserName, originalRepoName) == 0){
|
||||||
|
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
|
||||||
|
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
|
||||||
|
val parentUserName = rs.getString("PARENT_USER_NAME")
|
||||||
|
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
|
||||||
|
if(parentUserName != null && parentRepoName != null){
|
||||||
|
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
||||||
|
parentUserName, parentRepoName) == 0){
|
||||||
|
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
|
||||||
|
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Version(2, 6),
|
||||||
|
new Version(2, 5),
|
||||||
|
new Version(2, 4),
|
||||||
|
new Version(2, 3) {
|
||||||
|
override def update(conn: Connection): Unit = {
|
||||||
|
super.update(conn)
|
||||||
|
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
|
||||||
|
val curInfo = rs.getString("ADDITIONAL_INFO")
|
||||||
|
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
|
||||||
|
if (curInfo != newInfo) {
|
||||||
|
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
|
||||||
|
FileUtils.deleteDirectory(new File(Directory.PluginHome))
|
||||||
|
}
|
||||||
|
},
|
||||||
new Version(2, 2),
|
new Version(2, 2),
|
||||||
new Version(2, 1),
|
new Version(2, 1),
|
||||||
new Version(2, 0){
|
new Version(2, 0){
|
||||||
@@ -62,16 +117,14 @@ object AutoUpdate {
|
|||||||
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
||||||
|
|
||||||
super.update(conn)
|
super.update(conn)
|
||||||
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
|
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
||||||
while(rs.next){
|
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
||||||
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
if(dir.exists && dir.isDirectory){
|
||||||
if(dir.exists && dir.isDirectory){
|
dir.listFiles.foreach { file =>
|
||||||
dir.listFiles.foreach { file =>
|
if(file.getName.indexOf('.') < 0){
|
||||||
if(file.getName.indexOf('.') < 0){
|
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
|
||||||
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
|
if(mimeType.startsWith("image/")){
|
||||||
if(mimeType.startsWith("image/")){
|
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
|
||||||
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,14 +147,12 @@ object AutoUpdate {
|
|||||||
override def update(conn: Connection): Unit = {
|
override def update(conn: Connection): Unit = {
|
||||||
super.update(conn)
|
super.update(conn)
|
||||||
// Fix wiki repository configuration
|
// Fix wiki repository configuration
|
||||||
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
|
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
||||||
while(rs.next){
|
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
||||||
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
defining(git.getRepository.getConfig){ config =>
|
||||||
defining(git.getRepository.getConfig){ config =>
|
if(!config.getBoolean("http", "receivepack", false)){
|
||||||
if(!config.getBoolean("http", "receivepack", false)){
|
config.setBoolean("http", null, "receivepack", true)
|
||||||
config.setBoolean("http", null, "receivepack", true)
|
config.save
|
||||||
config.save
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,24 +198,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 +224,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,17 +234,29 @@ class AutoUpdateListener extends ServletContextListener {
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.debug("End schema update")
|
||||||
}
|
}
|
||||||
logger.debug("End schema update")
|
|
||||||
|
|
||||||
logger.debug("Starting plugin system...")
|
if(SystemSettingsService.enablePluginSystem){
|
||||||
plugin.PluginSystem.init()
|
getDatabase(context).withSession { implicit session =>
|
||||||
|
logger.debug("Starting plugin system...")
|
||||||
|
try {
|
||||||
|
plugin.PluginSystem.init()
|
||||||
|
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
PluginUpdateJob.schedule(scheduler)
|
PluginUpdateJob.schedule(scheduler)
|
||||||
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 +269,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"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 =>
|
||||||
val result = try {
|
if(authenticate(action.security, context, repository)){
|
||||||
PluginConnectionHolder.threadLocal.set(session.conn)
|
val result = try {
|
||||||
action.function(request, response, repository)
|
PluginConnectionHolder.threadLocal.set(session.conn)
|
||||||
} finally {
|
action.function(request, response, context, repository)
|
||||||
PluginConnectionHolder.threadLocal.remove()
|
} finally {
|
||||||
}
|
PluginConnectionHolder.threadLocal.remove()
|
||||||
result match {
|
|
||||||
case x: String => renderRepositoryHtml(request, response, systemSettings, repository, x)
|
|
||||||
case x: org.mozilla.javascript.NativeObject => {
|
|
||||||
x.get("format") match {
|
|
||||||
case "html" => renderRepositoryHtml(request, response, systemSettings, repository, x.get("body").toString)
|
|
||||||
case "json" => renderJson(request, response, x.get("body").toString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
processActionResult(result, request, response, context)
|
||||||
|
} else {
|
||||||
|
// TODO NotFound or Error?
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ object Directory {
|
|||||||
* Directory for files which are attached to issue.
|
* Directory for files which are attached to issue.
|
||||||
*/
|
*/
|
||||||
def getAttachedDir(owner: String, repository: String): File =
|
def getAttachedDir(owner: String, repository: String): File =
|
||||||
new File(s"${RepositoryHome}/${owner}/${repository}/issues")
|
new File(s"${RepositoryHome}/${owner}/${repository}/comments")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directory for uploaded files by the specified user.
|
* Directory for uploaded files by the specified user.
|
||||||
|
|||||||
55
src/main/scala/util/JDBCUtil.scala
Normal file
55
src/main/scala/util/JDBCUtil.scala
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import java.sql._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides implicit class which extends java.sql.Connection.
|
||||||
|
* This is used in automatic migration in [[servlet.AutoUpdateListener]].
|
||||||
|
*/
|
||||||
|
object JDBCUtil {
|
||||||
|
|
||||||
|
implicit class RichConnection(conn: Connection){
|
||||||
|
|
||||||
|
def update(sql: String, params: Any*): Int = {
|
||||||
|
execute(sql, params: _*){ stmt =>
|
||||||
|
stmt.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = {
|
||||||
|
execute(sql, params: _*){ stmt =>
|
||||||
|
using(stmt.executeQuery()){ rs =>
|
||||||
|
val list = new ListBuffer[T]
|
||||||
|
while(rs.next){
|
||||||
|
list += f(rs)
|
||||||
|
}
|
||||||
|
list.toSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def selectInt(sql: String, params: Any*): Int = {
|
||||||
|
execute(sql, params: _*){ stmt =>
|
||||||
|
using(stmt.executeQuery()){ rs =>
|
||||||
|
if(rs.next) rs.getInt(1) else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def execute[T](sql: String, params: Any*)(f: (PreparedStatement) => T): T = {
|
||||||
|
using(conn.prepareStatement(sql)){ stmt =>
|
||||||
|
params.zipWithIndex.foreach { case (p, i) =>
|
||||||
|
p match {
|
||||||
|
case x: Int => stmt.setInt(i + 1, x)
|
||||||
|
case x: String => stmt.setString(i + 1, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f(stmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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._
|
||||||
@@ -13,7 +14,7 @@ import org.eclipse.jgit.treewalk.filter._
|
|||||||
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||||
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
|
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import org.eclipse.jgit.api.errors.NoHeadException
|
import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
|
||||||
import service.RepositoryService
|
import service.RepositoryService
|
||||||
import org.eclipse.jgit.dircache.DirCacheEntry
|
import org.eclipse.jgit.dircache.DirCacheEntry
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -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)
|
} else {
|
||||||
treeWalk.setFilter(new TreeFilter(){
|
val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
|
||||||
|
treeWalk.enterSubtree()
|
||||||
|
treeWalk
|
||||||
|
}
|
||||||
|
|
||||||
var stopRecursive = false
|
using(treeWalk) { treeWalk =>
|
||||||
|
|
||||||
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 {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def shouldBeRecursive(): Boolean = !stopRecursive
|
|
||||||
|
|
||||||
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.transform(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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +507,17 @@ object JGitUtil {
|
|||||||
}.find(_._1 != null)
|
}.find(_._1 != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def createBranch(git: Git, fromBranch: String, newBranch: String) = {
|
||||||
|
try {
|
||||||
|
git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call()
|
||||||
|
Right("Branch created.")
|
||||||
|
} catch {
|
||||||
|
case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.")
|
||||||
|
// JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists.
|
||||||
|
case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
|
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
|
||||||
val entry = new DirCacheEntry(path)
|
val entry = new DirCacheEntry(path)
|
||||||
entry.setFileMode(mode)
|
entry.setFileMode(mode)
|
||||||
@@ -504,7 +526,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 +540,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 +674,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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ trait LinkConverter { self: RequestCache =>
|
|||||||
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
|
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
|
||||||
value
|
value
|
||||||
// escape HTML tags
|
// escape HTML tags
|
||||||
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
|
.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
||||||
// convert issue id to link
|
// convert issue id to link
|
||||||
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
||||||
getIssue(repository.owner, repository.name, m.group(2)) match {
|
getIssue(repository.owner, repository.name, m.group(2)) match {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.pegdown.ast._
|
|||||||
import org.pegdown.LinkRenderer.Rendering
|
import org.pegdown.LinkRenderer.Rendering
|
||||||
import java.text.Normalizer
|
import java.text.Normalizer
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.regex.Pattern
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
import service.{RequestCache, WikiService}
|
import service.{RequestCache, WikiService}
|
||||||
|
|
||||||
@@ -18,17 +19,23 @@ object Markdown {
|
|||||||
* Converts Markdown of Wiki pages to HTML.
|
* Converts Markdown of Wiki pages to HTML.
|
||||||
*/
|
*/
|
||||||
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
|
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
|
||||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
|
enableWikiLink: Boolean, enableRefsLink: Boolean,
|
||||||
|
enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = {
|
||||||
// escape issue id
|
// escape issue id
|
||||||
val source = if(enableRefsLink){
|
val s = if(enableRefsLink){
|
||||||
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
||||||
} else markdown
|
} else markdown
|
||||||
|
|
||||||
|
// escape task list
|
||||||
|
val source = if(enableTaskList){
|
||||||
|
GitBucketHtmlSerializer.escapeTaskList(s)
|
||||||
|
} else s
|
||||||
|
|
||||||
val rootNode = new PegDownProcessor(
|
val rootNode = new PegDownProcessor(
|
||||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
|
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML
|
||||||
).parseMarkdown(source.toCharArray)
|
).parseMarkdown(source.toCharArray)
|
||||||
|
|
||||||
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
|
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,15 +89,18 @@ class GitBucketHtmlSerializer(
|
|||||||
markdown: String,
|
markdown: String,
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
enableWikiLink: Boolean,
|
enableWikiLink: Boolean,
|
||||||
enableRefsLink: Boolean
|
enableRefsLink: Boolean,
|
||||||
|
enableTaskList: Boolean,
|
||||||
|
hasWritePermission: Boolean
|
||||||
)(implicit val context: app.Context) extends ToHtmlSerializer(
|
)(implicit val context: app.Context) extends ToHtmlSerializer(
|
||||||
new GitBucketLinkRender(context, repository, enableWikiLink),
|
new GitBucketLinkRender(context, repository, enableWikiLink),
|
||||||
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 +111,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
|
||||||
}
|
}
|
||||||
@@ -130,7 +152,10 @@ class GitBucketHtmlSerializer(
|
|||||||
|
|
||||||
override def visit(node: TextNode): Unit = {
|
override def visit(node: TextNode): Unit = {
|
||||||
// convert commit id and username to link.
|
// convert commit id and username to link.
|
||||||
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
|
val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
|
||||||
|
|
||||||
|
// convert task list to checkbox.
|
||||||
|
val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t
|
||||||
|
|
||||||
if (abbreviations.isEmpty) {
|
if (abbreviations.isEmpty) {
|
||||||
printer.print(text)
|
printer.print(text)
|
||||||
@@ -138,6 +163,28 @@ class GitBucketHtmlSerializer(
|
|||||||
printWithAbbreviations(text)
|
printWithAbbreviations(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def visit(node: BulletListNode): Unit = {
|
||||||
|
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
|
||||||
|
printer.println().print("""<ul class="task-list">""").indent(+2)
|
||||||
|
visitChildren(node)
|
||||||
|
printer.indent(-2).println().print("</ul>")
|
||||||
|
} else {
|
||||||
|
printIndentedTag(node, "ul")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def visit(node: ListItemNode): Unit = {
|
||||||
|
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
|
||||||
|
printer.println()
|
||||||
|
printer.print("""<li class="task-list-item">""")
|
||||||
|
visitChildren(node)
|
||||||
|
printer.print("</li>")
|
||||||
|
} else {
|
||||||
|
printer.println()
|
||||||
|
printTag(node, "li")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object GitBucketHtmlSerializer {
|
object GitBucketHtmlSerializer {
|
||||||
@@ -150,4 +197,14 @@ object GitBucketHtmlSerializer {
|
|||||||
val noSpecialChars = StringUtil.urlEncode(normalized)
|
val noSpecialChars = StringUtil.urlEncode(normalized)
|
||||||
noSpecialChars.toLowerCase(Locale.ENGLISH)
|
noSpecialChars.toLowerCase(Locale.ENGLISH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def escapeTaskList(text: String): String = {
|
||||||
|
Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
def convertCheckBox(text: String, hasWritePermission: Boolean): String = {
|
||||||
|
val disabled = if (hasWritePermission) "" else "disabled"
|
||||||
|
text.replaceAll("task:x:", """<input type="checkbox" class="task-list-item-checkbox" checked="checked" """ + disabled + "/>")
|
||||||
|
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package view
|
package view
|
||||||
import java.util.Date
|
import java.util.{Locale, 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
|
||||||
|
|
||||||
@@ -15,10 +15,55 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
*/
|
*/
|
||||||
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
|
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
|
||||||
|
|
||||||
|
val timeUnits = List(
|
||||||
|
(1000L, "second"),
|
||||||
|
(1000L * 60, "minute"),
|
||||||
|
(1000L * 60 * 60, "hour"),
|
||||||
|
(1000L * 60 * 60 * 24, "day"),
|
||||||
|
(1000L * 60 * 60 * 24 * 30, "month"),
|
||||||
|
(1000L * 60 * 60 * 24 * 365, "year")
|
||||||
|
).reverse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago"
|
||||||
|
*/
|
||||||
|
def datetimeAgo(date: Date): String = {
|
||||||
|
val duration = new Date().getTime - date.getTime
|
||||||
|
timeUnits.find(tuple => duration / tuple._1 > 0) match {
|
||||||
|
case Some((unitValue, unitString)) =>
|
||||||
|
val value = duration / unitValue
|
||||||
|
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
|
||||||
|
case None => "just now"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Format java.util.Date to "x {seconds/minutes/hours/days} ago"
|
||||||
|
* If duration over 1 month, format to "d MMM (yyyy)"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
def datetimeAgoRecentOnly(date: Date): String = {
|
||||||
|
val duration = new Date().getTime - date.getTime
|
||||||
|
timeUnits.find(tuple => duration / tuple._1 > 0) match {
|
||||||
|
case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}"
|
||||||
|
case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}"
|
||||||
|
case Some((unitValue, unitString)) =>
|
||||||
|
val value = duration / unitValue
|
||||||
|
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
|
||||||
|
case None => "just now"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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".
|
||||||
@@ -44,8 +89,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
* Converts Markdown of Wiki pages to HTML.
|
* Converts Markdown of Wiki pages to HTML.
|
||||||
*/
|
*/
|
||||||
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
|
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
|
||||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
|
enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
|
||||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
|
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
|
||||||
|
|
||||||
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
|
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
@@ -107,6 +152,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||||
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||||
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
|
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
|
||||||
|
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
@if(repository.repository.description.isDefined){
|
@if(repository.repository.description.isDefined){
|
||||||
<div>@repository.repository.description</div>
|
<div>@repository.repository.description</div>
|
||||||
}
|
}
|
||||||
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
|
<div><span class="muted small">Updated @helper.html.datetimeago(repository.repository.lastActivityDate)</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
<li@if(active=="plugins"){ class="active"}>
|
@if(service.SystemSettingsService.enablePluginSystem){
|
||||||
<a href="@path/admin/plugins">Plugins</a>
|
<li@if(active=="plugins"){ class="active"}>
|
||||||
</li>
|
<a href="@path/admin/plugins">Plugins</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
<li>
|
<li>
|
||||||
<a href="@path/console/login.jsp">H2 Console</a>
|
<a href="@path/console/login.jsp">H2 Console</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -31,6 +31,14 @@
|
|||||||
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
|
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
|
||||||
</p>
|
</p>
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
|
<!-- Information -->
|
||||||
|
<!--====================================================================-->
|
||||||
|
<hr>
|
||||||
|
<label><span class="strong">Information</span> (HTML is available)</label>
|
||||||
|
<fieldset>
|
||||||
|
<textarea name="information" style="width: 600px; height: 100px;">@settings.information</textarea>
|
||||||
|
</fieldset>
|
||||||
|
<!--====================================================================-->
|
||||||
<!-- Account registration -->
|
<!-- Account registration -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
|
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
|
||||||
Disable
|
Disable
|
||||||
</label>
|
</label>
|
||||||
|
<div>
|
||||||
|
<span id="error-removed" class="error"></span>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||||
|
|||||||
74
src/main/twirl/dashboard/header.scala.html
Normal file
74
src/main/twirl/dashboard/header.scala.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
@(openCount: Int,
|
||||||
|
closedCount: Int,
|
||||||
|
condition: service.IssuesService.IssueSearchCondition,
|
||||||
|
groups: List[String])(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
<div class="pull-right" id="table-issues-control">
|
||||||
|
@helper.html.dropdown("Visibility", flat = true){
|
||||||
|
<li>
|
||||||
|
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
|
||||||
|
@helper.html.checkicon(condition.visibility == Some("private"))
|
||||||
|
Private repository only
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("public")) None else Some("public"))).toURL)">
|
||||||
|
@helper.html.checkicon(condition.visibility == Some("public"))
|
||||||
|
Public repository only
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@helper.html.dropdown("Organization", flat = true){
|
||||||
|
@groups.map { group =>
|
||||||
|
<li>
|
||||||
|
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
|
||||||
|
@helper.html.checkicon(condition.groups.contains(group))
|
||||||
|
@avatar(group, 20) @group
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@helper.html.dropdown("Sort", flat = true){
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -1,50 +1,16 @@
|
|||||||
@(listparts: twirl.api.Html,
|
@(issues: List[service.IssuesService.IssueInfo],
|
||||||
allCount: Int,
|
page: Int,
|
||||||
assignedCount: Int,
|
openCount: Int,
|
||||||
createdByCount: Int,
|
closedCount: Int,
|
||||||
repositories: List[(String, String, Int)],
|
|
||||||
condition: service.IssuesService.IssueSearchCondition,
|
condition: service.IssuesService.IssueSearchCondition,
|
||||||
filter: String)(implicit context: app.Context)
|
filter: String,
|
||||||
|
groups: List[String])(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Your Issues"){
|
@html.main("Issues"){
|
||||||
<div class="container">
|
|
||||||
@dashboard.html.tab("issues")
|
@dashboard.html.tab("issues")
|
||||||
<div class="row-fluid">
|
<div class="container">
|
||||||
<div class="span3">
|
@issuesnavi(filter, "issues", condition)
|
||||||
<ul class="nav nav-pills nav-stacked">
|
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
<li@if(filter == "all"){ class="active"}>
|
|
||||||
<a href="@path/dashboard/issues/repos@condition.toURL">
|
|
||||||
<span class="count-right">@allCount</span>
|
|
||||||
In your repositories
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li@if(filter == "assigned"){ class="active"}>
|
|
||||||
<a href="@path/dashboard/issues/assigned@condition.toURL">
|
|
||||||
<span class="count-right">@assignedCount</span>
|
|
||||||
Assigned to you
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li@if(filter == "created_by"){ class="active"}>
|
|
||||||
<a href="@path/dashboard/issues/created_by@condition.toURL">
|
|
||||||
<span class="count-right">@createdByCount</span>
|
|
||||||
Created by you
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<hr/>
|
|
||||||
<ul class="nav nav-pills nav-stacked small">
|
|
||||||
@repositories.map { case (owner, name, count) =>
|
|
||||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
|
||||||
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
|
|
||||||
<span class="count-right">@count</span>
|
|
||||||
@owner/@name
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@listparts
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/main/twirl/dashboard/issueslist.scala.html
Normal file
67
src/main/twirl/dashboard/issueslist.scala.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@(issues: List[service.IssuesService.IssueInfo],
|
||||||
|
page: Int,
|
||||||
|
openCount: Int,
|
||||||
|
closedCount: Int,
|
||||||
|
condition: service.IssuesService.IssueSearchCondition,
|
||||||
|
filter: String,
|
||||||
|
groups: List[String])(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
@import service.IssuesService.IssueInfo
|
||||||
|
@*
|
||||||
|
<ul class="nav nav-pills-group pull-left fill-width">
|
||||||
|
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li>
|
||||||
|
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li>
|
||||||
|
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li>
|
||||||
|
</ul>
|
||||||
|
*@
|
||||||
|
<table class="table table-bordered table-hover table-issues">
|
||||||
|
<tr>
|
||||||
|
<th style="background-color: #eee;">
|
||||||
|
@dashboard.html.header(openCount, closedCount, condition, groups)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
|
||||||
|
<tr>
|
||||||
|
<td style="padding-top: 15px; padding-bottom: 15px;">
|
||||||
|
@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"/>
|
||||||
|
}
|
||||||
|
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
@if(commentCount > 0){
|
||||||
|
<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: 20px; 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>
|
||||||
|
</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>
|
||||||
22
src/main/twirl/dashboard/issuesnavi.scala.html
Normal file
22
src/main/twirl/dashboard/issuesnavi.scala.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@(filter: String,
|
||||||
|
active: String,
|
||||||
|
condition: service.IssuesService.IssueSearchCondition)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
<ul class="nav nav-pills-group pull-left fill-width">
|
||||||
|
<li class="@if(filter == "created_by"){active} first">
|
||||||
|
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
|
||||||
|
</li>
|
||||||
|
<li class="@if(filter == "assigned"){active}">
|
||||||
|
<a href="@path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
|
||||||
|
</li>
|
||||||
|
<li class="@if(filter == "mentioned"){active} last">
|
||||||
|
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
|
||||||
|
</li>
|
||||||
|
<li class="pull-right">
|
||||||
|
<form method="GET" id="search-filter-form" action="@path/dashboard/@active" style="margin-bottom: 0px;">
|
||||||
|
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px; width: 400px;"
|
||||||
|
value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
@@ -1,42 +1,16 @@
|
|||||||
@(listparts: twirl.api.Html,
|
@(issues: List[service.IssuesService.IssueInfo],
|
||||||
counts: List[service.PullRequestService.PullRequestCount],
|
page: Int,
|
||||||
repositories: List[(String, String, Int)],
|
openCount: Int,
|
||||||
|
closedCount: Int,
|
||||||
condition: service.IssuesService.IssueSearchCondition,
|
condition: service.IssuesService.IssueSearchCondition,
|
||||||
filter: String)(implicit context: app.Context)
|
filter: String,
|
||||||
|
groups: List[String])(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Your Issues"){
|
@html.main("Pull Requests"){
|
||||||
<div class="container">
|
|
||||||
@dashboard.html.tab("pulls")
|
@dashboard.html.tab("pulls")
|
||||||
<div class="row-fluid">
|
<div class="container">
|
||||||
<div class="span3">
|
@issuesnavi(filter, "pulls", condition)
|
||||||
<ul class="nav nav-pills nav-stacked">
|
@pullslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
<li@if(filter == "created_by"){ class="active"}>
|
|
||||||
<a href="@path/dashboard/pulls/owned@condition.toURL">
|
|
||||||
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
|
|
||||||
Yours
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li@if(filter == "not_created_by"){ class="active"}>
|
|
||||||
<a href="@path/dashboard/pulls/public@condition.toURL">
|
|
||||||
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
|
|
||||||
Public
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<hr/>
|
|
||||||
<ul class="nav nav-pills nav-stacked small">
|
|
||||||
@repositories.map { case (owner, name, count) =>
|
|
||||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
|
||||||
<a href="@path/dashboard/pulls/for/@owner/@name">
|
|
||||||
<span class="count-right">@count</span>
|
|
||||||
@owner/@name
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@listparts
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/main/twirl/dashboard/pullslist.scala.html
Normal file
67
src/main/twirl/dashboard/pullslist.scala.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@(issues: List[service.IssuesService.IssueInfo],
|
||||||
|
page: Int,
|
||||||
|
openCount: Int,
|
||||||
|
closedCount: Int,
|
||||||
|
condition: service.IssuesService.IssueSearchCondition,
|
||||||
|
filter: String,
|
||||||
|
groups: List[String])(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
@import service.IssuesService.IssueInfo
|
||||||
|
@*
|
||||||
|
<ul class="nav nav-pills-group pull-left fill-width">
|
||||||
|
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/pulls/created_by@condition.toURL">Created</a></li>
|
||||||
|
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/pulls/assigned@condition.toURL">Assigned</a></li>
|
||||||
|
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/pulls/mentioned@condition.toURL">Mentioned</a></li>
|
||||||
|
<li class="pull-right">
|
||||||
|
<div class="input-prepend" style="margin-bottom: 0px;">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
|
||||||
|
Filter
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="?q=is:open">Open issues and pull requests</a></li>
|
||||||
|
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
|
||||||
|
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
|
||||||
|
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
|
||||||
|
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
*@
|
||||||
|
<table class="table table-bordered table-hover table-issues">
|
||||||
|
<tr>
|
||||||
|
<th style="background-color: #eee;">
|
||||||
|
@dashboard.html.header(openCount, closedCount, condition, groups)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
|
||||||
|
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||||
|
<span class="pull-right muted">#@issue.issueId</span>
|
||||||
|
<div style="margin-left: 20px;">
|
||||||
|
@issue.content.map { content =>
|
||||||
|
@cut(content, 90)
|
||||||
|
}.getOrElse {
|
||||||
|
<span class="muted">No description available</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="small muted" style="margin-left: 20px;">
|
||||||
|
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||||
|
@if(commentCount > 0){
|
||||||
|
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
<div class="pull-right">
|
||||||
|
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
|
||||||
|
</div>
|
||||||
@@ -1,13 +1,47 @@
|
|||||||
@(active: String = "")(implicit context: app.Context)
|
@(active: String = "")(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
<ul class="nav nav-tabs">
|
<div class="dashboard-nav">
|
||||||
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
|
<div class="container">
|
||||||
@if(loginAccount.isDefined){
|
<a href="@path/" @if(active == ""){ class="active"}>
|
||||||
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
|
<img src="@assets/common/images/menu-feed.png">
|
||||||
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
|
News Feed
|
||||||
}
|
</a>
|
||||||
@if(active == ""){
|
@if(loginAccount.isDefined){
|
||||||
<li class="pull-right"><a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a></li>
|
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
|
||||||
}
|
<img src="@assets/common/images/menu-pulls.png">
|
||||||
</ul>
|
Pull Requests
|
||||||
|
</a>
|
||||||
|
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
|
||||||
|
<img src="@assets/common/images/menu-issues.png">
|
||||||
|
Issues
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style type="text/css">
|
||||||
|
div.dashboard-nav {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
text-align: right;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dashboard-nav a {
|
||||||
|
line-height: 10px;
|
||||||
|
margin-left: 20px;
|
||||||
|
padding-bottom: 13px;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dashboard-nav a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dashboard-nav a.active {
|
||||||
|
border-bottom: 2px solid #bb4444;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
@(activity.activityType match {
|
@(activity.activityType match {
|
||||||
case "open_issue" => detailActivity(activity, "activity-issue.png")
|
case "open_issue" => detailActivity(activity, "activity-issue.png")
|
||||||
case "comment_issue" => detailActivity(activity, "activity-comment.png")
|
case "comment_issue" => detailActivity(activity, "activity-comment.png")
|
||||||
|
case "comment_commit" => detailActivity(activity, "activity-comment.png")
|
||||||
case "close_issue" => detailActivity(activity, "activity-issue-close.png")
|
case "close_issue" => detailActivity(activity, "activity-issue-close.png")
|
||||||
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
|
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
|
||||||
case "open_pullreq" => detailActivity(activity, "activity-merge.png")
|
case "open_pullreq" => detailActivity(activity, "activity-merge.png")
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
@detailActivity(activity: model.Activity, image: String) = {
|
@detailActivity(activity: model.Activity, image: String) = {
|
||||||
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<div class="muted small">@datetime(activity.activityDate)</div>
|
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||||
<div class="strong">
|
<div class="strong">
|
||||||
@avatar(activity.activityUserName, 16)
|
@avatar(activity.activityUserName, 16)
|
||||||
@activityMessage(activity.message)
|
@activityMessage(activity.message)
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
|
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
|
||||||
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<div class="muted small">@datetime(activity.activityDate)</div>
|
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||||
<div class="strong">
|
<div class="strong">
|
||||||
@avatar(activity.activityUserName, 16)
|
@avatar(activity.activityUserName, 16)
|
||||||
@activityMessage(activity.message)
|
@activityMessage(activity.message)
|
||||||
@@ -91,7 +92,7 @@
|
|||||||
<div>
|
<div>
|
||||||
@avatar(activity.activityUserName, 16)
|
@avatar(activity.activityUserName, 16)
|
||||||
@activityMessage(activity.message)
|
@activityMessage(activity.message)
|
||||||
<span class="muted small">@datetime(activity.activityDate)</span>
|
<span class="muted small">@helper.html.datetimeago(activity.activityDate)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,24 @@
|
|||||||
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
|
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
|
try {
|
||||||
url: '@path/upload/image/@owner/@repository',
|
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
|
||||||
maxFilesize: 10,
|
url: '@path/upload/image/@owner/@repository',
|
||||||
acceptedFiles: 'image/*',
|
maxFilesize: 10,
|
||||||
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.',
|
acceptedFiles: 'image/*',
|
||||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.',
|
||||||
success: function(file, id) {
|
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||||
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
|
success: function(file, id) {
|
||||||
$('#@textareaId').val($('#@textareaId').val() + images);
|
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
|
||||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
$('#@textareaId').val($('#@textareaId').val() + images);
|
||||||
|
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
if (e.message !== "Dropzone already attached.") {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Adjust clickable area width
|
// Adjust clickable area width
|
||||||
$('#@textareaId').next('div.clickable').css('width', ($('#@textareaId').width() + 8) + 'px');
|
$('#@textareaId').next('div.clickable').css('width', ($('#@textareaId').width() + 8) + 'px');
|
||||||
|
|||||||
62
src/main/twirl/helper/branchcontrol.scala.html
Normal file
62
src/main/twirl/helper/branchcontrol.scala.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
@(branch: String = "",
|
||||||
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
|
hasWritePermission: Boolean)(body: Html)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
@helper.html.dropdown(
|
||||||
|
value = if(branch.length == 40) branch.substring(0, 10) else branch,
|
||||||
|
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree",
|
||||||
|
mini = true
|
||||||
|
) {
|
||||||
|
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">×</button></div></li>
|
||||||
|
<li><input id="branch-control-input" type="text" placeholder="Find or create branch ..."/></li>
|
||||||
|
@body
|
||||||
|
@if(hasWritePermission) {
|
||||||
|
<li id="create-branch" style="display: none;">
|
||||||
|
<a><form action="@url(repository)/branches" method="post" style="margin: 0;">
|
||||||
|
<span class="new-branch-name">Create branch: <span class="new-branch"></span></span>
|
||||||
|
<br><span style="padding-left: 17px;">from '@branch'</span>
|
||||||
|
<input type="hidden" name="new">
|
||||||
|
<input type="hidden" name="from" value="@branch">
|
||||||
|
</form></a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#branch-control-input').parent().click(function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
$('#branch-control-close').click(function() {
|
||||||
|
$('[data-toggle="dropdown"]').parent().removeClass('open');
|
||||||
|
});
|
||||||
|
$('#branch-control-input').keyup(function() {
|
||||||
|
var inputVal = $('#branch-control-input').val();
|
||||||
|
$.each($('#branch-control-input').parent().parent().find('a'), function(index, elem) {
|
||||||
|
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
|
||||||
|
$(elem).parent().show();
|
||||||
|
} else {
|
||||||
|
$(elem).parent().hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@if(hasWritePermission) {
|
||||||
|
if (inputVal) {
|
||||||
|
$('#create-branch').parent().find('li:last-child').show().find('.new-branch').text(inputVal);
|
||||||
|
} else {
|
||||||
|
$('#create-branch').parent().find('li:last-child').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@if(hasWritePermission) {
|
||||||
|
$('#create-branch').click(function() {
|
||||||
|
$(this).find('input[name="new"]').val($('.dropdown-menu input').val())
|
||||||
|
$(this).find('form').submit()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('.btn-group').click(function() {
|
||||||
|
$('#branch-control-input').val('');
|
||||||
|
$('.dropdown-menu li').show();
|
||||||
|
$('#create-branch').hide();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
35
src/main/twirl/helper/commitcomment.scala.html
Normal file
35
src/main/twirl/helper/commitcomment.scala.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@(comment: model.CommitComment,
|
||||||
|
hasWritePermission: Boolean,
|
||||||
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
|
latestCommitId: Option[String] = None)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
<div class="@if(comment.fileName.isDefined && (!latestCommitId.isDefined || latestCommitId.get == comment.commitId)){inline-comment}" @if(comment.fileName.isDefined){filename=@comment.fileName.get} @if(comment.newLine.isDefined){newline=@comment.newLine.get} @if(comment.oldLine.isDefined){oldline=@comment.oldLine.get}>
|
||||||
|
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||||
|
<div class="box commit-comment-box commit-comment-@comment.commentId">
|
||||||
|
<div class="box-header-small">
|
||||||
|
@user(comment.commentedUserName, styleClass="username strong")
|
||||||
|
<span class="muted">
|
||||||
|
commented
|
||||||
|
@if(comment.pullRequest){
|
||||||
|
on this Pull Request
|
||||||
|
}else{
|
||||||
|
@if(comment.fileName.isDefined){
|
||||||
|
on @comment.fileName.get
|
||||||
|
}
|
||||||
|
in <a href="@path/@repository.owner/@repository.name/commit/@comment.commitId">@comment.commitId.substring(0, 7)</a>
|
||||||
|
}
|
||||||
|
@helper.html.datetimeago(comment.registeredDate)
|
||||||
|
</span>
|
||||||
|
<span class="pull-right">
|
||||||
|
@if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
|
||||||
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||||
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-content commit-commentContent-@comment.commentId">
|
||||||
|
@markdown(comment.content, repository, false, true, true, hasWritePermission)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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() {
|
||||||
|
|||||||
10
src/main/twirl/helper/datetimeago.scala.html
Normal file
10
src/main/twirl/helper/datetimeago.scala.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@(latestUpdatedDate: java.util.Date,
|
||||||
|
recentOnly: Boolean = true)
|
||||||
|
@import view.helpers._
|
||||||
|
<span data-toggle="tooltip" title="@datetime(latestUpdatedDate)">
|
||||||
|
@if(recentOnly){
|
||||||
|
@datetimeAgoRecentOnly(latestUpdatedDate)
|
||||||
|
}else{
|
||||||
|
@datetimeAgo(latestUpdatedDate)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
@(diffs: Seq[util.JGitUtil.DiffInfo],
|
@(diffs: Seq[util.JGitUtil.DiffInfo],
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
newCommitId: Option[String],
|
newCommitId: Option[String],
|
||||||
oldCommitId: Option[String],
|
oldCommitId: Option[String],
|
||||||
showIndex: Boolean)(implicit context: app.Context)
|
showIndex: Boolean,
|
||||||
|
pullRequest: Boolean,
|
||||||
|
hasWritePermission: Boolean,
|
||||||
|
showLineNotes: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||||
@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>
|
</div>
|
||||||
Showing @diffs.size changed @plural(diffs.size, "file")
|
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) =>
|
||||||
@@ -36,9 +42,9 @@
|
|||||||
}
|
}
|
||||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||||
<a name="diff-@i"></a>
|
<a name="diff-@i"></a>
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered" commitId="@newCommitId" fileName="@diff.newPath">
|
||||||
<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 +72,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,12 +100,88 @@ $(function(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
// Render diffs as unified mode initially
|
||||||
@if(diff.newContent != None || diff.oldContent != None){
|
renderDiffs(1);
|
||||||
if($('#oldText-@i').length > 0){
|
|
||||||
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i');
|
$('#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) =>
|
||||||
|
@if(diff.newContent != None || diff.oldContent != None){
|
||||||
|
if($('#oldText-@i').length > 0){
|
||||||
|
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i', viewType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if(showLineNotes){
|
||||||
|
$('.inline-comment').each(function(i, v) {
|
||||||
|
var $v = $(v), filename = $v.attr('filename'),
|
||||||
|
oldline = $v.attr('oldline'), newline = $v.attr('newline'),
|
||||||
|
tmp = $('<tr class="not-diff"><td colspan="3" style="white-space: initial; line-height: initial; padding: 10px;"></td></tr>');
|
||||||
|
tmp.children('td').html($(this).clone().show());
|
||||||
|
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
|
||||||
|
$(this).hide();
|
||||||
|
}
|
||||||
|
if (typeof oldline !== 'undefined') {
|
||||||
|
$('table[filename="' + filename + '"]').find('table.inlinediff').find('.oldline').filter(function() {
|
||||||
|
return new RegExp('^' + oldline + '$').test($(this).text());
|
||||||
|
}).parent().nextAll(':not(.not-diff):first').before(tmp);
|
||||||
|
} else {
|
||||||
|
$('table[filename="' + filename + '"]').find('table.inlinediff').find('.newline').filter(function() {
|
||||||
|
return new RegExp('^' + newline + '\\+$').test($(this).text());
|
||||||
|
}).parent().nextAll(':not(.not-diff):first').before(tmp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@if(hasWritePermission) {
|
||||||
|
$('table.diff tr').hover(
|
||||||
|
function() {
|
||||||
|
$(this).find('b').css('display', 'inline-block');
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
$(this).find('b').css('display', 'none');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$('.add-comment').click(function() {
|
||||||
|
var $this = $(this),
|
||||||
|
$tr = $(this).closest('tr');
|
||||||
|
if (!$tr.nextAll(':not(.not-diff):first').prev().hasClass('inline-comment-form')) {
|
||||||
|
var commitId = $(this).closest('.table-bordered').attr('commitId'),
|
||||||
|
fileName = $(this).closest('.table-bordered').attr('fileName'),
|
||||||
|
oldLineNumber = $(this).closest('.newline').prev('.oldline').text(),
|
||||||
|
newLineNumber = $(this).closest('.newline').clone().children().remove().end().text(),
|
||||||
|
url = '@url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName + '&pullRequest=@pullRequest';
|
||||||
|
if (!isNaN(oldLineNumber) && oldLineNumber != null && oldLineNumber !== '') {
|
||||||
|
url += ('&oldLineNumber=' + oldLineNumber)
|
||||||
|
}
|
||||||
|
if (!isNaN(newLineNumber) && newLineNumber != null && newLineNumber !== '') {
|
||||||
|
url += ('&newLineNumber=' + newLineNumber)
|
||||||
|
}
|
||||||
|
$.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
dataType : 'html'
|
||||||
|
},
|
||||||
|
function(responseContent) {
|
||||||
|
$this.hide();
|
||||||
|
var tmp = $('<tr class="inline-comment-form not-diff"><td colspan="3" style="white-space: initial; padding: 10px;"></td></tr>');
|
||||||
|
tmp.children('td').html(responseContent);
|
||||||
|
$tr.nextAll(':not(.not-diff):first').before(tmp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('table.diff').on('click', '.btn-default', function() {
|
||||||
|
$(this).closest('.inline-comment-form').remove();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
7
src/main/twirl/helper/error.scala.html
Normal file
7
src/main/twirl/helper/error.scala.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@(error: Option[Any])
|
||||||
|
@if(error.isDefined){
|
||||||
|
<div class='alert alert-danger'>
|
||||||
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
@error
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
@(info: Option[Any])
|
@(info: Option[Any])
|
||||||
@if(info.isDefined){
|
@if(info.isDefined){
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
@info
|
@info
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
|
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
|
||||||
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
|
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@@ -34,11 +34,12 @@ $(function(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('#preview').click(function(){
|
$('#preview').click(function(){
|
||||||
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
|
$(this).closest('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
|
||||||
$.post('@url(repository)/_preview', {
|
$.post('@url(repository)/_preview', {
|
||||||
content : $('#content').val(),
|
content : $('#content').val(),
|
||||||
enableWikiLink : @enableWikiLink,
|
enableWikiLink : @enableWikiLink,
|
||||||
enableRefsLink : @enableRefsLink
|
enableRefsLink : @enableRefsLink,
|
||||||
|
enableTaskList : @enableTaskList
|
||||||
}, function(data){
|
}, function(data){
|
||||||
$('#preview-area').html(data);
|
$('#preview-area').html(data);
|
||||||
prettyPrint();
|
prettyPrint();
|
||||||
|
|||||||
@@ -4,13 +4,23 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@main("GitBucket"){
|
@main("GitBucket"){
|
||||||
|
@dashboard.html.tab()
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@dashboard.html.tab()
|
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span8">
|
<div class="span8">
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
|
||||||
|
</div>
|
||||||
@helper.html.activities(activities)
|
@helper.html.activities(activities)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
|
@settings.information.map { information =>
|
||||||
|
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
||||||
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
@Html(information)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@if(loginAccount.isEmpty){
|
@if(loginAccount.isEmpty){
|
||||||
@signinform(settings)
|
@signinform(settings)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
@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">
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
@helper.html.preview(repository, "", false, true, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
|
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@@ -27,4 +28,4 @@ $(function(){
|
|||||||
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
|
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,98 +1,131 @@
|
|||||||
@(issue: model.Issue,
|
@(issue: Option[model.Issue],
|
||||||
comments: List[model.IssueComment],
|
comments: List[model.Comment],
|
||||||
hasWritePermission: Boolean,
|
hasWritePermission: Boolean,
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
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._
|
||||||
@comments.map { comment =>
|
@if(issue.isDefined){
|
||||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
|
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
|
||||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
<div class="box issue-comment-box">
|
||||||
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
<div class="box-header-small">
|
||||||
<div class="box-header-small">
|
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
|
||||||
<i class="icon-comment"></i>
|
<span class="pull-right">
|
||||||
@user(comment.commentedUserName, styleClass="username strong")
|
@if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
|
||||||
@if(comment.action == "comment"){
|
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil"></i></a>
|
||||||
commented
|
|
||||||
} else {
|
|
||||||
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
|
|
||||||
}
|
}
|
||||||
<span class="pull-right">
|
</span>
|
||||||
@datetime(comment.registeredDate)
|
</div>
|
||||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
|
<div class="box-content issue-content" id="issueContent">
|
||||||
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
@markdown(issue.get.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
</div>
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
</div>
|
||||||
}
|
}
|
||||||
</span>
|
|
||||||
</div>
|
@comments.map {
|
||||||
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
|
case comment: model.IssueComment => {
|
||||||
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
|
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
|
||||||
@defining(comment.content.substring(comment.content.length - 40)){ id =>
|
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||||
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
|
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
||||||
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true)
|
<div class="box-header-small">
|
||||||
}
|
@user(comment.commentedUserName, styleClass="username strong")
|
||||||
} else {
|
<span class="muted">
|
||||||
@if(comment.action == "refer"){
|
@if(comment.action == "comment"){
|
||||||
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
|
commented
|
||||||
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
|
} else {
|
||||||
|
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
|
||||||
|
}
|
||||||
|
@helper.html.datetimeago(comment.registeredDate)
|
||||||
|
</span>
|
||||||
|
<span class="pull-right">
|
||||||
|
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
||||||
|
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||||
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||||
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
|
||||||
|
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
|
||||||
|
@defining(comment.content.substring(comment.content.length - 40)){ id =>
|
||||||
|
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
|
||||||
|
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@markdown(comment.content, repository, false, true)
|
@if(comment.action == "refer"){
|
||||||
|
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
|
||||||
|
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@markdown(comment.content, repository, false, true, true, hasWritePermission)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if(comment.action == "merge"){
|
||||||
|
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
|
||||||
|
<span class="label label-info">Merged</span>
|
||||||
|
@avatar(comment.commentedUserName, 20)
|
||||||
|
@user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
|
||||||
|
@if(pullreq.get.requestUserName == repository.owner){
|
||||||
|
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
|
||||||
|
} else {
|
||||||
|
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
|
||||||
|
}
|
||||||
|
@helper.html.datetimeago(comment.registeredDate)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if(comment.action == "close" || comment.action == "close_comment"){
|
||||||
|
<div class="small issue-comment-action">
|
||||||
|
<span class="label label-important">Closed</span>
|
||||||
|
@avatar(comment.commentedUserName, 20)
|
||||||
|
@if(issue.isDefined && issue.get.isPullRequest){
|
||||||
|
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
|
||||||
|
} else {
|
||||||
|
@user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
|
||||||
|
<div class="small issue-comment-action">
|
||||||
|
<span class="label label-success">Reopened</span>
|
||||||
|
@avatar(comment.commentedUserName, 20)
|
||||||
|
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if(comment.action == "delete_branch"){
|
||||||
|
<div class="small issue-comment-action">
|
||||||
|
<span class="label">Deleted</span>
|
||||||
|
@avatar(comment.commentedUserName, 20)
|
||||||
|
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@if(comment.action == "merge"){
|
case comment: model.CommitComment => {
|
||||||
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
|
@helper.html.commitcomment(comment, hasWritePermission, repository, pullreq.map(_.commitIdTo))
|
||||||
<span class="label label-info">Merged</span>
|
|
||||||
@avatar(comment.commentedUserName, 20)
|
|
||||||
@user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
|
|
||||||
@if(pullreq.get.requestUserName == repository.owner){
|
|
||||||
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
|
|
||||||
} else {
|
|
||||||
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
|
|
||||||
}
|
|
||||||
@datetime(comment.registeredDate)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if(comment.action == "close" || comment.action == "close_comment"){
|
|
||||||
<div class="small issue-comment-action">
|
|
||||||
<span class="label label-important">Closed</span>
|
|
||||||
@avatar(comment.commentedUserName, 20)
|
|
||||||
@if(issue.isPullRequest){
|
|
||||||
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate)
|
|
||||||
} else {
|
|
||||||
@user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
|
|
||||||
<div class="small issue-comment-action">
|
|
||||||
<span class="label label-success">Reopened</span>
|
|
||||||
@avatar(comment.commentedUserName, 20)
|
|
||||||
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if(comment.action == "delete_branch"){
|
|
||||||
<div class="small issue-comment-action">
|
|
||||||
<span class="label">Deleted</span>
|
|
||||||
@avatar(comment.commentedUserName, 20)
|
|
||||||
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @datetime(comment.registeredDate)
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
$('i.icon-pencil').click(function(){
|
@if(issue.isDefined){
|
||||||
var id = $(this).closest('a').data('comment-id');
|
$('.issue-comment-box i.icon-pencil').click(function(){
|
||||||
$.get('@url(repository)/issue_comments/_data/' + id,
|
var id = $(this).closest('a').data('comment-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;
|
||||||
});
|
});
|
||||||
@@ -109,5 +142,122 @@ $(function(){
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
$(document).on('click', '.commit-comment-box i.icon-pencil', function(){
|
||||||
|
var id = $(this).closest('a').data('comment-id');
|
||||||
|
var url = '@url(repository)/commit_comments/_data/' + id;
|
||||||
|
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
|
||||||
|
|
||||||
|
$.get(url,
|
||||||
|
{
|
||||||
|
dataType : 'html'
|
||||||
|
},
|
||||||
|
function(data){
|
||||||
|
$content.empty().html(data);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
$(document).on('click', '.commit-comment-box i.icon-remove-circle', function(){
|
||||||
|
if(confirm('Are you sure you want to delete this?')) {
|
||||||
|
var id = $(this).closest('a').data('comment-id');
|
||||||
|
$.post('@url(repository)/commit_comments/delete/' + id,
|
||||||
|
function(data){
|
||||||
|
if(data > 0) {
|
||||||
|
$('.commit-comment-' + id).closest('.not-diff').remove();
|
||||||
|
$('.commit-comment-' + id).closest('.inline-comment').remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
var extractMarkdown = function(data){
|
||||||
|
$('body').append('<div id="tmp"></div>');
|
||||||
|
$('#tmp').html(data);
|
||||||
|
var markdown = $('#tmp textarea').val();
|
||||||
|
$('#tmp').remove();
|
||||||
|
return markdown;
|
||||||
|
};
|
||||||
|
|
||||||
|
var replaceTaskList = function(issueContentHtml, checkboxes) {
|
||||||
|
var ss = [],
|
||||||
|
markdown = extractMarkdown(issueContentHtml),
|
||||||
|
xs = markdown.split(/- \[[x| ]\]/g);
|
||||||
|
for (var i=0; i<xs.length; i++) {
|
||||||
|
ss.push(xs[i]);
|
||||||
|
if (checkboxes.eq(i).prop('checked')) ss.push('- [x]');
|
||||||
|
else ss.push('- [ ]');
|
||||||
|
}
|
||||||
|
ss.pop();
|
||||||
|
return ss.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
$('div[class*=commit-commentContent-]').on('click', ':checkbox', function(ev){
|
||||||
|
var $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
|
||||||
|
commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''),
|
||||||
|
checkboxes = $commentContent.find(':checkbox');
|
||||||
|
$.get('@url(repository)/commit_comments/_data/' + commentId,
|
||||||
|
{
|
||||||
|
dataType : 'html'
|
||||||
|
},
|
||||||
|
function(responseContent){
|
||||||
|
$.ajax({
|
||||||
|
url: '@url(repository)/commit_comments/edit/' + commentId,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
issueId : 0,
|
||||||
|
content : replaceTaskList(responseContent, checkboxes)
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
$('.commit-commentContent-' + commentId).html(data.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
@if(issue.isDefined){
|
||||||
|
$('#issueContent').on('click', ':checkbox', function(ev){
|
||||||
|
var checkboxes = $('#issueContent :checkbox');
|
||||||
|
$.get('@url(repository)/issues/_data/@issue.get.issueId',
|
||||||
|
{
|
||||||
|
dataType : 'html'
|
||||||
|
},
|
||||||
|
function(responseContent){
|
||||||
|
$.ajax({
|
||||||
|
url: '@url(repository)/issues/edit/@issue.get.issueId',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
title : $('#issueTitle').text(),
|
||||||
|
content : replaceTaskList(responseContent, checkboxes)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div[id^=commentContent-]').on('click', ':checkbox', function(ev){
|
||||||
|
var $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
|
||||||
|
commentId = $commentContent.attr('id').replace(/commentContent-/, ''),
|
||||||
|
checkboxes = $commentContent.find(':checkbox');
|
||||||
|
$.get('@url(repository)/issue_comments/_data/' + commentId,
|
||||||
|
{
|
||||||
|
dataType : 'html'
|
||||||
|
},
|
||||||
|
function(responseContent){
|
||||||
|
$.ajax({
|
||||||
|
url: '@url(repository)/issue_comments/edit/' + commentId,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
issueId : 0,
|
||||||
|
content : replaceTaskList(responseContent, checkboxes)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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)
|
@navigation("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>
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
|
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(){
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -10,31 +10,83 @@
|
|||||||
@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)
|
<div>
|
||||||
<ul class="nav nav-tabs pull-left fill-width">
|
<div class="show-title pull-right">
|
||||||
<li class="pull-left"><a href="@url(repository)/issues"><i class="icon-arrow-left"></i> Back to issue list</a></li>
|
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||||
<li class="pull-right">Issue #@issue.issueId</li>
|
<a class="btn btn-small" href="#" id="edit">Edit</a>
|
||||||
</ul>
|
}
|
||||||
|
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||||
|
</div>
|
||||||
|
<div class="edit-title pull-right" style="display: none;">
|
||||||
|
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
<span class="show-title">
|
||||||
|
<span id="show-title">@issue.title</span>
|
||||||
|
<span class="muted">#@issue.issueId</span>
|
||||||
|
</span>
|
||||||
|
<span class="edit-title" style="display: none;">
|
||||||
|
<span id="error-edit-title" class="error"></span>
|
||||||
|
<input type="text" style="width: 700px;" id="edit-title" value="@issue.title"/>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
@if(issue.closed) {
|
||||||
|
<span class="label label-important issue-status">Closed</span>
|
||||||
|
} else {
|
||||||
|
<span class="label label-success issue-status">Open</span>
|
||||||
|
}
|
||||||
|
<span class="muted">
|
||||||
|
@user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining(
|
||||||
|
comments.count( _.action.contains("comment") )
|
||||||
|
){ count =>
|
||||||
|
@count @plural(count, "comment")
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<br/><br/>
|
||||||
|
<hr>
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span10">
|
<div class="span10">
|
||||||
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
|
@commentlist(Some(issue), comments, hasWritePermission, repository)
|
||||||
@commentlist(issue, comments, hasWritePermission, repository)
|
|
||||||
@commentform(issue, true, hasWritePermission, repository)
|
@commentform(issue, true, hasWritePermission, repository)
|
||||||
</div>
|
</div>
|
||||||
<div class="span2">
|
<div class="span2">
|
||||||
@if(issue.closed) {
|
@issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||||
<span class="label label-important issue-status">Closed</span>
|
|
||||||
} else {
|
|
||||||
<span class="label label-success issue-status">Open</span>
|
|
||||||
}
|
|
||||||
<div class="small" style="text-align: center;">
|
|
||||||
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
|
|
||||||
<span class="strong">@count</span> @plural(count, "comment")
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
@issues.html.labels(issue, issueLabels, 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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
173
src/main/twirl/issues/issueinfo.scala.html
Normal file
173
src/main/twirl/issues/issueinfo.scala.html
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
@(issue: model.Issue,
|
||||||
|
comments: List[model.Comment],
|
||||||
|
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;"> </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>
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;"> </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>
|
|
||||||
@@ -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).getOrElse("888888")" 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}"/>
|
<script>
|
||||||
@if(mode == "edit"){
|
$('div#label-color-@labelId').colorpicker();
|
||||||
<input type="hidden" name="editLabelId" value="@label.map(_.labelId)"/>
|
</script>
|
||||||
}
|
<span class="pull-right">
|
||||||
|
<span id="label-error-@labelId" class="error"></span>
|
||||||
|
<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>
|
</form>
|
||||||
<script>
|
</div>
|
||||||
$(function(){
|
<script>
|
||||||
@if(mode == "new"){
|
$(function(){
|
||||||
$('#newColor').colorpicker();
|
$('#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 {
|
||||||
|
// Replace table row
|
||||||
|
$('#label-row-@labelId').after(data).remove();
|
||||||
|
}
|
||||||
|
}).fail(function(xhr, status, error){
|
||||||
|
var errors = JSON.parse(xhr.responseText);
|
||||||
|
if(errors.labelName){
|
||||||
|
$('span#label-error-@labelId').text(errors.labelName);
|
||||||
|
} else if(errors.labelColor){
|
||||||
|
$('span#label-error-@labelId').text(errors.labelColor);
|
||||||
|
} else {
|
||||||
|
$('span#label-error-@labelId').text('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#cancel-@labelId').click(function(e){
|
||||||
|
$('div#edit-label-area-@labelId').remove();
|
||||||
|
@if(labelId == "new"){
|
||||||
|
$('#new-label-table').hide();
|
||||||
} else {
|
} else {
|
||||||
$('#editColor').colorpicker();
|
$('#label-@labelId').show();
|
||||||
|
|
||||||
$('#edit-label-form').submit(function(e){
|
|
||||||
$.ajax($(this).attr('action'), {
|
|
||||||
type: 'POST',
|
|
||||||
data: $(this).serialize()
|
|
||||||
})
|
|
||||||
.done(function(data){
|
|
||||||
$('#label-edit').parent().empty().html(data);
|
|
||||||
})
|
|
||||||
.fail(function(data, status){
|
|
||||||
displayErrors($.parseJSON(data.responseText));
|
|
||||||
});
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
});
|
||||||
</div>
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"> </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>
|
|
||||||
36
src/main/twirl/issues/labels/label.scala.html
Normal file
36
src/main/twirl/issues/labels/label.scala.html
Normal 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>
|
||||||
|
|
||||||
|
<a href="javascript:void(0);" onclick="deleteLabel(@label.labelId)">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
67
src/main/twirl/issues/labels/list.scala.html
Normal file
67
src/main/twirl/issues/labels/list.scala.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@(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.navigation("labels", hasWritePermission, repository)
|
||||||
|
<br>
|
||||||
|
<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($('#edit-label-area-new').size() != 0){
|
||||||
|
$('div#edit-label-area-new').remove();
|
||||||
|
$('#new-label-table').hide();
|
||||||
|
} 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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user