Compare commits

..

6 Commits
2.4 ... 1.11.1

Author SHA1 Message Date
takezoe
7deea43c75 Update README.,md for 1.11.1 release 2014-03-05 22:15:22 +09:00
takezoe
8842e1fef2 Fix error when base url is configured. 2014-03-05 22:12:52 +09:00
takezoe
5090112c39 Replace Context#path with the base url if it's configured. 2014-03-05 19:22:36 +09:00
takezoe
d4a3e88f1a (refs #299)Fix redirection path in PullRequestsController 2014-03-05 16:25:08 +09:00
shimamoto
acada42d3f (refs #292) Fix to limit issue result before joins issue labels. 2014-03-05 15:03:24 +09:00
takezoe
b6cfbcd19c (refs #296)Fix redirection path generation again 2014-03-05 10:37:38 +09:00
459 changed files with 6945 additions and 276820 deletions

View File

@@ -1,15 +1,12 @@
GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/badge/icon)](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/) GitBucket
========= =========
GitBucket is the easily installable Github clone written with Scala. GitBucket is the easily installable Github clone written with Scala.
Features
--------
The current version of GitBucket provides a basic features below: The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http and ssh access) - Public / Private Git repository (http access only)
- Repository viewer and online file editing - Repository viewer (some advanced features such as online file editing are not implemented)
- Repository search (Code and Issues) - Repository search (Code and Issues)
- Wiki - Wiki
- Issues - Issues
@@ -23,9 +20,10 @@ 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!
- File editing in repository viewer
- Comment for the changeset - Comment for the changeset
- Network graph - Network graph
- Statistics - Statics
- Watch / Star - Watch / Star
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki). If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -37,8 +35,6 @@ Installation
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher. 2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser. 3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx)
The default administrator account is **root** and password is **root**. The default administrator account is **root** and password is **root**.
or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options. or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
@@ -53,24 +49,6 @@ To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored i
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo) For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X ### Mac OS X
#### Installing Via Homebrew
$ brew install gitbucket
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
######################################################################## 100.0%
==> Caveats
Note: When using launchctl the port will be 8080.
To have launchd start gitbucket at login:
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
Then to load gitbucket now:
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
Or, if you don't want/need launchctl, you can just run:
java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
==> Summary
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
#### Manual Installation
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/` On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to Run the following commands in `Terminal` to
@@ -80,55 +58,6 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 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
- Plug-in system is available
- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1
- tar.gz export for repository contents
- LDAP authentication improvement (mail address became optional)
- Show news feed of a private repository to members
- Some bug fix and improvements
### 2.1 - 6 Jul 2014
- Upgrade to Slick 2.0 from 1.9
- Base part of the plug-in system is merged
- Many bug fix and improvements
### 2.0 - 31 May 2014
- Modern Github UI
- Preview in AceEditor
- Select lines by clicking line number in blob view
### 1.13 - 29 Apr 2014
- Direct file editing in the repository viewer using AceEditor
- File attachment for issues
- Atom feed of user activity
- Fix some bugs
### 1.12 - 29 Mar 2014
- SSH repository access is available
- Allow users can create and management their groups
- Git submodule support
- Close issues via commit messages
- Show repository description below the name on repository page
- Fix presentation of the source viewer
- Upgrade to sbt 0.13
- Fix some bugs
### 1.11.1 - 06 Mar 2014 ### 1.11.1 - 06 Mar 2014
- Bug fix - Bug fix

View File

@@ -4,7 +4,7 @@
<property name="target.dir" value="target"/> <property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/> <property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/> <property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.11"/> <property name="scala.version" value="2.10"/>
<property name="gitbucket.version" value="0.0.1"/> <property name="gitbucket.version" value="0.0.1"/>
<property name="jetty.version" value="8.1.8.v20121106"/> <property name="jetty.version" value="8.1.8.v20121106"/>
<property name="servlet.version" value="3.0.0.v201112011016"/> <property name="servlet.version" value="3.0.0.v201112011016"/>
@@ -50,8 +50,8 @@
</target> </target>
<target name="rename" depends="embed"> <target name="rename" depends="embed">
<move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war" <rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/> dest="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target> </target>
<target name="all" depends="rename"> <target name="all" depends="rename">

View File

@@ -1,13 +0,0 @@
# 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`

View File

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

View File

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

View File

@@ -1,10 +1,3 @@
#!/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">
@@ -14,15 +7,14 @@ cat << EOF > "$GITBUCKET_SERVICE_DIR/gitbucket.plist"
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/usr/bin/java</string> <string>/usr/bin/java</string>
<string>$GITBUCKET_JVM_OPTS</string> <string>-Dmail.smtp.starttls.enable=true</string>
<string>-jar</string> <string>-jar</string>
<string>gitbucket.war</string> <string>gitbucket.war</string>
<string>--host=$GITBUCKET_HOST</string> <string>--host=127.0.0.1</string>
<string>--port=$GITBUCKET_PORT</string> <string>--port=8080</string>
<string>--https=true</string> <string>--https=true</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
</dict> </dict>
</plist> </plist>
EOF

View File

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

View File

@@ -1,8 +1,6 @@
#!/bin/bash #!/bin/bash
# #
# RedHat: /etc/rc.d/init.d/gitbucket # /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
# #
@@ -10,44 +8,28 @@
# description: Run GitBucket server # description: Run GitBucket server
# processname: java # processname: java
set -e # Source function library
. /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 ] && source /etc/sysconfig/gitbucket # RedHat [ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket
[ -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=$GITBUCKET_LOG_DIR/run.log LOG_FILE=/var/log/gitbucket/run.log
PID_FILE=/var/run/gitbucket.pid PID_FILE=/var/run/gitbucket.pid
RED='\033[1m\E[37;41m' # Default return value
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 RETVAL=0
start() { start() {
echo -n $"Starting GitBucket server: " echo -n $"Starting GitBucket server: "
START_OPTS= # Compile statup parameters
if [ $GITBUCKET_PORT ]; then if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}" START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi fi
@@ -57,16 +39,21 @@ start() {
if [ $GITBUCKET_HOST ]; then if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}" START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi fi
if [ $GITBUCKET_HTTPS ]; then
START_OPTS="${START_OPTS} --https=true"
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 "Success" success "GitBucket startup"
else else
failure "Exit code $RETVAL" failure "GitBucket startup"
fi fi
echo echo
@@ -98,41 +85,25 @@ restart() {
start start
} }
## MacOS proxies for System V service hooks:
StartService() { case "$1" in
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
if [ `isMac` ]; then exit $RETVAL
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

View File

@@ -2,7 +2,6 @@
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
@@ -17,16 +16,7 @@
inkscape:version="0.48.4 r9939" inkscape:version="0.48.4 r9939"
sodipodi:docname="icons.svg"> sodipodi:docname="icons.svg">
<defs <defs
id="defs4"> id="defs4" />
<linearGradient
id="linearGradient4044"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop4046" />
</linearGradient>
</defs>
<sodipodi:namedview <sodipodi:namedview
id="base" id="base"
pagecolor="#ffffff" pagecolor="#ffffff"
@@ -34,15 +24,15 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="0.98994949" inkscape:zoom="1.4"
inkscape:cx="174.78739" inkscape:cx="450.21999"
inkscape:cy="-195.96338" inkscape:cy="97.51519"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1-9" inkscape:current-layer="layer1-9"
showgrid="false" showgrid="false"
inkscape:window-width="1366" inkscape:window-width="1366"
inkscape:window-height="715" inkscape:window-height="706"
inkscape:window-x="-8" inkscape:window-x="1912"
inkscape:window-y="-8" inkscape:window-y="-8"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:snap-global="false" inkscape:snap-global="false"
@@ -988,857 +978,6 @@
id="path4310" id="path4310"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
sodipodi:nodetypes="ccscc" /> sodipodi:nodetypes="ccscc" />
<path
id="path2991-7-1-4"
transform="translate(668.66057,1115.0272)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-5-8"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,711.41194,1163.4493)" />
<rect
id="rect2995-0-2-8"
y="1378.4849"
x="916.58545"
height="99.396141"
width="20.706863"
style="fill:#b3b3b3;stroke:none;stroke-width:0.93666755999999995" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 884.0251,1366.2678 -64.6851,-36.2114 10.70013,55.9569 53.98497,-19.7455 z"
id="rect4046-3-4"
inkscape:connector-curvature="0" />
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.98877633;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 873.36878,1359.3959 -43.65605,-24.4345 6.99871,38.1562 36.65734,-13.7217 z"
id="rect4046-5"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#b3b3b3;stroke-width:13.63542366;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 1162.2186,1316.0972 c -0.2525,22.2049 -0.505,44.4098 -0.7575,66.6147 3.3299,0.032 6.6599,0.063 9.9898,0.095 -2.3515,2.3672 -4.703,4.7345 -7.0544,7.1018 31.3741,31.374 62.7482,62.7482 94.1223,94.1223 23.3412,-23.3412 46.6824,-46.6824 70.0236,-70.0236 -31.3741,-31.3899 -62.7483,-62.7798 -94.1224,-94.1697 -2.6197,2.6356 -5.2395,5.2711 -7.8593,7.9067 0.032,-3.6298 0.063,-7.2596 0.095,-10.8894 -21.4789,-0.2525 -42.9579,-0.505 -64.4368,-0.7575 z"
id="rect3075-11"
inkscape:connector-curvature="0" />
<rect
id="rect2995-0-2-8-6"
y="899.99463"
x="-1417.3273"
height="99.396141"
width="20.706863"
style="fill:#b3b3b3;stroke:none;stroke-width:0.93666755999999995"
transform="matrix(0,-1,1,0,0,0)" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:10.37699986;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 1172.5522,1326.6744 c -0.1922,16.8985 -0.3844,33.7973 -0.5765,50.6959 2.5342,0.024 5.0684,0.047 7.6026,0.072 -1.7896,1.8014 -3.5792,3.6031 -5.3686,5.4047 23.8766,23.8766 47.7533,47.7533 71.63,71.6301 17.7636,-17.7634 35.5269,-35.5268 53.2902,-53.2902 -23.8766,-23.8888 -47.7534,-47.7775 -71.6302,-71.6662 -1.9936,2.0058 -3.9873,4.0114 -5.9811,6.0172 0.024,-2.7624 0.047,-5.5247 0.072,-8.2872 -16.3463,-0.1921 -32.6925,-0.3843 -49.0386,-0.5764 z"
id="rect3075-11-7"
inkscape:connector-curvature="0" />
<path
sodipodi:type="arc"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:6.57334423;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none"
id="path3100-2"
sodipodi:cx="700"
sodipodi:cy="812.36218"
sodipodi:rx="10"
sodipodi:ry="10"
d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-667.98357,1217.7251)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#b3b3b3;stroke-width:10.80681515;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4114"
width="45.086407"
height="62.401226"
x="-133.16023"
y="1850.2394"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
<path
id="path2991-7-6"
transform="translate(1090.5728,-207.2632)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#a0a0a0;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-8"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#808080;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,1133.3242,-158.84107)" />
<rect
id="rect2995-0-8"
y="10.829478"
x="1332.5247"
height="99.221687"
width="29.189819"
style="fill:#a0a0a0;stroke:#ffffff;stroke-width:1.11112404000000000;fill-opacity:1" />
<rect
id="rect2997-9-2"
y="129.62337"
x="1332.7828"
height="26.258072"
width="29.724136"
style="fill:#a0a0a0;stroke:#ffffff;stroke-width:0.57680577000000000;fill-opacity:1" />
<g
id="g4284-1"
transform="translate(670.07237,-816.24186)"
style="stroke:#a0a0a0;stroke-opacity:1">
<path
sodipodi:nodetypes="czcczcc"
inkscape:connector-curvature="0"
id="rect4201-26"
d="m 568.37427,1080.8464 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43273,8.6574 40.43273,8.6574 l 0,141.4674 c 0,0 -20.97035,-7.7215 -40.43273,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:14.36538028999999900;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" />
<rect
y="1108.1473"
x="597.4068"
height="5.4857273"
width="55.265846"
id="rect4203-0"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="1142.7776"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-4"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="1176.1093"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-3-9"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="czc"
inkscape:connector-curvature="0"
id="path4245-4"
d="m 563.55369,1233.6274 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29117,14.7566 46.29117,14.7566"
style="fill:#b3b3b3;stroke:#a0a0a0;stroke-width:19.63722609999999900;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<g
transform="matrix(-1.0032405,0,0,1,1329.8708,99.560238)"
id="g4277-6"
style="stroke:#a0a0a0;stroke-opacity:1">
<path
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:14.36538124000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
d="m 519.67634,980.83663 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43272,8.6574 40.43272,8.6574 l 0,141.46737 c 0,0 -20.97034,-7.7215 -40.43272,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
id="rect4201-2-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcczcc" />
<rect
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4203-21-3"
width="55.26585"
height="5.4857273"
x="548.70886"
y="1008.1376" />
<rect
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4203-2-6-6"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1042.7678" />
<rect
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4203-2-3-8-2"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1076.0995" />
<path
style="fill:#b3b3b3;stroke:#a0a0a0;stroke-width:19.63722609999999900;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 514.85576,1133.6176 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29116,14.7566 46.29116,14.7566"
id="path4245-5-4"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czc" />
</g>
</g>
<path
inkscape:connector-curvature="0"
id="path3850-1-1"
d="m 1409.5992,670.87038 0,-128.57724 c 0,0 1.8599,-15.30681 -16.7384,-15.30681 -18.5984,0 -51.1454,0 -51.1454,0"
style="fill:none;stroke:#a0a0a0;stroke-width:22.72570610000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
y="547.80316"
x="1294.749"
height="104.27072"
width="3.2554622"
id="rect3818-4-7"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="matrix(1.0049237,0,0,0.61497516,944.16607,536.33294)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-4"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,942.63054,386.00935)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-0"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,1056.9547,536.43446)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0-2-9"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852-4-4"
d="m 1369.3146,490.2451 0,70.69144 -45.5889,-32.13462 z"
style="fill:#a0a0a0;stroke:#a0a0a0;stroke-width:0.83335358000000004px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" />
<rect
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953"
width="15.304287"
height="97.947441"
x="1474.2273"
y="-367.14282"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-8"
width="15.304287"
height="97.947441"
x="-281.45197"
y="-1573.058"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-82"
width="15.304287"
height="97.947441"
x="-412.46057"
y="-1617.4926"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-82-4"
width="15.304287"
height="97.947441"
x="-1617.2937"
y="306.0546"
transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)" />
<g
id="g4016"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,3107.8871,982.01044)">
<rect
transform="scale(-1,-1)"
y="-1119.1083"
x="-1370.8767"
height="105.80523"
width="33.87508"
id="rect3953-82-4-1-4"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:16.74562263;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
transform="scale(-1,-1)"
y="-1207.0963"
x="-1358.6217"
height="113.43421"
width="9.3650599"
id="rect3953-82-4-1-7-0"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:9.11664104;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
transform="matrix(1.5972925,0,0,1.509886,-99.098035,-27.987625)"
d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
sodipodi:ry="26.263966"
sodipodi:rx="26.263966"
sodipodi:cy="838.20984"
sodipodi:cx="910.14746"
id="path3226"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:7.14799976;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.1933397,0,0,1.5429659,269.38527,-37.350485)"
d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
sodipodi:ry="26.263966"
sodipodi:rx="26.263966"
sodipodi:cy="838.20984"
sodipodi:cx="910.14746"
id="path3226-9"
style="fill:#ffffff;fill-opacity:1;stroke:none"
sodipodi:type="arc" />
</g>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect4027"
width="73.460579"
height="107.13"
x="1722.2299"
y="-207.2868"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<g
id="g4022"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,1095.262,-713.65443)">
<rect
transform="scale(-1,-1)"
y="-1121.4039"
x="-1505.5544"
height="105.80523"
width="33.87508"
id="rect3953-82-4-1"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:16.74562263;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
transform="scale(-1,-1)"
y="-1222.4006"
x="-1494.0955"
height="113.43421"
width="9.3650599"
id="rect3953-82-4-1-7"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:9.11664104;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="rect3182"
d="m 1469.5507,1220.8203 20.5952,52.7426 20.6425,-52.7426 -2.0832,-5.35 -37.0713,0 -2.0832,5.35 z"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:7.02416945;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
<path
id="path2991-7-6-1"
transform="translate(1482.3625,-199.43254)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#3c3c3c;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-8-7"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#3c3c80;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,1525.1139,-151.0104)" />
<rect
id="rect2995-0-8-4"
y="18.660131"
x="1724.3145"
height="99.221687"
width="29.189819"
style="fill:#3c3c3c;fill-opacity:1;stroke:#ffffff;stroke-width:1.11112404000000000" />
<rect
id="rect2997-9-2-0"
y="137.45401"
x="1724.5726"
height="26.258072"
width="29.724136"
style="fill:#3c3c3c;fill-opacity:1;stroke:#ffffff;stroke-width:0.57680577000000000" />
<g
id="g4284-1-9"
transform="translate(1061.8621,-808.41119)"
style="stroke:#3c3c3c;stroke-opacity:1">
<path
sodipodi:nodetypes="czcczcc"
inkscape:connector-curvature="0"
id="rect4201-26-4"
d="m 568.37427,1080.8464 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43273,8.6574 40.43273,8.6574 l 0,141.4674 c 0,0 -20.97035,-7.7215 -40.43273,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:14.36538028999999900;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" />
<rect
y="1108.1473"
x="597.4068"
height="5.4857273"
width="55.265846"
id="rect4203-0-8"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
y="1142.7776"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-4-8"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
y="1176.1093"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-3-9-2"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
sodipodi:nodetypes="czc"
inkscape:connector-curvature="0"
id="path4245-4-4"
d="m 563.55369,1233.6274 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29117,14.7566 46.29117,14.7566"
style="fill:#b3b3b3;stroke:#3c3c3c;stroke-width:19.63722609999999900;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<g
transform="matrix(-1.0032405,0,0,1,1329.8708,99.560238)"
id="g4277-6-5"
style="stroke:#3c3c3c;stroke-opacity:1">
<path
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:14.36538124000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
d="m 519.67634,980.83663 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43272,8.6574 40.43272,8.6574 l 0,141.46737 c 0,0 -20.97034,-7.7215 -40.43272,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
id="rect4201-2-0-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcczcc" />
<rect
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4203-21-3-1"
width="55.26585"
height="5.4857273"
x="548.70886"
y="1008.1376" />
<rect
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4203-2-6-6-7"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1042.7678" />
<rect
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4203-2-3-8-2-1"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1076.0995" />
<path
style="fill:#b3b3b3;stroke:#3c3c3c;stroke-width:19.63722609999999900;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 514.85576,1133.6176 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29116,14.7566 46.29116,14.7566"
id="path4245-5-4-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czc" />
</g>
</g>
<path
inkscape:connector-curvature="0"
id="path3850-1-1-5"
d="m 1801.3889,678.70099 0,-128.5772 c 0,0 1.8599,-15.3068 -16.7384,-15.3068 -18.5984,0 -51.1454,0 -51.1454,0"
style="fill:none;stroke:#3c3c3c;stroke-width:22.72570610000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
y="555.63385"
x="1686.5388"
height="104.27072"
width="3.2554622"
id="rect3818-4-7-2"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,1335.9558,544.16359)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-4-7"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,1334.4203,393.83999)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-0-6"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,1448.7444,544.26509)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0-2-9-1"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852-4-4-4"
d="m 1761.1043,498.07579 0,70.6914 -45.5889,-32.1346 z"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:0.83335358000000004px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-2"
width="15.304287"
height="97.947441"
x="1756.8015"
y="-638.64288"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-8-3"
width="15.304287"
height="97.947441"
x="-552.95203"
y="-1855.6323"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-82-2"
width="15.304287"
height="97.947441"
x="-683.96063"
y="-1900.0669"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-82-4-2"
width="15.304287"
height="97.947441"
x="-1899.8679"
y="577.55469"
transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)" />
<g
id="g4138">
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
y="2055.4602"
x="403.84506"
height="105.80523"
width="33.87508"
id="rect3953-82-4-1-4-6"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:16.74562263000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
y="1967.4722"
x="416.10007"
height="113.43421"
width="9.3650599"
id="rect3953-82-4-1-7-0-8"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:9.11664103999999930;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
transform="matrix(-1.1294564,1.1294564,-1.0676506,-1.0676506,3589.5398,939.55844)"
d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
sodipodi:ry="26.263966"
sodipodi:rx="26.263966"
sodipodi:cy="838.20984"
sodipodi:cx="910.14746"
id="path3226-5"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:7.14799976000000030;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(-0.84381859,0.84381859,-1.0910416,-1.0910416,3335.6033,1206.736)"
d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
sodipodi:ry="26.263966"
sodipodi:rx="26.263966"
sodipodi:cy="838.20984"
sodipodi:cx="910.14746"
id="path3226-9-7"
style="fill:#ffffff;fill-opacity:1;stroke:none"
sodipodi:type="arc" />
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
y="-476.95105"
x="2003.8865"
height="107.13"
width="73.460579"
id="rect4027-6"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<rect
transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)"
y="429.19318"
x="-2057.9661"
height="105.80523"
width="33.87508"
id="rect3953-82-4-1-8"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:16.74562263000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)"
y="328.19647"
x="-2046.5071"
height="113.43421"
width="9.3650599"
id="rect3953-82-4-1-7-9"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:9.11664103999999930;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="rect3182-2"
d="m 1662.9307,1196.5558 -22.7317,51.8577 51.8911,-22.6982 2.31,-5.2561 -26.2134,-26.2134 -5.256,2.31 z"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:7.02416944999999960;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
<path
id="path2991-7-1-4-1"
transform="translate(-154.10522,1432.0357)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#bebeff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-5-8-7"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,-111.35384,1480.4578)" />
<rect
id="rect2995-0-2-8-4"
y="1695.4933"
x="93.81971"
height="99.396141"
width="20.706863"
style="fill:#bebefa;stroke:none;fill-opacity:1" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 61.259358,1683.2763 -64.6850999,-36.2114 10.7001,55.9569 53.9849999,-19.7455 z"
id="rect4046-3-4-0"
inkscape:connector-curvature="0" />
<path
style="fill:#bebeff;stroke:#bebeff;stroke-width:1.98877633000000010;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1;stroke-opacity:1"
d="m 50.602958,1676.4044 -43.6559999,-24.4345 6.9986999,38.1562 36.6573,-13.7217 z"
id="rect4046-5-9"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#bebefa;stroke-width:13.63542366000000100;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 339.45286,1633.1057 c -0.2525,22.2049 -0.505,44.4098 -0.7575,66.6147 3.3299,0.032 6.6598,0.063 9.9898,0.095 -2.3515,2.3672 -4.703,4.7345 -7.0544,7.1018 31.3741,31.374 62.7482,62.7482 94.1222,94.1223 23.3413,-23.3412 46.6825,-46.6824 70.0237,-70.0236 -31.3741,-31.3899 -62.7483,-62.7798 -94.1224,-94.1697 -2.6197,2.6356 -5.2395,5.2711 -7.8593,7.9067 0.032,-3.6298 0.063,-7.2596 0.095,-10.8894 -21.4789,-0.2525 -42.9579,-0.505 -64.4368,-0.7575 z"
id="rect3075-11-4"
inkscape:connector-curvature="0" />
<rect
id="rect2995-0-2-8-6-8"
y="77.228889"
x="-1734.3357"
height="99.396141"
width="20.706863"
style="fill:#bebefa;stroke:none;fill-opacity:1"
transform="matrix(0,-1,1,0,0,0)" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:10.37699986;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 349.78646,1643.6829 c -0.1922,16.8985 -0.3844,33.7973 -0.5765,50.6959 2.5342,0.024 5.0684,0.047 7.6026,0.072 -1.7896,1.8014 -3.5792,3.6031 -5.3686,5.4047 23.8766,23.8766 47.7533,47.7533 71.63,71.6301 17.7636,-17.7634 35.5269,-35.5268 53.2902,-53.2902 -23.8766,-23.8888 -47.7534,-47.7775 -71.6302,-71.6662 -1.9936,2.0058 -3.9873,4.0114 -5.9811,6.0172 0.024,-2.7624 0.047,-5.5247 0.072,-8.2872 -16.3463,-0.1921 -32.6925,-0.3843 -49.0386,-0.5764 z"
id="rect3075-11-7-8"
inkscape:connector-curvature="0" />
<path
sodipodi:type="arc"
style="fill:#ffffff;stroke:#bebefa;stroke-width:6.57334423000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none"
id="path3100-2-2"
sodipodi:cx="700"
sodipodi:cy="812.36218"
sodipodi:rx="10"
sodipodi:ry="10"
d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-1490.7493,1534.7336)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#bebeff;stroke-width:10.80681515000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4114-4"
width="45.086407"
height="62.401226"
x="-939.10236"
y="1492.6151"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
<path
style="fill:none;stroke:#bebeff;stroke-width:25.84518814000000100;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 655.10691,1790.494 c 0,0 3.44333,-28.5633 47.63498,-35.4849 15.10377,-2.3655 48.7968,-8.2798 48.7968,-42.5816"
id="path3207-5"
inkscape:connector-curvature="0"
inkscape:transform-center-x="-9.2946303"
sodipodi:nodetypes="csc"
inkscape:transform-center-y="2.9369479e-005" />
<rect
y="1676.2623"
x="652.97418"
height="104.27072"
width="3.2554622"
id="rect3818-4-8-4-5"
style="fill:#ffffff;stroke:#bebefa;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="matrix(1.0049237,0,0,0.61497516,302.39116,1664.7945)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-7-8-1"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,300.85563,1514.4712)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4-8-7"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,401.70879,1561.5007)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4-8-2-1"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<g
id="g3992">
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
style="fill:#3c3c3c;fill-opacity:1;stroke:none"
width="34.635483"
height="158.96587"
x="1836.6243"
y="-1788.4895"
id="rect2995-0-8-4-1" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
style="fill:#3c3c3c;fill-opacity:1;stroke:none"
width="33.538391"
height="96.944809"
x="1628.6003"
y="1772.8655"
id="rect2995-0-8-4-1-4" />
</g>
<g
id="g4112"
transform="translate(88.611046,-13.773858)">
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
style="fill:#a0a0a0;fill-opacity:1;stroke:none"
width="34.635483"
height="158.96587"
x="1527.2657"
y="-1466.7803"
id="rect2995-0-8-4-1-5" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
style="fill:#a0a0a0;fill-opacity:1;stroke:none"
width="33.538391"
height="96.944809"
x="1306.8911"
y="1463.507"
id="rect2995-0-8-4-1-4-5" />
</g>
<path
style="fill:#b3b3b3;stroke:none"
d="m 2185.2705,373.3859 -109.47,85.45235 29.4727,-89.94984 z"
id="path3894-1-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:49.97417831;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="rect3088-5-5-7"
width="174.36192"
height="89.170021"
x="2060.0393"
y="293.00055" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#dcdcdc;stroke-width:2.10925268999999990;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4170"
width="35.913948"
height="206.36755"
x="2110.2112"
y="507.8555" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#ffffff;stroke-width:15.12008381000000100;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4166"
width="174.5864"
height="76.446434"
x="2035.1414"
y="548.66016" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#dcdcdc;stroke-width:1.42725468000000010;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4174"
width="43.442127"
height="43.442127"
x="1928.0846"
y="-1122.7543"
transform="matrix(0.72181305,0.69208809,-0.72181305,0.69208809,0,0)" />
<path
sodipodi:type="arc"
style="fill:#ffffe6;fill-opacity:1;stroke:#ffffff;stroke-width:10.1960001;stroke-linejoin:miter;stroke-miterlimit:4.30000019;stroke-opacity:1;stroke-dasharray:none"
id="path4364"
sodipodi:cx="1418.2542"
sodipodi:cy="434.14883"
sodipodi:rx="11.111678"
sodipodi:ry="11.111678"
d="m 1429.3658,434.14883 c 0,6.13681 -4.9748,11.11168 -11.1116,11.11168 -6.1369,0 -11.1117,-4.97487 -11.1117,-11.11168 0,-6.13681 4.9748,-11.11167 11.1117,-11.11167 6.1368,0 11.1116,4.97486 11.1116,11.11167 z"
transform="matrix(1.2783369,0,0,1.2783369,315.0834,31.171302)" />
<path
style="fill:#dcdcdc;stroke:none;fill-opacity:1"
d="m 2533.6893,373.6989 -109.47,85.45235 29.4727,-89.94984 z"
id="path3894-1-1-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#dcdcdc;stroke:#dcdcdc;stroke-width:49.97417449999999700;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="rect3088-5-5-7-7"
width="174.36192"
height="89.170021"
x="2408.458"
y="293.31354" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#888888;stroke-width:73.08132935000000400;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3220"
width="104.54597"
height="104.54597"
x="45.94949"
y="1925.303" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1"
width="117.84303"
height="30.608574"
x="1271.0641"
y="-1484.6459"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-7"
width="117.84303"
height="30.608574"
x="1408.8896"
y="1314.712"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
style="fill:#0088cc;fill-opacity:1;stroke:#0088cc;stroke-width:73.08132935000000400;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3220-4"
width="104.54597"
height="104.54597"
x="337.49615"
y="1924.5376" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-0"
width="117.84303"
height="30.608574"
x="1064.3683"
y="-1690.2594"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-7-9"
width="117.84303"
height="30.608574"
x="1614.5032"
y="1108.0162"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

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

View File

@@ -1,60 +1,53 @@
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"
val Name = "gitbucket" val Name = "gitbucket"
val Version = "0.0.1" val Version = "0.0.1"
val ScalaVersion = "2.11.2" val ScalaVersion = "2.10.3"
val ScalatraVersion = "2.3.0" val ScalatraVersion = "2.2.1"
lazy val project = Project ( lazy val project = Project (
"gitbucket", "gitbucket",
file(".") file("."),
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
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.0.0.201306101825-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.5",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
"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",
"com.typesafe.slick" %% "slick" % "1.0.1",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.173",
"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", "6", "-source", "6"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
packageOptions += Package.MainClass("JettyLauncher")
).enablePlugins(SbtTwirl)
} }

View File

@@ -1,9 +1,9 @@
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0") addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5") addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2") addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")

BIN
sbt-launch-0.12.3.jar Normal file

Binary file not shown.

Binary file not shown.

View File

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

3
sbt.sh
View File

@@ -1,2 +1 @@
#!/bin/sh java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.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 `dirname $0`/sbt-launch-0.13.5.jar "$@"

View File

@@ -1,8 +1,10 @@
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.File; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.security.ProtectionDomain; import java.security.ProtectionDomain;
@@ -42,14 +44,6 @@ 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();
@@ -65,27 +59,4 @@ public class JettyLauncher {
server.start(); server.start();
server.join(); server.join();
} }
private static File getGitBucketHome(){
String home = System.getProperty("gitbucket.home");
if(home != null && home.length() > 0){
return new File(home);
}
home = System.getenv("GITBUCKET_HOME");
if(home != null && home.length() > 0){
return new File(home);
}
return new File(System.getProperty("user.home"), ".gitbucket");
}
private static void deleteDirectory(File dir){
for(File file: dir.listFiles()){
if(file.isFile()){
file.delete();
} else if(file.isDirectory()){
deleteDirectory(file);
}
}
dir.delete();
}
} }

View File

@@ -1,135 +1,135 @@
CREATE TABLE ACCOUNT( CREATE TABLE ACCOUNT(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
MAIL_ADDRESS VARCHAR(100) NOT NULL, MAIL_ADDRESS VARCHAR(100) NOT NULL,
PASSWORD VARCHAR(40) NOT NULL, PASSWORD VARCHAR(40) NOT NULL,
ADMINISTRATOR BOOLEAN NOT NULL, ADMINISTRATOR BOOLEAN NOT NULL,
URL VARCHAR(200), URL VARCHAR(200),
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP NOT NULL,
LAST_LOGIN_DATE TIMESTAMP LAST_LOGIN_DATE TIMESTAMP
); );
CREATE TABLE REPOSITORY( CREATE TABLE REPOSITORY(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
PRIVATE BOOLEAN NOT NULL, PRIVATE BOOLEAN NOT NULL,
DESCRIPTION TEXT, DESCRIPTION TEXT,
DEFAULT_BRANCH VARCHAR(100), DEFAULT_BRANCH VARCHAR(100),
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP NOT NULL,
LAST_ACTIVITY_DATE TIMESTAMP NOT NULL LAST_ACTIVITY_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE COLLABORATOR( CREATE TABLE COLLABORATOR(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
COLLABORATOR_NAME VARCHAR(100) NOT NULL COLLABORATOR_NAME VARCHAR(100) NOT NULL
); );
CREATE TABLE ISSUE( CREATE TABLE ISSUE(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
OPENED_USER_NAME VARCHAR(100) NOT NULL, OPENED_USER_NAME VARCHAR(100) NOT NULL,
MILESTONE_ID INT, MILESTONE_ID INT,
ASSIGNED_USER_NAME VARCHAR(100), ASSIGNED_USER_NAME VARCHAR(100),
TITLE TEXT NOT NULL, TITLE TEXT NOT NULL,
CONTENT TEXT, CONTENT TEXT,
CLOSED BOOLEAN NOT NULL, CLOSED BOOLEAN NOT NULL,
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL UPDATED_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE ISSUE_ID( CREATE TABLE ISSUE_ID(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL ISSUE_ID INT NOT NULL
); );
CREATE TABLE ISSUE_COMMENT( CREATE TABLE ISSUE_COMMENT(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
COMMENT_ID INT AUTO_INCREMENT, COMMENT_ID INT AUTO_INCREMENT,
ACTION VARCHAR(10), ACTION VARCHAR(10),
COMMENTED_USER_NAME VARCHAR(100) NOT NULL, COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
CONTENT TEXT NOT NULL, CONTENT TEXT NOT NULL,
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL UPDATED_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE LABEL( CREATE TABLE LABEL(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
LABEL_ID INT AUTO_INCREMENT, LABEL_ID INT AUTO_INCREMENT,
LABEL_NAME VARCHAR(100) NOT NULL, LABEL_NAME VARCHAR(100) NOT NULL,
COLOR CHAR(6) NOT NULL COLOR CHAR(6) NOT NULL
); );
CREATE TABLE ISSUE_LABEL( CREATE TABLE ISSUE_LABEL(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
LABEL_ID INT NOT NULL LABEL_ID INT NOT NULL
); );
CREATE TABLE MILESTONE( CREATE TABLE MILESTONE(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
MILESTONE_ID INT AUTO_INCREMENT, MILESTONE_ID INT AUTO_INCREMENT,
TITLE VARCHAR(100) NOT NULL, TITLE VARCHAR(100) NOT NULL,
DESCRIPTION TEXT, DESCRIPTION TEXT,
DUE_DATE TIMESTAMP, DUE_DATE TIMESTAMP,
CLOSED_DATE TIMESTAMP CLOSED_DATE TIMESTAMP
); );
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME); ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME);
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS); ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS);
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME); ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME); ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID);
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID); ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID);
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID); ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID);
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID); ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID); ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID);
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
INSERT INTO ACCOUNT ( INSERT INTO ACCOUNT (
USER_NAME, USER_NAME,
MAIL_ADDRESS, MAIL_ADDRESS,
PASSWORD, PASSWORD,
ADMINISTRATOR, ADMINISTRATOR,
URL, URL,
REGISTERED_DATE, REGISTERED_DATE,
UPDATED_DATE, UPDATED_DATE,
LAST_LOGIN_DATE LAST_LOGIN_DATE
) VALUES ( ) VALUES (
'root', 'root',
'root@localhost', 'root@localhost',
'dc76e9f0c0006e8f919e0c515c66dbba3982f785', 'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
true, true,
'https://github.com/takezoe/gitbucket', 'https://github.com/takezoe/gitbucket',
SYSDATE, SYSDATE,
SYSDATE, SYSDATE,
NULL NULL
); );

View File

@@ -1,11 +0,0 @@
ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE;
CREATE TABLE SSH_KEY (
USER_NAME VARCHAR(100) NOT NULL,
SSH_KEY_ID INT AUTO_INCREMENT,
TITLE VARCHAR(100) NOT NULL,
PUBLIC_KEY TEXT NOT NULL
);
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_PK PRIMARY KEY (USER_NAME, SSH_KEY_ID);
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);

View File

@@ -1 +0,0 @@
DROP TABLE COMMIT_LOG;

View File

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

View File

@@ -1,6 +1,6 @@
import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter} import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
import app._ import app._
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._ import org.scalatra._
import javax.servlet._ import javax.servlet._
import java.util.EnumSet import java.util.EnumSet
@@ -10,8 +10,6 @@ class ScalatraBootstrap extends LifeCycle {
// Register TransactionFilter and BasicAuthenticationFilter at first // Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter) context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter) context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
@@ -22,6 +20,7 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new DashboardController, "/*") context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*") context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*") context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*")
context.mount(new AccountController, "/*") context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*") context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*") context.mount(new WikiController, "/*")
@@ -30,6 +29,7 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new IssuesController, "/*") context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*") context.mount(new PullRequestsController, "/*")
context.mount(new RepositorySettingsController, "/*") context.mount(new RepositorySettingsController, "/*")
context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*")
// Create GITBUCKET_HOME directory if it does not exist // Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(_root_.util.Directory.GitBucketHome) val dir = new java.io.File(_root_.util.Directory.GitBucketHome)

View File

@@ -1,27 +1,17 @@
package app package app
import service._ import service._
import util._ import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import util.ControlUtil._
import util.Implicits._
import ssh.SshUtil
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.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import model.GroupMember
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
trait AccountControllerBase extends AccountManagementControllerBase { trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String]) url: Option[String], fileId: Option[String])
@@ -29,8 +19,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String, case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String], clearImage: Boolean) url: Option[String], fileId: Option[String], clearImage: Boolean)
case class SshKeyForm(title: String, publicKey: String)
val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "password" -> trim(label("Password" , text(required, maxlength(20)))),
@@ -49,45 +37,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"clearImage" -> trim(label("Clear image" , boolean())) "clearImage" -> trim(label("Clear image" , boolean()))
)(AccountEditForm.apply) )(AccountEditForm.apply)
val sshKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim(label("Key" , text(required, validPublicKey)))
)(SshKeyForm.apply)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean()))
)(EditGroupForm.apply)
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class ForkRepositoryForm(owner: String, name: String)
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkRepositoryForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/** /**
* Displays user information. * Displays user information.
*/ */
@@ -102,30 +51,18 @@ trait AccountControllerBase extends AccountManagementControllerBase {
getActivitiesByUser(userName, true)) getActivitiesByUser(userName, true))
// Members // Members
case "members" if(account.isGroupAccount) => { case "members" if(account.isGroupAccount) =>
val members = getGroupMembers(account.userName) _root_.account.html.members(account, getGroupMembers(account.userName))
_root_.account.html.members(account, members.map(_.userName),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
// Repositories // Repositories
case _ => { case _ =>
val members = getGroupMembers(account.userName)
_root_.account.html.repositories(account, _root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName), if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)), getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
} }
} getOrElse NotFound } getOrElse NotFound
} }
get("/:userName.atom") {
val userName = params("userName")
contentType = "application/atom+xml; type=feed"
helper.xml.feed(getActivitiesByUser(userName, true))
}
get("/:userName/_avatar"){ get("/:userName/_avatar"){
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image => getAccountByUserName(userName).flatMap(_.image).map { image =>
@@ -139,9 +76,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_edit")(oneselfOnly { get("/:userName/_edit")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
account.html.edit(x, flash.get("info"))
} getOrElse NotFound
}) })
post("/:userName/_edit", editForm)(oneselfOnly { form => post("/:userName/_edit", editForm)(oneselfOnly { form =>
@@ -181,254 +116,22 @@ trait AccountControllerBase extends AccountManagementControllerBase {
redirect("/") redirect("/")
}) })
get("/:userName/_ssh")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
account.html.ssh(x, getPublicKeys(x.userName))
} getOrElse NotFound
})
post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form =>
val userName = params("userName")
addPublicKey(userName, form.title, form.publicKey)
redirect(s"/${userName}/_ssh")
})
get("/:userName/_ssh/delete/:id")(oneselfOnly {
val userName = params("userName")
val sshKeyId = params("id").toInt
deletePublicKey(userName, sshKeyId)
redirect(s"/${userName}/_ssh")
})
get("/register"){ get("/register"){
if(context.settings.allowAccountRegistration){ if(loadSystemSettings().allowAccountRegistration){
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){
redirect("/") redirect("/")
} else { } else {
account.html.register() account.html.edit(None, None)
} }
} else NotFound } else NotFound
} }
post("/register", newForm){ form => post("/register", newForm){ form =>
if(context.settings.allowAccountRegistration){ if(loadSystemSettings().allowAccountRegistration){
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url) createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false) updateImage(form.userName, form.fileId, false)
redirect("/signin") redirect("/signin")
} else NotFound } else NotFound
} }
get("/groups/new")(usersOnly {
account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
})
post("/groups/new", newGroupForm)(usersOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect(s"/${form.groupName}")
})
get("/:groupName/_editgroup")(managersOnly {
defining(params("groupName")){ groupName =>
account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
get("/:groupName/_deletegroup")(managersOnly {
defining(params("groupName")){ groupName =>
// Remove from GROUP_MEMBER
updateGroupMembers(groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
}
}
redirect("/")
})
post("/:groupName/_editgroup", editGroupForm)(managersOnly { form =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, false)
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect(s"/${form.groupName}")
} getOrElse NotFound
}
})
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { member =>
addCollaborator(form.owner, form.name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
// redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
private def uniqueRepository: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
private def validPublicKey: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match {
case Some(_) => None
case None => Some("Key is invalid.")
}
}
} }

View File

@@ -9,9 +9,10 @@ import org.scalatra.json._
import org.json4s._ import org.json4s._
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 model._ import model.Account
import service.{SystemSettingsService, AccountService} import service.{SystemSettingsService, AccountService}
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest} import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
import org.scalatra.i18n._ import org.scalatra.i18n._
@@ -24,9 +25,8 @@ abstract class ControllerBase extends ScalatraFilter
implicit val jsonFormats = DefaultFormats implicit val jsonFormats = DefaultFormats
// TODO Scala 2.11 // Don't set content type via Accept header.
// // Don't set content type via Accept header. override def format(implicit request: HttpServletRequest) = ""
// override def format(implicit request: HttpServletRequest) = ""
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try { override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
val httpRequest = request.asInstanceOf[HttpServletRequest] val httpRequest = request.asInstanceOf[HttpServletRequest]
@@ -36,16 +36,18 @@ abstract class ControllerBase extends ScalatraFilter
if(path.startsWith("/console/")){ if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val baseUrl = this.baseUrl(httpRequest)
if(account == null){ if(account == null){
// Redirect to login form // Redirect to login form
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path)) // TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
} else if(account.isAdmin){ } else if(account.isAdmin){
// H2 Console (administrators only) // H2 Console (administrators only)
// TODO Should use the configured base url.
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
// Redirect to dashboard // Redirect to dashboard
httpResponse.sendRedirect(baseUrl + "/") // TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/")
} }
} else if(path.startsWith("/git/")){ } else if(path.startsWith("/git/")){
// Git repository // Git repository
@@ -66,7 +68,7 @@ abstract class ControllerBase extends ScalatraFilter
implicit def context: Context = { implicit def context: Context = {
contextCache.get match { contextCache.get match {
case null => { case null => {
val context = Context(loadSystemSettings(), LoginAccount, request) val context = Context(loadSystemSettings().baseUrl.getOrElse(servletContext.getContextPath), LoginAccount, request)
contextCache.set(context) contextCache.set(context)
context context
} }
@@ -126,25 +128,22 @@ abstract class ControllerBase extends ScalatraFilter
} }
} }
// TODO Scala 2.11 override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty, includeContextPath: Boolean = true, includeServletPath: Boolean = true)
includeContextPath: Boolean = true, includeServletPath: Boolean = true, (implicit request: HttpServletRequest, response: HttpServletResponse) =
absolutize: Boolean = true, withSessionId: Boolean = true)
(implicit request: HttpServletRequest, response: HttpServletResponse): String =
if (path.startsWith("http")) path if (path.startsWith("http")) path
else baseUrl + super.url(path, params, false, false, false) else baseUrl + url(path, params, false, false, false)
} }
/** /**
* Context object for the current request. * Context object for the current request.
*
* @param path the context path
*/ */
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
val path = settings.baseUrl.getOrElse(request.getContextPath) lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl = settings.baseUrl(request)
val host = new java.net.URL(baseUrl).getHost
/** /**
* Get object from cache. * Get object from cache.
@@ -166,7 +165,7 @@ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount:
/** /**
* Base trait for controllers which manages account information. * Base trait for controllers which manages account information.
*/ */
trait AccountManagementControllerBase extends ControllerBase { trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService => self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
@@ -177,9 +176,9 @@ trait AccountManagementControllerBase extends ControllerBase {
} }
} else { } else {
fileId.map { fileId => fileId.map { fileId =>
val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get) val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
FileUtils.moveFile( FileUtils.moveFile(
new java.io.File(getTemporaryDir(session.getId), fileId), getTemporaryFile(fileId),
new java.io.File(getUserUploadDir(userName), filename) new java.io.File(getUserUploadDir(userName), filename)
) )
updateAvatarImage(userName, Some(filename)) updateAvatarImage(userName, Some(filename))
@@ -199,3 +198,28 @@ trait AccountManagementControllerBase extends ControllerBase {
} }
} }
/**
* Base trait for controllers which needs file uploading feature.
*/
trait FileUploadControllerBase {
def generateFileId: String =
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
def TemporaryDir(implicit session: HttpSession): java.io.File =
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
new java.io.File(TemporaryDir, fileId)
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
// getTemporaryFile(fileId).delete()
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
session.getAndRemove[String](Keys.Session.Upload(fileId))
}

View File

@@ -0,0 +1,199 @@
package app
import util.Directory._
import util.ControlUtil._
import util._
import service._
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import org.scalatra.i18n.Messages
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator
/**
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator =>
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class ForkRepositoryForm(owner: String, name: String)
val newForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){
if(getRepository(form.owner, form.name, baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(repository.owner == loginUserName){
// redirect to the repository
redirect(s"/${repository.owner}/${repository.name}")
} else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) =>
// redirect to the repository
redirect(s"/${owner}/${name}")
} getOrElse {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id)
}
}
case Left(_) => ???
}
}
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
}
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
/**
* Duplicate check for the repository name.
*/
private def unique: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}

View File

@@ -1,110 +1,109 @@
package app package app
import service._ import service._
import util.{UsersAuthenticator, Keys} import util.{UsersAuthenticator, Keys}
import util.Implicits._ import util.Implicits._
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 UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly { get("/dashboard/issues/repos")(usersOnly {
searchIssues("all") searchIssues("all")
}) })
get("/dashboard/issues/assigned")(usersOnly { get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned") searchIssues("assigned")
}) })
get("/dashboard/issues/created_by")(usersOnly { get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by") searchIssues("created_by")
}) })
get("/dashboard/pulls")(usersOnly { get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by", None)
}) })
get("/dashboard/pulls/owned")(usersOnly { get("/dashboard/pulls/owned")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by", None)
}) })
get("/dashboard/pulls/public")(usersOnly { get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None) searchPullRequests("not_created_by", None)
}) })
get("/dashboard/pulls/for/:owner/:repository")(usersOnly { get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository"))) searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
}) })
private def searchIssues(filter: String) = { private def searchIssues(filter: String) = {
import IssuesService._ import IssuesService._
// condition // condition
val condition = session.putAndGet(Keys.Session.DashboardIssues, val condition = session.putAndGet(Keys.Session.DashboardIssues,
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
) )
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
//val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
//
dashboard.html.issues( dashboard.html.issues(
dashboard.html.issueslist( issues.html.listparts(
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
page, page,
countIssue(condition.copy(state = "open" ), false, userRepos: _*), countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
countIssue(condition.copy(state = "closed"), false, userRepos: _*), countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
condition), condition),
countIssue(condition.copy(assigned = None, author = None), false, userRepos: _*), countIssue(condition, Map.empty, false, repositories: _*),
countIssue(condition.copy(assigned = Some(userName), author = None), false, userRepos: _*), countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
countIssue(condition.copy(assigned = None, author = Some(userName)), false, userRepos: _*), countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
countIssueGroupByRepository(condition, false, userRepos: _*), countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
condition, condition,
filter) filter)
} }
private def searchPullRequests(filter: String, repository: Option[String]) = { private def searchPullRequests(filter: String, repository: Option[String]) = {
import IssuesService._ import IssuesService._
import PullRequestService._ import PullRequestService._
// condition // condition
val condition = session.putAndGet(Keys.Session.DashboardPulls, { val condition = session.putAndGet(Keys.Session.DashboardPulls, {
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
}.copy(repo = repository)) }.copy(repo = repository))
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val allRepos = getAllRepositories(userName) val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) val filterUser = Map(filter -> userName)
val filterUser = Map(filter -> userName) val page = IssueSearchCondition.page(request)
val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository(
val counts = countIssueGroupByRepository( IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
IssueSearchCondition().copy(state = condition.state), true, userRepos: _*)
dashboard.html.pulls(
dashboard.html.pulls( pulls.html.listparts(
dashboard.html.pullslist( searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), page,
page, countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
countIssue(condition.copy(state = "open" ), true, allRepos: _*), countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
countIssue(condition.copy(state = "closed"), true, allRepos: _*), condition,
condition, None,
None, false),
false), getPullRequestCountGroupByUser(condition.state == "closed", userName, None),
getAllPullRequestCountGroupByUser(condition.state == "closed", userName), getRepositoryNamesOfUser(userName).map { RepoName =>
userRepos.map { case (userName, repoName) => (userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0)) }.sortBy(_._3).reverse,
}.sortBy(_._3).reverse, condition,
condition, filter)
filter)
}
}
}
}

View File

@@ -1,44 +1,31 @@
package app package app
import util.{Keys, FileUtil} import _root_.util.{Keys, FileUtil}
import util.ControlUtil._ import util.ControlUtil._
import util.Directory._
import org.scalatra._ import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem} import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
/** /**
* Provides Ajax based file upload functionality. * Provides Ajax based file upload functionality.
* *
* This servlet saves uploaded file. * This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/ */
class FileUploadController extends ScalatraServlet with FileUploadSupport { class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){ post("/image"){
execute { (file, fileId) => fileParams.get("file") match {
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get) case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
session += Keys.Session.Upload(fileId) -> file.name FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
} session += Keys.Session.Upload(fileId) -> file.name
}
post("/image/:owner/:repository"){
execute { (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
}
}
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)
Ok(fileId) Ok(fileId)
} }
case _ => BadRequest case None => BadRequest
}
} }
} }

View File

@@ -1,106 +1,81 @@
package app package app
import util._ import util._
import util.Implicits._ import service._
import service._ import jp.sf.amateras.scalatra.forms._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
class IndexController extends IndexControllerBase with RepositoryService with ActivityService with AccountService with UsersAuthenticator
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
trait IndexControllerBase extends ControllerBase { self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
case class SignInForm(userName: String, password: String)
case class SignInForm(userName: String, password: String)
val form = mapping(
val form = mapping( "userName" -> trim(label("Username", text(required))),
"userName" -> trim(label("Username", text(required))), "password" -> trim(label("Password", text(required)))
"password" -> trim(label("Password", text(required))) )(SignInForm.apply)
)(SignInForm.apply)
get("/"){
get("/"){ val loginAccount = context.loginAccount
val loginAccount = context.loginAccount
if(loginAccount.isEmpty) { html.index(getRecentActivities(),
html.index(getRecentActivities(), getVisibleRepositories(loginAccount, baseUrl),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), loadSystemSettings(),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
) )
} else { }
val loginUserName = loginAccount.get.userName
val loginUserGroups = getGroupsByUserName(loginUserName) get("/signin"){
var visibleOwnerSet : Set[String] = Set(loginUserName) val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){
visibleOwnerSet ++= loginUserGroups flash += Keys.Flash.Redirect -> redirect.get
}
html.index(getRecentActivitiesByOwners(visibleOwnerSet), html.signin(loadSystemSettings())
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), }
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
) post("/signin", form){ form =>
} authenticate(loadSystemSettings(), form.userName, form.password) match {
} case Some(account) => signin(account)
case None => redirect("/signin")
get("/signin"){ }
val redirect = params.get("redirect") }
if(redirect.isDefined && redirect.get.startsWith("/")){
flash += Keys.Flash.Redirect -> redirect.get get("/signout"){
} session.invalidate
html.signin() redirect("/")
} }
post("/signin", form){ form => /**
authenticate(context.settings, form.userName, form.password) match { * Set account information into HttpSession and redirect.
case Some(account) => signin(account) */
case None => redirect("/signin") private def signin(account: model.Account) = {
} session.setAttribute(Keys.Session.LoginAccount, account)
} updateLastLoginDate(account.userName)
get("/signout"){ flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
session.invalidate if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/") redirect("/")
} } else {
redirect(redirectUrl)
get("/activities.atom"){ }
contentType = "application/atom+xml; type=feed" }.getOrElse {
helper.xml.feed(getRecentActivities()) redirect("/")
} }
}
/**
* Set account information into HttpSession and redirect. /**
*/ * JSON API for collaborator completion.
private def signin(account: model.Account) = { *
session.setAttribute(Keys.Session.LoginAccount, account) * TODO Move to other controller?
updateLastLoginDate(account.userName) */
get("/_user/proposals")(usersOnly {
if(LDAPUtil.isDummyMailAddress(account)) { contentType = formats("json")
redirect("/" + account.userName + "/_edit") org.json4s.jackson.Serialization.write(
} Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
)
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => })
if(redirectUrl.stripSuffix("/") == request.getContextPath){
redirect("/")
} else { }
redirect(redirectUrl)
}
}.getOrElse {
redirect("/")
}
}
/**
* JSON API for collaborator completion.
*/
get("/_user/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
)
})
/**
* JSON APU for checking user existence.
*/
post("/_user/existence")(usersOnly {
getAccountByUserName(params("userName")).isDefined
})
}

View File

@@ -1,412 +1,392 @@
package app package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import IssuesService._ import IssuesService._
import util._ import util._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue import model.Issue
import plugin.PluginSystem
class IssuesController extends IssuesControllerBase
class IssuesController extends IssuesControllerBase with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase {
trait IssuesControllerBase extends ControllerBase { self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
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])
val issueCreateForm = mapping( val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())), "content" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())), "assignedUserName" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())), "milestoneId" -> trim(optional(number())),
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply) )(IssueCreateForm.apply)
val issueTitleEditForm = mapping( val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))) "title" -> trim(label("Title", text(required))),
)(x => x) "content" -> trim(optional(text()))
val issueEditForm = mapping( )(IssueEditForm.apply)
"content" -> trim(optional(text()))
)(x => x) val commentForm = mapping(
"issueId" -> label("Issue Id", number()),
val commentForm = mapping( "content" -> trim(label("Comment", text(required)))
"issueId" -> label("Issue Id", number()), )(CommentForm.apply)
"content" -> trim(label("Comment", text(required)))
)(CommentForm.apply) val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()),
val issueStateForm = mapping( "content" -> trim(optional(text()))
"issueId" -> label("Issue Id", number()), )(IssueStateForm.apply)
"content" -> trim(optional(text()))
)(IssueStateForm.apply) get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _)
get("/:owner/:repository/issues")(referrersOnly { repository => })
searchIssues(repository)
}) get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _)
get("/:owner/:repository/issues/:id")(referrersOnly { repository => })
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
getIssue(owner, name, issueId) map { get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
issues.html.issue( searchIssues("created_by", _)
_, })
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt), get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
getMilestonesWithIssueCount(owner, name), getIssue(owner, name, issueId) map {
getLabels(owner, name), issues.html.issue(
hasWritePermission(owner, name, context.loginAccount), _,
repository) getComments(owner, name, issueId.toInt),
} getOrElse NotFound getIssueLabels(owner, name, issueId.toInt),
} (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
}) getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
get("/:owner/:repository/issues/new")(readableUsersOnly { repository => hasWritePermission(owner, name, context.loginAccount),
defining(repository.owner, repository.name){ case (owner, name) => repository)
issues.html.create( } getOrElse NotFound
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, }
getMilestones(owner, name), })
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
repository) defining(repository.owner, repository.name){ case (owner, name) =>
} issues.html.create(
}) (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestones(owner, name),
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => getLabels(owner, name),
defining(repository.owner, repository.name){ case (owner, name) => hasWritePermission(owner, name, context.loginAccount),
val writable = hasWritePermission(owner, name, context.loginAccount) repository)
val userName = context.loginAccount.get.userName }
})
// insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content, post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
if(writable) form.assignedUserName else None, defining(repository.owner, repository.name){ case (owner, name) =>
if(writable) form.milestoneId else None) val writable = hasWritePermission(owner, name, context.loginAccount)
val userName = context.loginAccount.get.userName
// insert labels
if(writable){ // insert issue
form.labelNames.map { value => val issueId = createIssue(owner, name, userName, form.title, form.content,
val labels = getLabels(owner, name) if(writable) form.assignedUserName else None,
value.split(",").foreach { labelName => if(writable) form.milestoneId else None)
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId) // insert labels
} if(writable){
} form.labelNames.map { value =>
} val labels = getLabels(owner, name)
} value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
// record activity registerIssueLabel(owner, name, issueId, label.labelId)
recordCreateIssueActivity(owner, name, userName, issueId, form.title) }
}
// extract references and create refer comment }
getIssue(owner, name, issueId.toString).foreach { issue => }
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
} // record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ // extract references and create refer comment
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") getIssue(owner, name, issueId.toString).foreach { issue =>
} createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
}
redirect(s"/${owner}/${name}/issues/${issueId}")
} // notifications
}) Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) => }
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => redirect(s"/${owner}/${name}/issues/${issueId}")
if(isEditable(owner, name, issue.openedUserName)){ }
// update issue })
updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
createReferComment(owner, name, issue.copy(title = title), title) defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") if(isEditable(owner, name, issue.openedUserName)){
} else Unauthorized // update issue
} getOrElse NotFound updateIssue(owner, name, issue.issueId, form.title, form.content)
} // extract references and create refer comment
}) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) => redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
defining(repository.owner, repository.name){ case (owner, name) => } else Unauthorized
getIssue(owner, name, params("id")).map { issue => } getOrElse NotFound
if(isEditable(owner, name, issue.openedUserName)){ }
// update issue })
updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
createReferComment(owner, name, issue, content.getOrElse("")) handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} else Unauthorized } getOrElse NotFound
} getOrElse NotFound })
}
}) post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => redirect(s"/${repository.owner}/${repository.name}/${
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
redirect(s"/${repository.owner}/${repository.name}/${ } getOrElse NotFound
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") })
} getOrElse NotFound
}) ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => getComment(owner, name, params("id")).map { comment =>
handleComment(form.issueId, form.content, repository)() map { case (issue, id) => if(isEditable(owner, name, comment.commentedUserName)){
redirect(s"/${repository.owner}/${repository.name}/${ updateComment(comment.commentId, form.content)
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} getOrElse NotFound } else Unauthorized
}) } getOrElse NotFound
}
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => })
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment => ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
if(isEditable(owner, name, comment.commentedUserName)){ defining(repository.owner, repository.name){ case (owner, name) =>
updateComment(comment.commentId, form.content) getComment(owner, name, params("id")).map { comment =>
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") if(isEditable(owner, name, comment.commentedUserName)){
} else Unauthorized Ok(deleteComment(comment.commentId))
} getOrElse NotFound } else Unauthorized
} } getOrElse NotFound
}) }
})
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
getComment(owner, name, params("id")).map { comment => getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(owner, name, comment.commentedUserName)){ if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
Ok(deleteComment(comment.commentId)) params.get("dataType") collect {
} else Unauthorized case t if t == "html" => issues.html.editissue(
} getOrElse NotFound x.title, x.content, x.issueId, x.userName, x.repositoryName)
} } getOrElse {
}) contentType = formats("json")
org.json4s.jackson.Serialization.write(
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => Map("title" -> x.title,
getIssue(repository.owner, repository.name, params("id")) map { x => "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ repository, false, true)
params.get("dataType") collect { ))
case t if t == "html" => issues.html.editissue( }
x.content, x.issueId, x.userName, x.repositoryName) } else Unauthorized
} getOrElse { } getOrElse NotFound
contentType = formats("json") })
org.json4s.jackson.Serialization.write(
Map("title" -> x.title, ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", getComment(repository.owner, repository.name, params("id")) map { x =>
repository, false, true) if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
)) params.get("dataType") collect {
} case t if t == "html" => issues.html.editcomment(
} else Unauthorized x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse NotFound } getOrElse {
}) contentType = formats("json")
org.json4s.jackson.Serialization.write(
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => Map("content" -> view.Markdown.toHtml(x.content,
getComment(repository.owner, repository.name, params("id")) map { x => repository, false, true)
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ ))
params.get("dataType") collect { }
case t if t == "html" => issues.html.editcomment( } else Unauthorized
x.content, x.commentId, x.userName, x.repositoryName) } getOrElse NotFound
} getOrElse { })
contentType = formats("json")
org.json4s.jackson.Serialization.write( ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
Map("content" -> view.Markdown.toHtml(x.content, defining(params("id").toInt){ issueId =>
repository, false, true) registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
)) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
} }
} else Unauthorized })
} getOrElse NotFound
}) ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId =>
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
defining(params("id").toInt){ issueId => issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) }
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) })
}
}) ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => Ok("updated")
defining(params("id").toInt){ issueId => })
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
} updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
}) milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) issues.milestones.html.progress(openCount + closeCount, closeCount, false)
Ok("updated") } getOrElse NotFound
}) } getOrElse Ok()
})
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
milestoneId("milestoneId").map { milestoneId => defining(params.get("value")){ action =>
getMilestonesWithIssueCount(repository.owner, repository.name) executeBatch(repository) {
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => handleComment(_, None, repository)( _ => action)
issues.milestones.html.progress(openCount + closeCount, closeCount) }
} getOrElse NotFound }
} getOrElse Ok() })
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => params("value").toIntOpt.map{ labelId =>
defining(params.get("value")){ action => executeBatch(repository) { issueId =>
action match { getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) } registerIssueLabel(repository.owner, repository.name, issueId, labelId)
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) } }
case _ => // TODO BadRequest }
} } getOrElse NotFound
} })
})
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => defining(assignedUserName("value")){ value =>
params("value").toIntOpt.map{ labelId => executeBatch(repository) {
executeBatch(repository) { issueId => updateAssignedUserName(repository.owner, repository.name, _, value)
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { }
registerIssueLabel(repository.owner, repository.name, issueId, labelId) }
} })
}
} getOrElse NotFound post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
}) defining(milestoneId("value")){ value =>
executeBatch(repository) {
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => updateMilestoneId(repository.owner, repository.name, _, value)
defining(assignedUserName("value")){ value => }
executeBatch(repository) { }
updateAssignedUserName(repository.owner, repository.name, _, value) })
}
} val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
}) val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
defining(milestoneId("value")){ value => hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value) private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
} params("checked").split(',') map(_.toInt) foreach execute
} redirect(s"/${repository.owner}/${repository.name}/issues")
}) }
get("/:owner/:repository/_attached/:file")(referrersOnly { repository => private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
(Directory.getAttachedDir(repository.owner, repository.name) match { StringUtil.extractIssueId(message).foreach { issueId =>
case dir if(dir.exists && dir.isDirectory) => if(getIssue(owner, repository, issueId).isDefined){
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
contentType = FileUtil.getMimeType(file.getName) fromIssue.issueId + ":" + fromIssue.title, "refer")
file }
} }
case _ => None }
}) getOrElse NotFound
}) /**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") */
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] =
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
defining(repository.owner, repository.name){ case (owner, name) =>
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { val userName = context.loginAccount.get.userName
params("checked").split(',') map(_.toInt) foreach execute
params("from") match { getIssue(owner, name, issueId.toString) map { issue =>
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") val (action, recordActivity) =
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls") getAction(issue)
} .collect {
} case "close" => true -> (Some("close") ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { case "reopen" => false -> (Some("reopen") ->
StringUtil.extractIssueId(message).foreach { issueId => Some(recordReopenIssueActivity _))
if(getIssue(owner, repository, issueId).isDefined){ }
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, .map { case (closed, t) =>
fromIssue.issueId + ":" + fromIssue.title, "refer") updateClosed(owner, name, issueId, closed)
} t
} }
} .getOrElse(None -> None)
/** val commentId = content
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
*/ .getOrElse ( action.get.capitalize -> action.get )
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) match {
(getAction: model.Issue => Option[String] = case (content, action) => createComment(owner, name, userName, issueId, content, action)
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { }
defining(repository.owner, repository.name){ case (owner, name) => // record activity
val userName = context.loginAccount.get.userName content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
getIssue(owner, name, issueId.toString) map { issue => (owner, name, userName, issueId, _)
val (action, recordActivity) = }
getAction(issue) recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
.collect {
case "close" if(!issue.closed) => true -> // extract references and create refer comment
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) content.map { content =>
case "reopen" if(issue.closed) => false -> createReferComment(owner, name, issue, content)
(Some("reopen") -> Some(recordReopenIssueActivity _)) }
}
.map { case (closed, t) => // notifications
updateClosed(owner, name, issueId, closed) Notifier() match {
t case f =>
} content foreach {
.getOrElse(None -> None) f.toNotify(repository, issueId, _){
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
val commentId = content if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) }
.getOrElse ( action.get.capitalize -> action.get ) }
match { action foreach {
case (content, action) => createComment(owner, name, userName, issueId, content, action) f.toNotify(repository, issueId, _){
} Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
// record comment activity if comment is entered }
content foreach { }
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _) issue -> commentId
} }
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) }
}
// extract references and create refer comment
content.map { content => private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
createReferComment(owner, name, issue, content) defining(repository.owner, repository.name){ case (owner, repoName) =>
} val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
// notifications val sessionKey = Keys.Session.Issues(owner, repoName)
Notifier() match {
case f => // retrieve search condition
content foreach { val condition = session.putAndGet(sessionKey,
f.toNotify(repository, issueId, _){ if(request.hasQueryString) IssueSearchCondition(request)
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") )
}
} issues.html.list(
action foreach { searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
f.toNotify(repository, issueId, _){ page,
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") (getCollaborators(owner, repoName) :+ owner).sorted,
} getMilestones(owner, repoName),
} getLabels(owner, repoName),
} countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
issue -> commentId 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,
private def searchIssues(repository: RepositoryService.RepositoryInfo) = { filter,
defining(repository.owner, repository.name){ case (owner, repoName) => repository,
val page = IssueSearchCondition.page(request) hasWritePermission(owner, repoName, context.loginAccount))
val sessionKey = Keys.Session.Issues(owner, repoName) }
}
// retrieve search condition
val condition = session.putAndGet(sessionKey, }
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(
"issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}
}

View File

@@ -2,67 +2,50 @@ package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.{ReferrerAuthenticator, CollaboratorsAuthenticator} import util.CollaboratorsAuthenticator
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 IssuesService with RepositoryService with AccountService with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait LabelsControllerBase extends ControllerBase { trait LabelsControllerBase extends ControllerBase {
self: LabelsService with IssuesService with RepositoryService self: LabelsService with RepositoryService with CollaboratorsAuthenticator =>
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
val labelForm = mapping( val newForm = mapping(
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), "newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"labelColor" -> trim(label("Color", text(required, color))) "newColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
get("/:owner/:repository/issues/labels")(referrersOnly { repository => val editForm = mapping(
issues.labels.html.list( "editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
getLabels(repository.owner, repository.name), "editColor" -> trim(label("Color", text(required, color)))
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()), )(LabelForm.apply)
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount)) post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect(s"/${repository.owner}/${repository.name}/issues")
}) })
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
issues.labels.html.edit(None, repository) issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
}) })
ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) => ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { repository =>
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
issues.labels.html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
issues.labels.html.edit(Some(label), repository) issues.labels.html.edit(Some(label), repository)
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(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.label( issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt) deleteLabel(repository.owner, repository.name, params("labelId").toInt)
Ok() issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
}) })
/** /**
@@ -70,7 +53,7 @@ trait LabelsControllerBase extends ControllerBase {
*/ */
private def labelName: Constraint = new Constraint(){ private def labelName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(value.contains(',')){ if(!value.matches("^[^,]+$")){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.") Some(s"${name} starts with invalid character.")
@@ -79,4 +62,4 @@ trait LabelsControllerBase extends ControllerBase {
} }
} }
} }

View File

@@ -13,6 +13,7 @@ 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.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
@@ -62,6 +63,10 @@ trait PullRequestsControllerBase extends ControllerBase {
searchPullRequests(None, repository) searchPullRequests(None, repository)
}) })
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
@@ -74,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq( pulls.html.pullreq(
issue, pullreq, issue, pullreq,
getComments(owner, name, issueId), getComments(owner, name, issueId),
getIssueLabels(owner, name, issueId), getIssueLabels(owner, name, issueId.toInt),
(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),
getLabels(owner, name), getLabels(owner, name),
@@ -95,18 +100,18 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.mergeguide( pulls.html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId), checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
pullreq, pullreq,
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
} }
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository => get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId => params("id").toIntOpt.map { issueId =>
val branchName = multiParams("splat").head val branchName = params("branchName")
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){ if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call() git.branchDelete().setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
} }
} }
@@ -119,7 +124,7 @@ trait PullRequestsControllerBase extends ControllerBase {
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
LockUtil.lock(s"${owner}/${name}"){ LockUtil.lock(s"${owner}/${name}/merge"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) => getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git => using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close. // mark issue as merged and close.
@@ -152,7 +157,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
mergeCommit.setAuthor(personIdent) mergeCommit.setAuthor(personIdent)
mergeCommit.setCommitter(personIdent) mergeCommit.setCommitter(personIdent)
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n\n" +
form.message) form.message)
// insertObject and got mergeCommit Object Id // insertObject and got mergeCommit Object Id
@@ -172,18 +177,12 @@ trait PullRequestsControllerBase extends ControllerBase {
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
// close issue by content of pull request commits.flatten.foreach { commit =>
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch if(!existsCommitId(owner, name, commit.id)){
if(pullreq.branch == defaultBranch){ insertCommitId(owner, name, commit.id)
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
} }
issue.content match {
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
case _ =>
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
} }
// call web hook // call web hook
getWebHookURLs(owner, name) match { getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>
@@ -196,7 +195,7 @@ trait PullRequestsControllerBase extends ControllerBase {
// notifications // notifications
Notifier().toNotify(repository, issueId, "merge"){ Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
} }
redirect(s"/${owner}/${name}/pull/${issueId}") redirect(s"/${owner}/${name}/pull/${issueId}")
@@ -209,7 +208,7 @@ trait PullRequestsControllerBase extends ControllerBase {
get("/:owner/:repository/compare")(referrersOnly { forkedRepository => get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => { case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository => getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
using( using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)), Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
@@ -246,7 +245,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
} }
}; };
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
) yield { ) yield {
using( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -255,7 +254,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit, val forkedId = getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originBranch, originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch) forkedRepository.owner, forkedRepository.name, forkedBranch)
@@ -298,7 +297,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
} }
}; };
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
) yield { ) yield {
using( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -351,7 +350,7 @@ trait PullRequestsControllerBase extends ControllerBase {
// notifications // notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}") Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
} }
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
@@ -362,7 +361,7 @@ trait PullRequestsControllerBase extends ControllerBase {
*/ */
private def checkConflict(userName: String, repositoryName: String, branch: String, private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}"){ LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}" val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}" val tmpRefName = s"refs/merge-check/${userName}/${branch}"
@@ -398,7 +397,7 @@ trait PullRequestsControllerBase extends ControllerBase {
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String, private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
issueId: Int): Boolean = { issueId: Int): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}") { LockUtil.lock(s"${userName}/${repositoryName}/merge") {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git => using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
// merge // merge
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
@@ -427,8 +426,24 @@ trait PullRequestsControllerBase extends ControllerBase {
(defaultOwner, value) (defaultOwner, value)
} }
/**
* Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list.
*/
private def getRepositoryNames(node: RepositoryTreeNode): List[String] =
node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
using( using(
Git.open(getRepositoryDir(userName, repositoryName)), Git.open(getRepositoryDir(userName, repositoryName)),
Git.open(getRepositoryDir(requestUserName, requestRepositoryName)) Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
@@ -439,16 +454,18 @@ trait PullRequestsControllerBase extends ControllerBase {
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit) new CommitInfo(revCommit)
}.toList.splitWith { (commit1, commit2) => }.toList.splitWith { (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
} }
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
(commits, diffs) (commits, diffs)
} }
}
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)
@@ -458,15 +475,14 @@ trait PullRequestsControllerBase extends ControllerBase {
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
) )
issues.html.list( pulls.html.list(
"pulls", searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
userName,
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted, countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
getMilestones(owner, repoName), countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
getLabels(owner, repoName), countIssue(condition, Map.empty, true, owner -> repoName),
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition, condition,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))

View File

@@ -1,274 +1,266 @@
package app package app
import service._ import service._
import util.Directory._ import util.Directory._
import util.Implicits._ import util.{UsersAuthenticator, OwnerAuthenticator}
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator} 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 import service.WebHookService.WebHookPayload
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
with RepositoryService with AccountService with WebHookService
class RepositorySettingsController extends RepositorySettingsControllerBase with OwnerAuthenticator with UsersAuthenticator
with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WebHookService
trait RepositorySettingsControllerBase extends ControllerBase { with OwnerAuthenticator with UsersAuthenticator =>
self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator => // for repository options
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
// for repository options
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) val optionsForm = mapping(
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
val optionsForm = mapping( "description" -> trim(label("Description" , optional(text()))),
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))), "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"description" -> trim(label("Description" , optional(text()))), "isPrivate" -> trim(label("Repository Type", boolean()))
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), )(OptionsForm.apply)
"isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply) // for collaborator addition
case class CollaboratorForm(userName: String)
// for collaborator addition
case class CollaboratorForm(userName: String) val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator)))
val collaboratorForm = mapping( )(CollaboratorForm.apply)
"userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply) // for web hook url addition
case class WebHookForm(url: String)
// for web hook url addition
case class WebHookForm(url: String) val webHookForm = mapping(
"url" -> trim(label("url", text(required, webHook)))
val webHookForm = mapping( )(WebHookForm.apply)
"url" -> trim(label("url", text(required, webHook)))
)(WebHookForm.apply) // for transfer ownership
case class TransferOwnerShipForm(newOwner: String)
// for transfer ownership
case class TransferOwnerShipForm(newOwner: String) val transferForm = mapping(
"newOwner" -> trim(label("New owner", text(required, transferUser)))
val transferForm = mapping( )(TransferOwnerShipForm.apply)
"newOwner" -> trim(label("New owner", text(required, transferUser)))
)(TransferOwnerShipForm.apply) /**
* Redirect to the Options page.
/** */
* Redirect to the Options page. get("/:owner/:repository/settings")(ownerOnly { repository =>
*/ redirect(s"/${repository.owner}/${repository.name}/settings/options")
get("/:owner/:repository/settings")(ownerOnly { repository => })
redirect(s"/${repository.owner}/${repository.name}/settings/options")
}) /**
* Display the Options page.
/** */
* Display the Options page. get("/:owner/:repository/settings/options")(ownerOnly {
*/ settings.html.options(_, flash.get("info"))
get("/:owner/:repository/settings/options")(ownerOnly { })
settings.html.options(_, flash.get("info"))
}) /**
* Save the repository options.
/** */
* Save the repository options. post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
*/ saveRepositoryOptions(
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => repository.owner,
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch repository.name,
saveRepositoryOptions( form.description,
repository.owner, form.defaultBranch,
repository.name, repository.repository.parentUserName.map { _ =>
form.description, repository.repository.isPrivate
defaultBranch, } getOrElse form.isPrivate
repository.repository.parentUserName.map { _ => )
repository.repository.isPrivate // Change repository name
} getOrElse form.isPrivate if(repository.name != form.repositoryName){
) // Update database
// Change repository name renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
if(repository.name != form.repositoryName){ // Move git repository
// Update database defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
// Move git repository }
defining(getRepositoryDir(repository.owner, repository.name)){ dir => // Move wiki repository
FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName)) defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
} FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
// Move wiki repository }
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => }
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) flash += "info" -> "Repository settings has been updated."
} redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
} })
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => /**
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch) * Display the Collaborators page.
} */
flash += "info" -> "Repository settings has been updated." get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") settings.html.collaborators(
}) getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
/** repository)
* Display the Collaborators page. })
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => /**
settings.html.collaborators( * Add the collaborator.
getCollaborators(repository.owner, repository.name), */
getAccountByUserName(repository.owner).get.isGroupAccount, post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
repository) if(!getAccountByUserName(repository.owner).get.isGroupAccount){
}) addCollaborator(repository.owner, repository.name, form.userName)
}
/** redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
* Add the collaborator. })
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => /**
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ * Add the collaborator.
addCollaborator(repository.owner, repository.name, form.userName) */
} get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") if(!getAccountByUserName(repository.owner).get.isGroupAccount){
}) removeCollaborator(repository.owner, repository.name, params("name"))
}
/** redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
* Add the collaborator. })
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => /**
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ * Display the web hook page.
removeCollaborator(repository.owner, repository.name, params("name")) */
} get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
}) })
/** /**
* Display the web hook page. * Add the web hook URL.
*/ */
get("/:owner/:repository/settings/hooks")(ownerOnly { repository => post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info")) addWebHookURL(repository.owner, repository.name, form.url)
}) redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Add the web hook URL. /**
*/ * Delete the web hook URL.
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => */
addWebHookURL(repository.owner, repository.name, form.url) get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") deleteWebHookURL(repository.owner, repository.name, params("url"))
}) redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Delete the web hook URL. /**
*/ * Send the test request to registered web hook URLs.
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => */
deleteWebHookURL(repository.owner, repository.name, params("url")) get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
}) import scala.collection.JavaConverters._
val commits = git.log
/** .add(git.getRepository.resolve(repository.repository.defaultBranch))
* Send the test request to registered web hook URLs. .setMaxCount(3)
*/ .call.iterator.asScala.map(new CommitInfo(_))
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => getWebHookURLs(repository.owner, repository.name) match {
import scala.collection.JavaConverters._ case webHookURLs if(webHookURLs.nonEmpty) =>
val commits = git.log for(ownerAccount <- getAccountByUserName(repository.owner)){
.add(git.getRepository.resolve(repository.repository.defaultBranch)) callWebHook(repository.owner, repository.name, webHookURLs,
.setMaxCount(3) WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
.call.iterator.asScala.map(new CommitInfo(_)) }
case _ =>
getAccountByUserName(repository.owner).foreach { ownerAccount => }
callWebHook(repository.owner, repository.name,
List(model.WebHook(repository.owner, repository.name, form.url)), flash += "info" -> "Test payload deployed!"
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount) }
) redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
} })
flash += "url" -> form.url
flash += "info" -> "Test payload deployed!" /**
} * Display the danger zone.
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") */
}) get("/:owner/:repository/settings/danger")(ownerOnly {
settings.html.danger(_)
/** })
* Display the danger zone.
*/ /**
get("/:owner/:repository/settings/danger")(ownerOnly { * Transfer repository ownership.
settings.html.danger(_) */
}) post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
// Change repository owner
/** if(repository.owner != form.newOwner){
* Transfer repository ownership. // Update database
*/ renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => // Move git repository
// Change repository owner defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
if(repository.owner != form.newOwner){ FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
LockUtil.lock(s"${repository.owner}/${repository.name}"){ }
// Update database // Move wiki repository
renameRepository(repository.owner, repository.name, form.newOwner, repository.name) defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
// Move git repository FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
defining(getRepositoryDir(repository.owner, repository.name)){ dir => }
FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) }
} redirect(s"/${form.newOwner}/${repository.name}")
// Move wiki repository })
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) /**
} * Delete the repository.
} */
} post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
redirect(s"/${form.newOwner}/${repository.name}") deleteRepository(repository.owner, repository.name)
})
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
/** FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
* Delete the repository. FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
*/
post("/:owner/:repository/settings/delete")(ownerOnly { repository => redirect(s"/${repository.owner}")
LockUtil.lock(s"${repository.owner}/${repository.name}"){ })
deleteRepository(repository.owner, repository.name)
/**
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name)) * Provides duplication check for web hook url.
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) */
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) private def webHook: Constraint = new Constraint(){
} override def validate(name: String, value: String, messages: Messages): Option[String] =
redirect(s"/${repository.owner}") getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
}) }
/** /**
* Provides duplication check for web hook url. * Provides Constraint to validate the collaborator name.
*/ */
private def webHook: Constraint = new Constraint(){ private def collaborator: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.") getAccountByUserName(value) match {
} case None => Some("User does not exist.")
case Some(x) if(x.isGroupAccount)
/** => Some("User does not exist.")
* Provides Constraint to validate the collaborator name. case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
*/ => Some("User can access this repository already.")
private def collaborator: Constraint = new Constraint(){ case _ => None
override def validate(name: String, value: String, messages: Messages): Option[String] = }
getAccountByUserName(value) match { }
case None => Some("User does not exist.")
case Some(x) if(x.isGroupAccount) /**
=> Some("User does not exist.") * Duplicate check for the rename repository name.
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) */
=> Some("User can access this repository already.") private def renameRepositoryName: Constraint = new Constraint(){
case _ => None override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
} params.get("repository").filter(_ != value).flatMap { _ =>
} params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
/** }
* Duplicate check for the rename repository name. }
*/ }
private def renameRepositoryName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = /**
params.get("repository").filter(_ != value).flatMap { _ => * Provides Constraint to validate the repository transfer user.
params.get("owner").flatMap { userName => */
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") private def transferUser: Constraint = new Constraint(){
} override def validate(name: String, value: String, messages: Messages): Option[String] =
} getAccountByUserName(value) match {
} case None => Some("User does not exist.")
case Some(x) => if(x.userName == params("owner")){
/** Some("This is current repository owner.")
* Provides Constraint to validate the repository transfer user. } else {
*/ params.get("repository").flatMap { repositoryName =>
private def transferUser: Constraint = new Constraint(){ getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
override def validate(name: String, value: String, messages: Messages): Option[String] = }
getAccountByUserName(value) match { }
case None => Some("User does not exist.") }
case Some(x) => if(x.userName == params("owner")){ }
Some("This is current repository owner.")
} else {
params.get("repository").flatMap { repositoryName =>
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
}
}
}
}
} }

View File

@@ -1,74 +1,27 @@
package app package app
import _root_.util.JGitUtil.CommitInfo
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import _root_.util.ControlUtil._ import util.ControlUtil._
import _root_.util._ import _root_.util._
import service._ import service._
import org.scalatra._ import org.scalatra._
import java.io.File import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import jp.sf.amateras.scalatra.forms._ import java.util.zip.{ZipEntry, ZipOutputStream}
import org.eclipse.jgit.dircache.DirCache import scala.Some
import org.eclipse.jgit.revwalk.RevCommit
import service.WebHookService.WebHookPayload
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService 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 ReferrerAuthenticator with CollaboratorsAuthenticator =>
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
case class EditorForm(
branch: String,
path: String,
content: String,
message: Option[String],
charset: String,
lineSeparator: String,
newFileName: String,
oldFileName: Option[String]
)
case class DeleteForm(
branch: String,
path: String,
message: Option[String],
fileName: String
)
val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))),
"lineSeparator" -> trim(label("Line Separator", text(required))),
"newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
)(EditorForm.apply)
val deleteForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"message" -> trim(label("Message", optional(text()))),
"fileName" -> trim(label("Filename", text(required)))
)(DeleteForm.apply)
/** /**
* Returns converted HTML from Markdown for preview. * Returns converted HTML from Markdown for preview.
@@ -111,77 +64,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case Right((logs, hasNext)) => case Right((logs, hasNext)) =>
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.time) == view.helpers.date(commit2.time)
}, page, hasNext) }, page, hasNext)
case Left(_) => NotFound case Left(_) => NotFound
} }
} }
}) })
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
})
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), None,
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
form.message.getOrElse(s"Create ${form.newFileName}"))
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
}")
})
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName,
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
if(form.oldFileName.exists(_ == form.newFileName)){
form.message.getOrElse(s"Update ${form.newFileName}")
} else {
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
})
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
}")
})
post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
form.message.getOrElse(s"Delete ${form.fileName}"))
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
})
/** /**
* Displays the file content of the specified branch or commit. * Displays the file content of the specified branch or commit.
*/ */
@@ -191,19 +80,46 @@ 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 => @scala.annotation.tailrec
if(raw){ def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match {
// Download case true if(walk.getPathString == path) => walk.getObjectId(0)
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => case true => getPathObjectId(path, walk)
contentType = FileUtil.getContentType(path, bytes) }
bytes
val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
}
if(raw){
// Download
defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContent(git, objectId, false) else None
val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else {
// binary
JGitUtil.ContentInfo("binary", None)
} }
} else { } else {
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), // image or large
new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount)) JGitUtil.ContentInfo(viewer, None)
} }
} getOrElse NotFound
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
}
} }
}) })
@@ -242,12 +158,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/** /**
* Deletes branch. * Deletes branch.
*/ */
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository =>
val branchName = multiParams("splat").head val branchName = params("branchName")
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){ if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call() git.branchDelete().setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
} }
} }
@@ -264,13 +180,51 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/** /**
* Download repository contents as an archive. * Download repository contents as an archive.
*/ */
get("/:owner/:repository/archive/*")(referrersOnly { repository => get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
multiParams("splat").head match { val name = params("name")
case name if name.endsWith(".zip") =>
archiveRepository(name, ".zip", repository) if(name.endsWith(".zip")){
case name if name.endsWith(".tar.gz") => val revision = name.replaceFirst("\\.zip$", "")
archiveRepository(name, ".tar.gz", repository) val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
case _ => BadRequest if(workDir.exists){
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
val zipFile = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new TreeWalk(git.getRepository)){ walk =>
val reader = walk.getObjectReader
val objectId = new MutableObjectId
using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out =>
walk.addTree(revCommit.getTree)
walk.setRecursive(true)
while(walk.next){
val name = walk.getPathString
val mode = walk.getFileMode(0)
if(mode != FileMode.TREE){
walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name)
val loader = reader.open(objectId)
entry.setSize(loader.getSize)
out.putNextEntry(entry)
loader.copyTo(out)
}
}
}
}
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
zipFile
} else {
BadRequest
} }
}) })
@@ -279,29 +233,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getRepository( getRepository(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name), repository.repository.originRepositoryName.getOrElse(repository.name),
context.baseUrl), baseUrl),
getForkedRepositories( getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)), repository.repository.originRepositoryName.getOrElse(repository.name)),
repository) repository)
}) })
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = { private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst { val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst { } orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} getOrElse path.split("/")(0) } orElse Some(path.split("/")(0)) get
(id, path.substring(id.length).stripPrefix("/")) (id, path.substring(id.length).replaceFirst("^/", ""))
} }
private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme") private val readmeFiles = Seq("readme.md", "readme.markdown")
/** /**
* Provides HTML of the file list. * Provides HTML of the file list.
* *
* @param repository the repository information * @param repository the repository information
* @param revstr the branch name or commit id(optional) * @param revstr the branch name or commit id(optional)
* @param path the directory path (optional) * @param path the directory path (optional)
@@ -309,135 +263,30 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/ */
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = { private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
if(repository.commitCount == 0){ if(repository.commitCount == 0){
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) repo.html.guide(repository)
} 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
// process README.md or README.markdown // process README.md or README.markdown
val readme = files.find { file => val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase) readmeFiles.contains(file.name.toLowerCase)
}.map { file => }.map { file =>
val path = (file.name :: parentPath.reverse).reverse file -> StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
} }
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(lastModifiedCommit), // last modified commit new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount)) files, readme)
} }
} getOrElse NotFound } getOrElse NotFound
} }
} }
} }
private def commitFile(repository: service.RepositoryService.RepositoryInfo,
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
content: String, charset: String, message: String) = {
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
LockUtil.lock(s"${repository.owner}/${repository.name}"){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val loginAccount = context.loginAccount.get
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName)
JGitUtil.processTree(git, headTip){ (path, tree) =>
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
}
builder.finish()
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
headName, loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.release()
// update refs
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
// close issue by commit message
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call web hook
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
getWebHookURLs(repository.owner, repository.name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount))
}
case _ =>
}
}
}
}
private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
@scala.annotation.tailrec
def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => _getPathObjectId(path, walk)
case false => None
}
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
_getPathObjectId(path, treeWalk)
}
}
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
val revision = name.stripSuffix(suffix)
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists) {
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
val file = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new java.io.FileOutputStream(file)) { out =>
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(out)
.call()
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
file
}
}
} }

View File

@@ -2,7 +2,6 @@ package app
import util._ import util._
import ControlUtil._ import ControlUtil._
import Implicits._
import service._ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._

View File

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

View File

@@ -4,11 +4,9 @@ import service._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import util.Directory._
import util.Implicits._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.Directory._
class UserManagementController extends UserManagementControllerBase class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator
@@ -25,10 +23,10 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String) memberNames: Option[String])
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String, clearImage: Boolean, isRemoved: Boolean) memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping( val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
@@ -53,28 +51,28 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"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()))),
"members" -> trim(label("Members" ,text(required, members))) "memberNames" -> trim(label("Member Names" ,optional(text())))
)(NewGroupForm.apply) )(NewGroupForm.apply)
val editGroupForm = mapping( val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"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()))),
"members" -> trim(label("Members" ,text(required, members))), "memberNames" -> trim(label("Member Names" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())), "clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean())) "removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply) )(EditGroupForm.apply)
get("/admin/users")(adminOnly { get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved) val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName)
}.toMap
admin.users.html.list(users, members, includeRemoved) admin.users.html.list(users, members, includeRemoved)
}) })
@@ -129,11 +127,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url) createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map { updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false) updateImage(form.groupName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
@@ -145,11 +139,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}) })
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.members.split(",").map { defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account => getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved) updateGroup(groupName, form.url, form.isRemoved)
@@ -165,11 +155,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
} else { } else {
// Update GROUP_MEMBER // Update GROUP_MEMBER
updateGroupMembers(form.groupName, members) updateGroupMembers(form.groupName, memberNames)
// Update COLLABORATOR for group repositories // Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName) removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) => memberNames.foreach { userName =>
addCollaborator(form.groupName, repositoryName, userName) addCollaborator(form.groupName, repositoryName, userName)
} }
} }
@@ -182,12 +172,8 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
}) })
private def members: Constraint = new Constraint(){ post("/admin/users/_usercheck")(adminOnly {
override def validate(name: String, value: String, messages: Messages): Option[String] = { getAccountByUserName(params("userName")).isDefined
if(value.split(",").exists { })
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
} }

View File

@@ -4,10 +4,10 @@ import service._
import util._ import util._
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import scala.Some
import java.util.ResourceBundle import java.util.ResourceBundle
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase
@@ -36,8 +36,7 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki")(referrersOnly { repository => get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page => getWikiPage(repository.owner, repository.name, "Home").map { page =>
wiki.html.page("Home", page, getWikiPageList(repository.owner, repository.name), wiki.html.page("Home", page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
}) })
@@ -45,8 +44,7 @@ trait WikiControllerBase extends ControllerBase {
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
getWikiPage(repository.owner, repository.name, pageName).map { page => getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, getWikiPageList(repository.owner, repository.name), wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
}) })

View File

@@ -1,39 +1,34 @@
package model package model
trait AccountComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
import self._ object Accounts extends Table[Account]("ACCOUNT") {
def userName = column[String]("USER_NAME", O PrimaryKey)
lazy val Accounts = TableQuery[Accounts] def fullName = column[String]("FULL_NAME")
def mailAddress = column[String]("MAIL_ADDRESS")
class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") { def password = column[String]("PASSWORD")
val userName = column[String]("USER_NAME", O PrimaryKey) def isAdmin = column[Boolean]("ADMINISTRATOR")
val fullName = column[String]("FULL_NAME") def url = column[String]("URL")
val mailAddress = column[String]("MAIL_ADDRESS") def registeredDate = column[java.util.Date]("REGISTERED_DATE")
val password = column[String]("PASSWORD") def updatedDate = column[java.util.Date]("UPDATED_DATE")
val isAdmin = column[Boolean]("ADMINISTRATOR") def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
val url = column[String]("URL") def image = column[String]("IMAGE")
val registeredDate = column[java.util.Date]("REGISTERED_DATE") def groupAccount = column[Boolean]("GROUP_ACCOUNT")
val updatedDate = column[java.util.Date]("UPDATED_DATE") def removed = column[Boolean]("REMOVED")
val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount ~ removed <> (Account, Account.unapply _)
val image = column[String]("IMAGE") }
val groupAccount = column[Boolean]("GROUP_ACCOUNT")
val removed = column[Boolean]("REMOVED") case class Account(
def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply) userName: String,
} fullName: String,
} mailAddress: String,
password: String,
case class Account( isAdmin: Boolean,
userName: String, url: Option[String],
fullName: String, registeredDate: java.util.Date,
mailAddress: String, updatedDate: java.util.Date,
password: String, lastLoginDate: Option[java.util.Date],
isAdmin: Boolean, image: Option[String],
url: Option[String], isGroupAccount: Boolean,
registeredDate: java.util.Date, isRemoved: Boolean
updatedDate: java.util.Date, )
lastLoginDate: Option[java.util.Date],
image: Option[String],
isGroupAccount: Boolean,
isRemoved: Boolean
)

View File

@@ -1,29 +1,31 @@
package model package model
trait ActivityComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
import self._
lazy val Activities = TableQuery[Activities] object Activities extends Table[Activity]("ACTIVITY") with BasicTemplate {
def activityId = column[Int]("ACTIVITY_ID", O AutoInc)
def activityUserName = column[String]("ACTIVITY_USER_NAME")
def activityType = column[String]("ACTIVITY_TYPE")
def message = column[String]("MESSAGE")
def additionalInfo = column[String]("ADDITIONAL_INFO")
def activityDate = column[java.util.Date]("ACTIVITY_DATE")
def * = activityId ~ userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate <> (Activity, Activity.unapply _)
def autoInc = userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate returning activityId
}
class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate { object CommitLog extends Table[(String, String, String)]("COMMIT_LOG") with BasicTemplate {
val activityId = column[Int]("ACTIVITY_ID", O AutoInc) def commitId = column[String]("COMMIT_ID")
val activityUserName = column[String]("ACTIVITY_USER_NAME") def * = userName ~ repositoryName ~ commitId
val activityType = column[String]("ACTIVITY_TYPE") def byPrimaryKey(userName: String, repositoryName: String, commitId: String) = byRepository(userName, repositoryName) && (this.commitId is commitId.bind)
val message = column[String]("MESSAGE")
val additionalInfo = column[String]("ADDITIONAL_INFO")
val activityDate = column[java.util.Date]("ACTIVITY_DATE")
def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
}
} }
case class Activity( case class Activity(
activityId: Int,
userName: String, userName: String,
repositoryName: String, repositoryName: String,
activityUserName: String, activityUserName: String,
activityType: String, activityType: String,
message: String, message: String,
additionalInfo: Option[String], additionalInfo: Option[String],
activityDate: java.util.Date, activityDate: java.util.Date
activityId: Int = 0
) )

View File

@@ -1,47 +1,44 @@
package model package model
protected[model] trait TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
protected[model] trait BasicTemplate { self: Table[_] =>
trait BasicTemplate { self: Table[_] => def userName = column[String]("USER_NAME")
val userName = column[String]("USER_NAME") def repositoryName = column[String]("REPOSITORY_NAME")
val repositoryName = column[String]("REPOSITORY_NAME")
def byRepository(owner: String, repository: String) =
def byRepository(owner: String, repository: String) = (userName is owner.bind) && (repositoryName is repository.bind)
(userName === owner.bind) && (repositoryName === repository.bind)
def byRepository(userName: Column[String], repositoryName: Column[String]) =
def byRepository(userName: Column[String], repositoryName: Column[String]) = (this.userName is userName) && (this.repositoryName is repositoryName)
(this.userName === userName) && (this.repositoryName === repositoryName) }
}
protected[model] trait IssueTemplate extends BasicTemplate { self: Table[_] =>
trait IssueTemplate extends BasicTemplate { self: Table[_] => def issueId = column[Int]("ISSUE_ID")
val issueId = column[Int]("ISSUE_ID")
def byIssue(owner: String, repository: String, issueId: Int) =
def byIssue(owner: String, repository: String, issueId: Int) = byRepository(owner, repository) && (this.issueId is issueId.bind)
byRepository(owner, repository) && (this.issueId === issueId.bind)
def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byRepository(userName, repositoryName) && (this.issueId is issueId)
byRepository(userName, repositoryName) && (this.issueId === issueId) }
}
protected[model] trait LabelTemplate extends BasicTemplate { self: Table[_] =>
trait LabelTemplate extends BasicTemplate { self: Table[_] => def labelId = column[Int]("LABEL_ID")
val labelId = column[Int]("LABEL_ID")
def byLabel(owner: String, repository: String, labelId: Int) =
def byLabel(owner: String, repository: String, labelId: Int) = byRepository(owner, repository) && (this.labelId is labelId.bind)
byRepository(owner, repository) && (this.labelId === labelId.bind)
def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byRepository(userName, repositoryName) && (this.labelId is labelId)
byRepository(userName, repositoryName) && (this.labelId === labelId) }
}
protected[model] trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
trait MilestoneTemplate extends BasicTemplate { self: Table[_] => def milestoneId = column[Int]("MILESTONE_ID")
val milestoneId = column[Int]("MILESTONE_ID")
def byMilestone(owner: String, repository: String, milestoneId: Int) =
def byMilestone(owner: String, repository: String, milestoneId: Int) = byRepository(owner, repository) && (this.milestoneId is milestoneId.bind)
byRepository(owner, repository) && (this.milestoneId === milestoneId.bind)
def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byRepository(userName, repositoryName) && (this.milestoneId is milestoneId)
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId) }
}
}

View File

@@ -1,17 +1,13 @@
package model package model
trait CollaboratorComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
lazy val Collaborators = TableQuery[Collaborators] object Collaborators extends Table[Collaborator]("COLLABORATOR") with BasicTemplate {
def collaboratorName = column[String]("COLLABORATOR_NAME")
def * = userName ~ repositoryName ~ collaboratorName <> (Collaborator, Collaborator.unapply _)
class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate { def byPrimaryKey(owner: String, repository: String, collaborator: String) =
val collaboratorName = column[String]("COLLABORATOR_NAME") byRepository(owner, repository) && (collaboratorName is collaborator.bind)
def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply)
def byPrimaryKey(owner: String, repository: String, collaborator: String) =
byRepository(owner, repository) && (collaboratorName === collaborator.bind)
}
} }
case class Collaborator( case class Collaborator(

View File

@@ -1,20 +1,14 @@
package model package model
trait GroupMemberComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
lazy val GroupMembers = TableQuery[GroupMembers] object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
class GroupMembers(tag: Tag) extends Table[GroupMember](tag, "GROUP_MEMBER") { def userName = column[String]("USER_NAME", O PrimaryKey)
val groupName = column[String]("GROUP_NAME", O PrimaryKey) def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
val userName = column[String]("USER_NAME", O PrimaryKey)
val isManager = column[Boolean]("MANAGER")
def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply)
}
} }
case class GroupMember( case class GroupMember(
groupName: String, groupName: String,
userName: String, userName: String
isManager: Boolean )
)

View File

@@ -1,49 +1,41 @@
package model package model
trait IssueComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
import self._ object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTemplate {
def * = userName ~ repositoryName ~ issueId
lazy val IssueId = TableQuery[IssueId] def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
lazy val IssueOutline = TableQuery[IssueOutline] }
lazy val Issues = TableQuery[Issues]
object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate {
class IssueId(tag: Tag) extends Table[(String, String, Int)](tag, "ISSUE_ID") with IssueTemplate { def commentCount = column[Int]("COMMENT_COUNT")
def * = (userName, repositoryName, issueId) def * = userName ~ repositoryName ~ issueId ~ commentCount
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) }
}
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate { def openedUserName = column[String]("OPENED_USER_NAME")
val commentCount = column[Int]("COMMENT_COUNT") def assignedUserName = column[String]("ASSIGNED_USER_NAME")
def * = (userName, repositoryName, issueId, commentCount) def title = column[String]("TITLE")
} def content = column[String]("CONTENT")
def closed = column[Boolean]("CLOSED")
class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate { def registeredDate = column[java.util.Date]("REGISTERED_DATE")
val openedUserName = column[String]("OPENED_USER_NAME") def updatedDate = column[java.util.Date]("UPDATED_DATE")
val assignedUserName = column[String]("ASSIGNED_USER_NAME") def pullRequest = column[Boolean]("PULL_REQUEST")
val title = column[String]("TITLE") def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _)
val content = column[String]("CONTENT")
val closed = column[Boolean]("CLOSED") def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
val registeredDate = column[java.util.Date]("REGISTERED_DATE") }
val updatedDate = column[java.util.Date]("UPDATED_DATE")
val pullRequest = column[Boolean]("PULL_REQUEST") case class Issue(
def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply) userName: String,
repositoryName: String,
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) issueId: Int,
} openedUserName: String,
} milestoneId: Option[Int],
assignedUserName: Option[String],
case class Issue( title: String,
userName: String, content: Option[String],
repositoryName: String, closed: Boolean,
issueId: Int, registeredDate: java.util.Date,
openedUserName: String, updatedDate: java.util.Date,
milestoneId: Option[Int], isPullRequest: Boolean)
assignedUserName: Option[String],
title: String,
content: Option[String],
closed: Boolean,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
isPullRequest: Boolean
)

View File

@@ -1,34 +1,28 @@
package model package model
trait IssueCommentComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
import self._ object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate {
def commentId = column[Int]("COMMENT_ID", O AutoInc)
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){ def action = column[String]("ACTION")
def autoInc = this returning this.map(_.commentId) def commentedUserName = column[String]("COMMENTED_USER_NAME")
} def content = column[String]("CONTENT")
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate { def updatedDate = column[java.util.Date]("UPDATED_DATE")
val commentId = column[Int]("COMMENT_ID", O AutoInc) def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
val action = column[String]("ACTION")
val commentedUserName = column[String]("COMMENTED_USER_NAME") def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
val content = column[String]("CONTENT") def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
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) case class IssueComment(
userName: String,
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind repositoryName: String,
} issueId: Int,
} commentId: Int,
action: String,
case class IssueComment( commentedUserName: String,
userName: String, content: String,
repositoryName: String, registeredDate: java.util.Date,
issueId: Int, updatedDate: java.util.Date
commentId: Int = 0, )
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
)

View File

@@ -1,20 +1,15 @@
package model package model
trait IssueLabelComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
lazy val IssueLabels = TableQuery[IssueLabels] object IssueLabels extends Table[IssueLabel]("ISSUE_LABEL") with IssueTemplate with LabelTemplate {
def * = userName ~ repositoryName ~ issueId ~ labelId <> (IssueLabel, IssueLabel.unapply _)
class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate { def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply) byIssue(owner, repository, issueId) && (this.labelId is labelId.bind)
def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
byIssue(owner, repository, issueId) && (this.labelId === labelId.bind)
}
} }
case class IssueLabel( case class IssueLabel(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
issueId: Int, issueId: Int,
labelId: Int labelId: Int)
)

View File

@@ -1,25 +1,21 @@
package model package model
trait LabelComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
lazy val Labels = TableQuery[Labels] object Labels extends Table[Label]("LABEL") with LabelTemplate {
def labelName = column[String]("LABEL_NAME")
def color = column[String]("COLOR")
def * = userName ~ repositoryName ~ labelId ~ labelName ~ color <> (Label, Label.unapply _)
class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate { def ins = userName ~ repositoryName ~ labelName ~ color
override val labelId = column[Int]("LABEL_ID", O AutoInc) def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
val labelName = column[String]("LABEL_NAME") def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
val color = column[String]("COLOR")
def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply)
def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
}
} }
case class Label( case class Label(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
labelId: Int = 0, labelId: Int,
labelName: String, labelName: String,
color: String){ color: String){
@@ -31,7 +27,8 @@ case class Label(
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000" "000000"
} else { } else {
"ffffff" "FFFFFF"
} }
} }
}
}

View File

@@ -1,30 +1,24 @@
package model package model
trait MilestoneComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
import self._
lazy val Milestones = TableQuery[Milestones] object Milestones extends Table[Milestone]("MILESTONE") with MilestoneTemplate {
def title = column[String]("TITLE")
def description = column[String]("DESCRIPTION")
def dueDate = column[java.util.Date]("DUE_DATE")
def closedDate = column[java.util.Date]("CLOSED_DATE")
def * = userName ~ repositoryName ~ milestoneId ~ title ~ description.? ~ dueDate.? ~ closedDate.? <> (Milestone, Milestone.unapply _)
class Milestones(tag: Tag) extends Table[Milestone](tag, "MILESTONE") with MilestoneTemplate { def ins = userName ~ repositoryName ~ title ~ description.? ~ dueDate.? ~ closedDate.?
override val milestoneId = column[Int]("MILESTONE_ID", O AutoInc) def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
val title = column[String]("TITLE") def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId)
val description = column[String]("DESCRIPTION")
val dueDate = column[java.util.Date]("DUE_DATE")
val closedDate = column[java.util.Date]("CLOSED_DATE")
def * = (userName, repositoryName, milestoneId, title, description.?, dueDate.?, closedDate.?) <> (Milestone.tupled, Milestone.unapply)
def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId)
}
} }
case class Milestone( case class Milestone(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
milestoneId: Int = 0, milestoneId: Int,
title: String, title: String,
description: Option[String], description: Option[String],
dueDate: Option[java.util.Date], dueDate: Option[java.util.Date],
closedDate: Option[java.util.Date] closedDate: Option[java.util.Date])
)

View File

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

View File

@@ -1,42 +0,0 @@
package model
trait Profile {
val profile: slick.driver.JdbcProfile
import profile.simple._
// java.util.Date Mapped Column Types
implicit val dateColumnType = MappedColumnType.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime),
t => new java.util.Date(t.getTime)
)
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
}
object Profile extends {
val profile = slick.driver.H2Driver
} with AccountComponent
with ActivityComponent
with CollaboratorComponent
with GroupMemberComponent
with IssueComponent
with IssueCommentComponent
with IssueLabelComponent
with LabelComponent
with MilestoneComponent
with PullRequestComponent
with RepositoryComponent
with SshKeyComponent
with WebHookComponent
with PluginComponent with Profile {
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
}

View File

@@ -1,22 +1,18 @@
package model package model
trait PullRequestComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
lazy val PullRequests = TableQuery[PullRequests] object PullRequests extends Table[PullRequest]("PULL_REQUEST") with IssueTemplate {
def branch = column[String]("BRANCH")
def requestUserName = column[String]("REQUEST_USER_NAME")
def requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
def requestBranch = column[String]("REQUEST_BRANCH")
def commitIdFrom = column[String]("COMMIT_ID_FROM")
def commitIdTo = column[String]("COMMIT_ID_TO")
def * = userName ~ repositoryName ~ issueId ~ branch ~ requestUserName ~ requestRepositoryName ~ requestBranch ~ commitIdFrom ~ commitIdTo <> (PullRequest, PullRequest.unapply _)
class PullRequests(tag: Tag) extends Table[PullRequest](tag, "PULL_REQUEST") with IssueTemplate { def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
val branch = column[String]("BRANCH") def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
val requestUserName = column[String]("REQUEST_USER_NAME")
val requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
val requestBranch = column[String]("REQUEST_BRANCH")
val commitIdFrom = column[String]("COMMIT_ID_FROM")
val commitIdTo = column[String]("COMMIT_ID_TO")
def * = (userName, repositoryName, issueId, branch, requestUserName, requestRepositoryName, requestBranch, commitIdFrom, commitIdTo) <> (PullRequest.tupled, PullRequest.unapply)
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
}
} }
case class PullRequest( case class PullRequest(
@@ -29,4 +25,4 @@ case class PullRequest(
requestBranch: String, requestBranch: String,
commitIdFrom: String, commitIdFrom: String,
commitIdTo: String commitIdTo: String
) )

View File

@@ -1,26 +1,21 @@
package model package model
trait RepositoryComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
import self._
lazy val Repositories = TableQuery[Repositories] object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate {
def isPrivate = column[Boolean]("PRIVATE")
def description = column[String]("DESCRIPTION")
def defaultBranch = column[String]("DEFAULT_BRANCH")
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
def originUserName = column[String]("ORIGIN_USER_NAME")
def originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
def parentUserName = column[String]("PARENT_USER_NAME")
def parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate ~ originUserName.? ~ originRepositoryName.? ~ parentUserName.? ~ parentRepositoryName.? <> (Repository, Repository.unapply _)
class Repositories(tag: Tag) extends Table[Repository](tag, "REPOSITORY") with BasicTemplate { def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
val isPrivate = column[Boolean]("PRIVATE")
val description = column[String]("DESCRIPTION")
val defaultBranch = column[String]("DEFAULT_BRANCH")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
val lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
val originUserName = column[String]("ORIGIN_USER_NAME")
val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
val parentUserName = column[String]("PARENT_USER_NAME")
val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?) <> (Repository.tupled, Repository.unapply)
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
} }
case class Repository( case class Repository(

View File

@@ -1,24 +0,0 @@
package model
trait SshKeyComponent { self: Profile =>
import profile.simple._
lazy val SshKeys = TableQuery[SshKeys]
class SshKeys(tag: Tag) extends Table[SshKey](tag, "SSH_KEY") {
val userName = column[String]("USER_NAME")
val sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc)
val title = column[String]("TITLE")
val publicKey = column[String]("PUBLIC_KEY")
def * = (userName, sshKeyId, title, publicKey) <> (SshKey.tupled, SshKey.unapply)
def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName === userName.bind) && (this.sshKeyId === sshKeyId.bind)
}
}
case class SshKey(
userName: String,
sshKeyId: Int = 0,
title: String,
publicKey: String
)

View File

@@ -1,16 +1,12 @@
package model package model
trait WebHookComponent extends TemplateComponent { self: Profile => import scala.slick.driver.H2Driver.simple._
import profile.simple._
lazy val WebHooks = TableQuery[WebHooks] object WebHooks extends Table[WebHook]("WEB_HOOK") with BasicTemplate {
def url = column[String]("URL")
def * = userName ~ repositoryName ~ url <> (WebHook, WebHook.unapply _)
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind)
val url = column[String]("URL")
def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
}
} }
case class WebHook( case class WebHook(

View File

@@ -1,3 +1,20 @@
package object model { package object model {
type Session = slick.jdbc.JdbcBackend#Session import scala.slick.driver.BasicDriver.Implicit._
} import scala.slick.lifted.{Column, MappedTypeMapper}
// java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime),
t => new java.util.Date(t.getTime)
)
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
package service package service
import model.Profile._ import model._
import profile.simple._ import scala.slick.driver.H2Driver.simple._
import model.{Account, GroupMember} import Database.threadLocalSession
// TODO [Slick 2.0]NOT import directly?
import model.Profile.dateColumnType
import service.SystemSettingsService.SystemSettings import service.SystemSettingsService.SystemSettings
import util.StringUtil._ import util.StringUtil._
import model.GroupMember
import scala.Some
import model.Account
import util.LDAPUtil import util.LDAPUtil
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -14,7 +15,7 @@ trait AccountService {
private val logger = LoggerFactory.getLogger(classOf[AccountService]) private val logger = LoggerFactory.getLogger(classOf[AccountService])
def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] = def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
if(settings.ldapAuthentication){ if(settings.ldapAuthentication){
ldapAuthentication(settings, userName, password) ldapAuthentication(settings, userName, password)
} else { } else {
@@ -24,7 +25,7 @@ trait AccountService {
/** /**
* Authenticate by internal database. * Authenticate by internal database.
*/ */
private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = { private def defaultAuthentication(userName: String, password: String) = {
getAccountByUserName(userName).collect { getAccountByUserName(userName).collect {
case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account) case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
} getOrElse None } getOrElse None
@@ -33,39 +34,19 @@ trait AccountService {
/** /**
* Authenticate by LDAP. * Authenticate by LDAP.
*/ */
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
(implicit s: Session): Option[Account] = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match { LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(ldapUserInfo) => { case Right(ldapUserInfo) => {
// Create or update account by LDAP information // Create or update account by LDAP information
getAccountByUserName(ldapUserInfo.userName, true) match { getAccountByUserName(userName, true) match {
case Some(x) if(!x.isRemoved) => { case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
if(settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
} else {
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
}
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if(x.isRemoved) => { case Some(x) if(x.isRemoved) => {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.") logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..")
defaultAuthentication(userName, password) defaultAuthentication(userName, password)
} }
case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match { case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
case Some(x) if(!x.isRemoved) => {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if(x.isRemoved) => {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password)
}
case None => {
createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
getAccountByUserName(ldapUserInfo.userName)
}
}
} }
getAccountByUserName(userName)
} }
case Left(errorMessage) => { case Left(errorMessage) => {
logger.info(s"LDAP Authentication Failed: ${errorMessage}") logger.info(s"LDAP Authentication Failed: ${errorMessage}")
@@ -74,21 +55,20 @@ trait AccountService {
} }
} }
def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = def getAccountByUserName(userName: String, includeRemoved: Boolean = false): Option[Account] =
Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] = def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){ if(includeRemoved){
Accounts sortBy(_.userName) list Query(Accounts) sortBy(_.userName) list
} else { } else {
Accounts filter (_.removed === false.bind) sortBy(_.userName) list Query(Accounts) filter (_.removed is false.bind) sortBy(_.userName) list
} }
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]) def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
(implicit s: Session): Unit =
Accounts insert Account( Accounts insert Account(
userName = userName, userName = userName,
password = password, password = password,
@@ -103,14 +83,14 @@ trait AccountService {
isGroupAccount = false, isGroupAccount = false,
isRemoved = false) isRemoved = false)
def updateAccount(account: Account)(implicit s: Session): Unit = def updateAccount(account: Account): Unit =
Accounts Accounts
.filter { a => a.userName === account.userName.bind } .filter { a => a.userName is account.userName.bind }
.map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed) } .map { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? ~ a.removed }
.update ( .update (
account.password, account.password,
account.fullName, account.fullName,
account.mailAddress, account.mailAddress,
account.isAdmin, account.isAdmin,
account.url, account.url,
account.registeredDate, account.registeredDate,
@@ -118,13 +98,13 @@ trait AccountService {
account.lastLoginDate, account.lastLoginDate,
account.isRemoved) account.isRemoved)
def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit = def updateAvatarImage(userName: String, image: Option[String]): Unit =
Accounts.filter(_.userName === userName.bind).map(_.image.?).update(image) Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
def updateLastLoginDate(userName: String)(implicit s: Session): Unit = def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName === userName.bind).map(_.lastLoginDate).update(currentDate) Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit = def createGroup(groupName: String, url: Option[String]): Unit =
Accounts insert Account( Accounts insert Account(
userName = groupName, userName = groupName,
password = "", password = "",
@@ -139,35 +119,36 @@ trait AccountService {
isGroupAccount = true, isGroupAccount = true,
isRemoved = false) isRemoved = false)
def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit = def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
Accounts.filter(_.userName === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed) Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = { def updateGroupMembers(groupName: String, members: List[String]): Unit = {
GroupMembers.filter(_.groupName === groupName.bind).delete Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { case (userName, isManager) => members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName, isManager) GroupMembers insert GroupMember (groupName, userName)
} }
} }
def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] = def getGroupMembers(groupName: String): List[String] =
GroupMembers Query(GroupMembers)
.filter(_.groupName === groupName.bind) .filter(_.groupName is groupName.bind)
.sortBy(_.userName) .sortBy(_.userName)
.map(_.userName)
.list .list
def getGroupsByUserName(userName: String)(implicit s: Session): List[String] = def getGroupsByUserName(userName: String): List[String] =
GroupMembers Query(GroupMembers)
.filter(_.userName === userName.bind) .filter(_.userName is userName.bind)
.sortBy(_.groupName) .sortBy(_.groupName)
.map(_.groupName) .map(_.groupName)
.list .list
def removeUserRelatedData(userName: String)(implicit s: Session): Unit = { def removeUserRelatedData(userName: String): Unit = {
GroupMembers.filter(_.userName === userName.bind).delete Query(GroupMembers).filter(_.userName is userName.bind).delete
Collaborators.filter(_.collaboratorName === userName.bind).delete Query(Collaborators).filter(_.collaboratorName is userName.bind).delete
Repositories.filter(_.userName === userName.bind).delete Query(Repositories).filter(_.userName is userName.bind).delete
} }
} }
object AccountService extends AccountService object AccountService extends AccountService

View File

@@ -1,19 +1,19 @@
package service package service
import model.Profile._ import model._
import profile.simple._ import scala.slick.driver.H2Driver.simple._
import model.Activity import Database.threadLocalSession
trait ActivityService { trait ActivityService {
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] = def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] =
Activities Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => .filter { case (t1, t2) =>
if(isPublic){ if(isPublic){
(t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind) (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind)
} else { } else {
(t1.activityUserName === activityUserName.bind) (t1.activityUserName is activityUserName.bind)
} }
} }
.sortBy { case (t1, t2) => t1.activityId desc } .sortBy { case (t1, t2) => t1.activityId desc }
@@ -21,159 +21,151 @@ trait ActivityService {
.take(30) .take(30)
.list .list
def getRecentActivities()(implicit s: Session): List[Activity] = def getRecentActivities(): List[Activity] =
Activities Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => t2.isPrivate === false.bind } .filter { case (t1, t2) => t2.isPrivate is false.bind }
.sortBy { case (t1, t2) => t1.activityId desc } .sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 } .map { case (t1, t2) => t1 }
.take(30) .take(30)
.list .list
def getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] = def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit =
Activities Activities.autoInc insert(userName, repositoryName, activityUserName,
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) }
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String)
(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"create_repository", "create_repository",
s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"open_issue", "open_issue",
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"close_issue", "close_issue",
s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"close_issue", "close_issue",
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"reopen_issue", "reopen_issue",
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String) def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_issue", "comment_issue",
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)), Some(cut(comment, 200)),
currentDate) currentDate)
def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String) def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_issue", "comment_issue",
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)), Some(cut(comment, 200)),
currentDate) 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 = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"create_wiki", "create_wiki",
s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki", s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
Some(pageName), Some(pageName),
currentDate) currentDate)
def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"edit_wiki", "edit_wiki",
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
Some(pageName + ":" + commitId), Some(pageName + ":" + commitId),
currentDate) currentDate)
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String, def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
branchName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit = branchName: String, commits: List[util.JGitUtil.CommitInfo]) =
Activities insert Activity(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"push", "push",
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")), Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
currentDate) currentDate)
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String, def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit = tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
Activities insert Activity(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_tag", "create_tag",
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String, def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit = tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
Activities insert Activity(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"delete_tag", "delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"create_branch", "create_branch",
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"delete_branch", "delete_branch",
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String)(implicit s: Session): Unit = def recordForkActivity(userName: String, repositoryName: String, activityUserName: String) =
Activities insert Activity(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"fork", "fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]", s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"open_pullreq", "open_pullreq",
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String) def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit =
(implicit s: Session): Unit = Activities.autoInc insert(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"merge_pullreq", "merge_pullreq",
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(message), Some(message),
currentDate) currentDate)
private def cut(value: String, length: Int): String = def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
CommitLog insert (userName, repositoryName, commitId)
}
def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) =
CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*)
def getAllCommitIds(userName: String, repositoryName: String): List[String] =
Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list
def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined
private def cut(value: String, length: Int): String =
if(value.length > length) value.substring(0, length) + "..." else value if(value.length > length) value.substring(0, length) + "..." else value
} }

View File

@@ -1,398 +1,372 @@
package service package service
import scala.slick.jdbc.{StaticQuery => Q} import scala.slick.driver.H2Driver.simple._
import Q.interpolation import Database.threadLocalSession
import scala.slick.jdbc.{StaticQuery => Q}
import model.Profile._ import Q.interpolation
import profile.simple._
import model.{Issue, IssueComment, IssueLabel, Label} import model._
import util.Implicits._ import util.Implicits._
import util.StringUtil._ import util.StringUtil._
trait IssuesService { trait IssuesService {
import IssuesService._ import IssuesService._
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = def getIssue(owner: String, repository: String, issueId: String) =
if (issueId forall (_.isDigit)) if (issueId forall (_.isDigit))
Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption Query(Issues) filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
else None else None
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = def getComments(owner: String, repository: String, issueId: Int) =
IssueComments filter (_.byIssue(owner, repository, issueId)) list Query(IssueComments) filter (_.byIssue(owner, repository, issueId)) list
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) = def getComment(owner: String, repository: String, commentId: String) =
if (commentId forall (_.isDigit)) if (commentId forall (_.isDigit))
IssueComments filter { t => Query(IssueComments) filter { t =>
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
} firstOption } firstOption
else None else None
def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) = def getIssueLabels(owner: String, repository: String, issueId: Int) =
IssueLabels IssueLabels
.innerJoin(Labels).on { (t1, t2) => .innerJoin(Labels).on { (t1, t2) =>
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
} }
.filter ( _._1.byIssue(owner, repository, issueId) ) .filter ( _._1.byIssue(owner, repository, issueId) )
.map ( _._2 ) .map ( _._2 )
.list .list
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
/** /**
* 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 onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param repos Tuple of the repository owner and the repository name * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
* @return the count of the search result * @param repos Tuple of the repository owner and the repository name
*/ * @return the count of the search result
def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean, repos: (String, String)*) */
(implicit s: Session): Int = def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first repos: (String, String)*): Int =
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
/** /**
* Returns the Map which contains issue count for each labels. * Returns the Map which contains issue count for each labels.
* *
* @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
* @return the Map which contains issue count for each labels (key is label name, value is issue count) * @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)
def countIssueGroupByLabels(owner: String, repository: String, */
condition: IssueSearchCondition)(implicit s: Session): Map[String, Int] = { def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String]): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
.innerJoin(IssueLabels).on { (t1, t2) => searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) .innerJoin(IssueLabels).on { (t1, t2) =>
} t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
.innerJoin(Labels).on { case ((t1, t2), t3) => }
t2.byLabel(t3.userName, t3.repositoryName, t3.labelId) .innerJoin(Labels).on { case ((t1, t2), t3) =>
} t2.byLabel(t3.userName, t3.repositoryName, t3.labelId)
.groupBy { case ((t1, t2), t3) => }
t3.labelName .groupBy { case ((t1, t2), t3) =>
} t3.labelName
.map { case (labelName, t) => }
labelName -> t.length .map { case (labelName, t) =>
} labelName ~ t.length
.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. * 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 onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. * @param condition the search condition
* @param repos Tuple of the repository owner and the repository name * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @return list which contains issue count for each repository * @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
def countIssueGroupByRepository( * @return list which contains issue count for each repository
condition: IssueSearchCondition, onlyPullRequest: Boolean, */
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { def countIssueGroupByRepository(
searchIssueQuery(repos, condition.copy(repo = None), onlyPullRequest) condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
.groupBy { t => repos: (String, String)*): List[(String, String, Int)] = {
t.userName -> t.repositoryName searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
} .groupBy { t =>
.map { case (repo, t) => t.userName ~ t.repositoryName
(repo._1, repo._2, t.length) }
} .map { case (repo, t) =>
.sortBy(_._3 desc) repo ~ t.length
.list }
} .sortBy(_._3 desc)
.list
/** }
* Returns the search result against issues.
* /**
* @param condition the search condition * Returns the search result against issues.
* @param pullRequest if true then returns only pull requests, false then returns only issues. *
* @param offset the offset for pagination * @param condition the search condition
* @param limit the limit for pagination * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
* @param repos Tuple of the repository owner and the repository name * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @return the search result (list of tuples which contain issue, labels and comment count) * @param offset the offset for pagination
*/ * @param limit the limit for pagination
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, * @param repos Tuple of the repository owner and the repository name
offset: Int, limit: Int, repos: (String, String)*) * @return the search result (list of tuples which contain issue, labels and comment count)
(implicit s: Session): List[IssueInfo] = { */
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
// get issues and comment count and labels offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } // get issues and comment count and labels
.sortBy { case (t1, t2) => searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
(condition.sort match { .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
case "created" => t1.registeredDate .sortBy { case (t1, t2) =>
case "comments" => t2.commentCount (condition.sort match {
case "updated" => t1.updatedDate case "created" => t1.registeredDate
}) match { case "comments" => t2.commentCount
case sort => condition.direction match { case "updated" => t1.updatedDate
case "asc" => sort asc }) match {
case "desc" => sort desc case sort => condition.direction match {
} case "asc" => sort asc
} case "desc" => sort desc
} }
.drop(offset).take(limit) }
.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) } .drop(offset).take(limit)
.leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.map { case ((((t1, t2), t3), t4), t5) => .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?) .map { case (((t1, t2), t3), t4) =>
} (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
.list }
.splitWith { (c1, c2) => .list
c1._1.userName == c2._1.userName && .splitWith { (c1, c2) =>
c1._1.repositoryName == c2._1.repositoryName && c1._1.userName == c2._1.userName &&
c1._1.issueId == c2._1.issueId c1._1.repositoryName == c2._1.repositoryName &&
} c1._1.issueId == c2._1.issueId
.map { issues => issues.head match { }
case (issue, commentCount, _, _, _, milestone) => .map { issues => issues.head match {
IssueInfo(issue, case (issue, commentCount, _,_,_) =>
issues.flatMap { t => t._3.map ( (issue,
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) issues.flatMap { t => t._3.map (
)} toList, Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
milestone, )} toList,
commentCount) commentCount)
}} toList }} toList
} }
/** /**
* 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) =
Issues filter { t1 => Query(Issues) filter { t1 =>
condition.repo condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos) .getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) } .map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) && .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) && (t1.closed is (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) && (t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) && (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(t1.pullRequest === pullRequest.bind) && (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(IssueLabels filter { t2 => (t1.pullRequest is true.bind, onlyPullRequest) &&
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (IssueLabels filter { t2 =>
(t2.labelId in (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(Labels filter { t3 => (t2.labelId in
(t3.byRepository(t1.userName, t1.repositoryName)) && (Labels filter { t3 =>
(t3.labelName inSetBind condition.labels) (t3.byRepository(t1.userName, t1.repositoryName)) &&
} map(_.labelId))) (t3.labelName inSetBind condition.labels)
} exists, condition.labels.nonEmpty) } map(_.labelId)))
} } exists, condition.labels.nonEmpty)
}
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
isPullRequest: Boolean = false)(implicit s: Session) = assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) =
// next id number // next id number
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
.firstOption.filter { id => .firstOption.filter { id =>
Issues insert Issue( Issues insert Issue(
owner, owner,
repository, repository,
id, id,
loginUser, loginUser,
milestoneId, milestoneId,
assignedUserName, assignedUserName,
title, title,
content, content,
false, false,
currentDate, currentDate,
currentDate, currentDate,
isPullRequest) isPullRequest)
// increment issue id // increment issue id
IssueId IssueId
.filter (_.byPrimaryKey(owner, repository)) .filter (_.byPrimaryKey(owner, repository))
.map (_.issueId) .map (_.issueId)
.update (id) > 0 .update (id) > 0
} get } get
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
IssueLabels insert IssueLabel(owner, repository, issueId, labelId) IssueLabels insert (IssueLabel(owner, repository, issueId, labelId))
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
def createComment(owner: String, repository: String, loginUser: String, def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: String)(implicit s: Session): Int = issueId: Int, content: String, action: String) =
IssueComments.autoInc insert IssueComment( IssueComments.autoInc insert (
userName = owner, owner,
repositoryName = repository, repository,
issueId = issueId, issueId,
action = action, action,
commentedUserName = loginUser, loginUser,
content = content, content,
registeredDate = currentDate, currentDate,
updatedDate = currentDate) currentDate)
def updateIssue(owner: String, repository: String, issueId: Int, def updateIssue(owner: String, repository: String, issueId: Int,
title: String, content: Option[String])(implicit s: Session) = title: String, content: Option[String]) =
Issues Issues
.filter (_.byPrimaryKey(owner, repository, issueId)) .filter (_.byPrimaryKey(owner, repository, issueId))
.map { t => .map { t =>
(t.title, t.content.?, t.updatedDate) t.title ~ t.content.? ~ t.updatedDate
} }
.update (title, content, currentDate) .update (title, content, currentDate)
def updateAssignedUserName(owner: String, repository: String, issueId: Int, def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) =
assignedUserName: Option[String])(implicit s: Session) = Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) =
def updateMilestoneId(owner: String, repository: String, issueId: Int, Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
milestoneId: Option[Int])(implicit s: Session) =
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) def updateComment(commentId: Int, content: String) =
IssueComments
def updateComment(commentId: Int, content: String)(implicit s: Session) = .filter (_.byPrimaryKey(commentId))
IssueComments .map { t =>
.filter (_.byPrimaryKey(commentId)) t.content ~ t.updatedDate
.map { t => }
t.content -> t.updatedDate .update (content, currentDate)
}
.update (content, currentDate) def deleteComment(commentId: Int) =
IssueComments filter (_.byPrimaryKey(commentId)) delete
def deleteComment(commentId: Int)(implicit s: Session) =
IssueComments filter (_.byPrimaryKey(commentId)) delete def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
Issues
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) = .filter (_.byPrimaryKey(owner, repository, issueId))
Issues .map { t =>
.filter (_.byPrimaryKey(owner, repository, issueId)) t.closed ~ t.updatedDate
.map { t => }
t.closed -> t.updatedDate .update (closed, currentDate)
}
.update (closed, currentDate) /**
* Search issues by keyword.
/** *
* Search issues by keyword. * @param owner the repository owner
* * @param repository the repository name
* @param owner the repository owner * @param query the keywords separated by whitespace.
* @param repository the repository name * @return issues with comment count and matched content of issue or comment
* @param query the keywords separated by whitespace. */
* @return issues with comment count and matched content of issue or comment def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
*/ import scala.slick.driver.H2Driver.likeEncode
def searchIssuesByKeyword(owner: String, repository: String, query: String) val keywords = splitWords(query.toLowerCase)
(implicit s: Session): List[(Issue, Int, String)] = {
import slick.driver.JdbcDriver.likeEncode // Search Issue
val keywords = splitWords(query.toLowerCase) val issues = Issues
.innerJoin(IssueOutline).on { case (t1, t2) =>
// Search Issue t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
val issues = Issues }
.innerJoin(IssueOutline).on { case (t1, t2) => .filter { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) keywords.map { keyword =>
} (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
.filter { case (t1, t2) => (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
keywords.map { keyword => } .reduceLeft(_ && _)
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || }
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) .map { case (t1, t2) =>
} .reduceLeft(_ && _) (t1, 0, t1.content.?, t2.commentCount)
} }
.map { case (t1, t2) =>
(t1, 0, t1.content.?, t2.commentCount) // Search IssueComment
} val comments = IssueComments
.innerJoin(Issues).on { case (t1, t2) =>
// Search IssueComment t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
val comments = IssueComments }
.innerJoin(Issues).on { case (t1, t2) => .innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
} }
.innerJoin(IssueOutline).on { case ((t1, t2), t3) => .filter { case ((t1, t2), t3) =>
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) keywords.map { query =>
} t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
.filter { case ((t1, t2), t3) => }.reduceLeft(_ && _)
keywords.map { query => }
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') .map { case ((t1, t2), t3) =>
}.reduceLeft(_ && _) (t2, t1.commentId, t1.content.?, t3.commentCount)
} }
.map { case ((t1, t2), t3) =>
(t2, t1.commentId, t1.content.?, t3.commentCount) issues.union(comments).sortBy { case (issue, commentId, _, _) =>
} issue.issueId ~ commentId
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
issues.union(comments).sortBy { case (issue, commentId, _, _) => issue1.issueId == issue2.issueId
issue.issueId -> commentId }.map { _.head match {
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
issue1.issueId == issue2.issueId }
}.map { _.head match { }.toList
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse("")) }
}
}.toList }
}
object IssuesService {
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = { import javax.servlet.http.HttpServletRequest
extractCloseId(message).foreach { issueId =>
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ val IssueLimit = 30
createComment(owner, repository, userName, issue.issueId, "Close", "close")
updateClosed(owner, repository, issue.issueId, true) case class IssueSearchCondition(
} labels: Set[String] = Set.empty,
} milestoneId: Option[Option[Int]] = None,
} repo: Option[String] = None,
} state: String = "open",
sort: String = "created",
object IssuesService { direction: String = "desc"){
import javax.servlet.http.HttpServletRequest
def toURL: String =
val IssueLimit = 30 "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
case class IssueSearchCondition( milestoneId.map { id => "milestone=" + (id match {
labels: Set[String] = Set.empty, case Some(x) => x.toString
milestoneId: Option[Option[Int]] = None, case None => "none"
author: Option[String] = None, })},
assigned: Option[String] = None, repo.map("for=" + urlEncode(_)),
repo: Option[String] = None, Some("state=" + urlEncode(state)),
state: String = "open", Some("sort=" + urlEncode(sort)),
sort: String = "created", Some("direction=" + urlEncode(direction))).flatten.mkString("&")
direction: String = "desc"){
}
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty && object IssueSearchCondition {
state == "open" && sort == "created" && direction == "desc"
} private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
val value = request.getParameter(name)
def nonEmpty: Boolean = !isEmpty if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
}
def toURL: String =
"?" + List( def apply(request: HttpServletRequest): IssueSearchCondition =
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), IssueSearchCondition(
milestoneId.map { id => "milestone=" + (id match { param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
case Some(x) => x.toString param(request, "milestone").map{
case None => "none" case "none" => None
})}, case x => x.toIntOpt
author .map(x => "author=" + urlEncode(x)), },
assigned.map(x => "assigned=" + urlEncode(x)), param(request, "for"),
repo.map("for=" + urlEncode(_)), param(request, "state", Seq("open", "closed")).getOrElse("open"),
Some("state=" + urlEncode(state)), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
Some("sort=" + urlEncode(sort)), param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
def page(request: HttpServletRequest) = try {
} val i = param(request, "page").getOrElse("1").toInt
if(i <= 0) 1 else i
object IssueSearchCondition { } catch {
case e: NumberFormatException => 1
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { }
val value = request.getParameter(name) }
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
} }
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition(
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map{
case "none" => None
case x => x.toIntOpt
},
param(request, "author"),
param(request, "assigned"),
param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
}
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int)
}

View File

@@ -1,32 +1,26 @@
package service package service
import model.Profile._ import scala.slick.driver.H2Driver.simple._
import profile.simple._ import Database.threadLocalSession
import model.Label
import model._
trait LabelsService { trait LabelsService {
def getLabels(owner: String, repository: String)(implicit s: Session): List[Label] = def getLabels(owner: String, repository: String): List[Label] =
Labels.filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list Query(Labels).filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] = def getLabel(owner: String, repository: String, labelId: Int): Option[Label] =
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption Query(Labels).filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int = def createLabel(owner: String, repository: String, labelName: String, color: String): Unit =
Labels returning Labels.map(_.labelId) += Label( Labels.ins insert (owner, repository, labelName, color)
userName = owner,
repositoryName = repository,
labelName = labelName,
color = color
)
def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String) def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String): Unit =
(implicit s: Session): Unit = Labels.filter(_.byPrimaryKey(owner, repository, labelId)).map(t => t.labelName ~ t.color)
Labels.filter(_.byPrimaryKey(owner, repository, labelId)) .update(labelName, color)
.map(t => t.labelName -> t.color)
.update(labelName, color)
def deleteLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Unit = { def deleteLabel(owner: String, repository: String, labelId: Int): Unit = {
IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete
} }

View File

@@ -1,49 +1,39 @@
package service package service
import model.Profile._ import scala.slick.driver.H2Driver.simple._
import profile.simple._ import Database.threadLocalSession
import model.Milestone
// TODO [Slick 2.0]NOT import directly? import model._
import model.Profile.dateColumnType
trait MilestonesService { trait MilestonesService {
def createMilestone(owner: String, repository: String, title: String, description: Option[String], def createMilestone(owner: String, repository: String, title: String, description: Option[String],
dueDate: Option[java.util.Date])(implicit s: Session): Unit = dueDate: Option[java.util.Date]): Unit =
Milestones insert Milestone( Milestones.ins insert (owner, repository, title, description, dueDate, None)
userName = owner,
repositoryName = repository,
title = title,
description = description,
dueDate = dueDate,
closedDate = None
)
def updateMilestone(milestone: Milestone)(implicit s: Session): Unit = def updateMilestone(milestone: Milestone): Unit =
Milestones Milestones
.filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId)) .filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId))
.map (t => (t.title, t.description.?, t.dueDate.?, t.closedDate.?)) .map (t => t.title ~ t.description.? ~ t.dueDate.? ~ t.closedDate.?)
.update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate) .update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate)
def openMilestone(milestone: Milestone)(implicit s: Session): Unit = def openMilestone(milestone: Milestone): Unit = updateMilestone(milestone.copy(closedDate = None))
updateMilestone(milestone.copy(closedDate = None))
def closeMilestone(milestone: Milestone)(implicit s: Session): Unit = def closeMilestone(milestone: Milestone): Unit = updateMilestone(milestone.copy(closedDate = Some(currentDate)))
updateMilestone(milestone.copy(closedDate = Some(currentDate)))
def deleteMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Unit = { def deleteMilestone(owner: String, repository: String, milestoneId: Int): Unit = {
Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None) Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None)
Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete
} }
def getMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Option[Milestone] = def getMilestone(owner: String, repository: String, milestoneId: Int): Option[Milestone] =
Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption Query(Milestones).filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption
def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = { def getMilestonesWithIssueCount(owner: String, repository: String): List[(Milestone, Int, Int)] = {
val counts = Issues val counts = Issues
.filter { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) } .filter { t => (t.byRepository(owner, repository)) && (t.milestoneId isNotNull) }
.groupBy { t => t.milestoneId -> t.closed } .groupBy { t => t.milestoneId ~ t.closed }
.map { case (t1, t2) => t1._1 -> t1._2 -> t2.length } .map { case (t1, t2) => (t1._1 ~ t1._2) -> t2.length }
.toMap .toMap
getMilestones(owner, repository).map { milestone => getMilestones(owner, repository).map { milestone =>
@@ -51,7 +41,6 @@ trait MilestonesService {
} }
} }
def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] = def getMilestones(owner: String, repository: String): List[Milestone] =
Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list Query(Milestones).filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list
} }

View File

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

View File

@@ -1,63 +1,41 @@
package service package service
import model.Profile._ import scala.slick.driver.H2Driver.simple._
import profile.simple._ import Database.threadLocalSession
import model.{PullRequest, Issue} import model._
import util.ControlUtil._
trait PullRequestService { self: IssuesService => trait PullRequestService { self: IssuesService =>
import PullRequestService._ import PullRequestService._
def getPullRequest(owner: String, repository: String, issueId: Int) def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
(implicit s: Session): Option[(Issue, PullRequest)] =
getIssue(owner, repository, issueId.toString).flatMap{ issue => getIssue(owner, repository, issueId.toString).flatMap{ issue =>
PullRequests.filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{ Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
pullreq => (issue, pullreq) pullreq => (issue, pullreq)
} }
} }
def updateCommitId(owner: String, repository: String, issueId: Int, commitIdTo: String, commitIdFrom: String) def updateCommitIdTo(owner: String, repository: String, issueId: Int, commitIdTo: String): Unit =
(implicit s: Session): Unit = Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdTo).update(commitIdTo)
PullRequests.filter(_.byPrimaryKey(owner, repository, issueId))
.map(pr => pr.commitIdTo -> pr.commitIdFrom)
.update((commitIdTo, commitIdFrom))
def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String]) def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
(implicit s: Session): List[PullRequestCount] = Query(PullRequests)
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) => .filter { case (t1, t2) =>
(t2.closed === closed.bind) && (t2.closed is closed.bind) &&
(t1.userName === owner.get.bind, owner.isDefined) && (t1.userName is owner.bind) &&
(t1.repositoryName === repository.get.bind, repository.isDefined) (t1.repositoryName is repository.get.bind, repository.isDefined)
} }
.groupBy { case (t1, t2) => t2.openedUserName } .groupBy { case (t1, t2) => t2.openedUserName }
.map { case (userName, t) => userName -> t.length } .map { case (userName, t) => userName ~ t.length }
.sortBy(_._2 desc)
.list
.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) .sortBy(_._2 desc)
.list .list
.map { x => PullRequestCount(x._1, x._2) } .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): Unit =
PullRequests insert PullRequest( PullRequests insert (PullRequest(
originUserName, originUserName,
originRepositoryName, originRepositoryName,
issueId, issueId,
@@ -66,17 +44,16 @@ trait PullRequestService { self: IssuesService =>
requestRepositoryName, requestRepositoryName,
requestBranch, requestBranch,
commitIdFrom, commitIdFrom,
commitIdTo) commitIdTo))
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean) def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean): List[PullRequest] =
(implicit s: Session): List[PullRequest] = Query(PullRequests)
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) => .filter { case (t1, t2) =>
(t1.requestUserName === userName.bind) && (t1.requestUserName is userName.bind) &&
(t1.requestRepositoryName === repositoryName.bind) && (t1.requestRepositoryName is repositoryName.bind) &&
(t1.requestBranch === branch.bind) && (t1.requestBranch is branch.bind) &&
(t2.closed === closed.bind) (t2.closed is closed.bind)
} }
.map { case (t1, t2) => t1 } .map { case (t1, t2) => t1 }
.list .list

View File

@@ -3,20 +3,21 @@ package service
import util.{FileUtil, StringUtil, JGitUtil} import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import model.Issue
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk import org.eclipse.jgit.treewalk.TreeWalk
import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import model.Profile._
import profile.simple._
trait RepositorySearchService { self: IssuesService => trait
RepositorySearchService { self: IssuesService =>
import RepositorySearchService._ import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String)(implicit session: Session): Int = def countIssues(owner: String, repository: String, query: String): Int =
searchIssuesByKeyword(owner, repository, query).length searchIssuesByKeyword(owner, repository, query).length
def searchIssues(owner: String, repository: String, query: String)(implicit session: Session): List[IssueSearchResult] = def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) => searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult( IssueSearchResult(
issue.issueId, issue.issueId,
@@ -38,7 +39,7 @@ trait RepositorySearchService { self: IssuesService =>
Nil Nil
} else { } else {
val files = searchRepositoryFiles(git, query) val files = searchRepositoryFiles(git, query)
val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD") val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
files.map { case (path, text) => files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query) val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult( FileSearchResult(
@@ -59,12 +60,11 @@ trait RepositorySearchService { self: IssuesService =>
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
val keywords = StringUtil.splitWords(query.toLowerCase) val keywords = StringUtil.splitWords(query.toLowerCase)
val list = new scala.collection.mutable.ListBuffer[(String, String)] val list = new ListBuffer[(String, String)]
while (treeWalk.next()) { while (treeWalk.next()) {
val mode = treeWalk.getFileMode(0) if(treeWalk.getFileMode(0) != FileMode.TREE){
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){ JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){ if(FileUtil.isText(bytes)){
val text = StringUtil.convertFromByteArray(bytes) val text = StringUtil.convertFromByteArray(bytes)
val lowerText = text.toLowerCase val lowerText = text.toLowerCase
@@ -107,7 +107,7 @@ object RepositorySearchService {
case class SearchResult( case class SearchResult(
files : List[(String, String)], files : List[(String, String)],
issues: List[(model.Issue, Int, String)]) issues: List[(Issue, Int, String)])
case class IssueSearchResult( case class IssueSearchResult(
issueId: Int, issueId: Int,

View File

@@ -1,8 +1,8 @@
package service package service
import model.Profile._ import model._
import profile.simple._ import scala.slick.driver.H2Driver.simple._
import model.{Repository, Account, Collaborator} import Database.threadLocalSession
import util.JGitUtil import util.JGitUtil
trait RepositoryService { self: AccountService => trait RepositoryService { self: AccountService =>
@@ -20,8 +20,7 @@ trait RepositoryService { self: AccountService =>
*/ */
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
originRepositoryName: Option[String] = None, originUserName: Option[String] = None, originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None) parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = {
(implicit s: Session): Unit = {
Repositories insert Repositories insert
Repository( Repository(
userName = userName, userName = userName,
@@ -40,83 +39,70 @@ trait RepositoryService { self: AccountService =>
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
} }
def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String) def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String): Unit = {
(implicit s: Session): Unit = { (Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
getAccountByUserName(newUserName).foreach { account => Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list val commitLog = Query(CommitLog ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t => Repositories.filter { t =>
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind) (t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
Repositories.filter { t => Repositories.filter { t =>
(t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind) (t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t => PullRequests.filter { t =>
t.requestRepositoryName === oldRepositoryName.bind t.requestRepositoryName is oldRepositoryName.bind
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.requestUserName ~ t.requestRepositoryName }.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)) :_*)
Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Collaborators .insertAll(collaborators .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitLog .insertAll(commitLog .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list // Update activity messages
Issues.insertAll(issues.map { x => x.copy( val updateActivities = Activities.filter { t =>
userName = newUserName, (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
repositoryName = newRepositoryName, (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
milestoneId = x.milestoneId.map { id => }.map { t => t.activityId ~ t.message }.list
newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId
}
)} :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) updateActivities.foreach { case (activityId, message) =>
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Activities.filter(_.activityId is activityId.bind).map(_.message).update(
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) message
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) .replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
if(account.isGroupAccount){ .replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*) .replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
} else { .replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) )
}
// Update activity messages
val updateActivities = Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
}.map { t => t.activityId -> t.message }.list
updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
.replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
)
}
} }
} }
} }
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = { def deleteRepository(userName: String, repositoryName: String): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete Collaborators .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
@@ -135,8 +121,8 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of repository owner * @param userName the user name of repository owner
* @return the list of repository names * @return the list of repository names
*/ */
def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] = def getRepositoryNamesOfUser(userName: String): List[String] =
Repositories filter(_.userName === userName.bind) map (_.repositoryName) list Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
/** /**
* Returns the specified repository information. * Returns the specified repository information.
@@ -146,11 +132,11 @@ trait RepositoryService { self: AccountService =>
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @return the repository information * @return the repository information
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
(Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
// for getting issue count and pull request count // for getting issue count and pull request count
val issues = Issues.filter { t => val issues = Query(Issues).filter { t =>
t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind) t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind)
}.map(_.pullRequest).list }.map(_.pullRequest).list
new RepositoryInfo( new RepositoryInfo(
@@ -161,46 +147,22 @@ trait RepositoryService { self: AccountService =>
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
), ))
getRepositoryManagers(repository.userName))
} }
} }
/** def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
* Returns the repositories without private repository that user does not have access right. Query(Repositories).filter { t1 =>
* Include public repository, private own repository and private but collaborator repository. (t1.userName is userName.bind) ||
* (Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
* @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)
}.list
}
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 =>
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
if(withoutPhysicalInfo){ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
), ))
getRepositoryManagers(repository.userName))
} }
} }
@@ -211,61 +173,45 @@ trait RepositoryService { self: AccountService =>
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate. * @return the repository information which is sorted in descending order of lastActivityDate.
*/ */
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None, def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = {
withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
// for Administrators // for Administrators
case Some(x) if(x.isAdmin) => Repositories case Some(x) if(x.isAdmin) => Query(Repositories)
// for Normal Users // for Normal Users
case Some(x) if(!x.isAdmin) => case Some(x) if(!x.isAdmin) =>
Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) || Query(Repositories) filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) ||
(Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists) (Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
} }
// for Guests // for Guests
case None => Repositories filter(_.isPrivate === false.bind) case None => Query(Repositories) filter(_.isPrivate is false.bind)
}).filter { t => }).filter { t =>
repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true) repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
if(withoutPhysicalInfo){ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
), ))
getRepositoryManagers(repository.userName))
} }
} }
private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
} else {
Seq(userName)
}
/** /**
* Updates the last activity date of the repository. * Updates the last activity date of the repository.
*/ */
def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit = def updateLastActivityDate(userName: String, repositoryName: String): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate) Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
/** /**
* Save repository options. * Save repository options.
*/ */
def saveRepositoryOptions(userName: String, repositoryName: String, def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit = description: Option[String], defaultBranch: String, isPrivate: Boolean): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)) Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) } .map { r => r.description.? ~ r.defaultBranch ~ r.isPrivate ~ r.updatedDate }
.update (description, defaultBranch, isPrivate, currentDate) .update (description, defaultBranch, isPrivate, currentDate)
/** /**
@@ -275,8 +221,8 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @param collaboratorName the collaborator name * @param collaboratorName the collaborator name
*/ */
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = def addCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
Collaborators insert Collaborator(userName, repositoryName, collaboratorName) Collaborators insert(Collaborator(userName, repositoryName, collaboratorName))
/** /**
* Remove collaborator from the repository. * Remove collaborator from the repository.
@@ -285,7 +231,7 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @param collaboratorName the collaborator name * @param collaboratorName the collaborator name
*/ */
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/** /**
@@ -294,7 +240,7 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param repositoryName the repository name * @param repositoryName the repository name
*/ */
def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit = def removeCollaborators(userName: String, repositoryName: String): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/** /**
@@ -304,10 +250,10 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @return the list of collaborators name * @return the list of collaborators name
*/ */
def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] = def getCollaborators(userName: String, repositoryName: String): List[String] =
Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list Query(Collaborators).filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account]): Boolean = {
loginAccount match { loginAccount match {
case Some(a) if(a.isAdmin) => true case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true case Some(a) if(a.userName == owner) => true
@@ -316,41 +262,37 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int = private def getForkedCount(userName: String, repositoryName: String): Int =
Query(Repositories.filter { t => Query(Repositories.filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}.length).first }.length).first
def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] = def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] =
Repositories.filter { t => Query(Repositories).filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
} }
.sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list .sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list
} }
object RepositoryService { object RepositoryService {
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository, case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){ branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
/** /**
* Creates instance with issue count and pull request count. * Creates instance with issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
/** /**
* Creates instance without issue count and pull request count. * Creates instance without issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
} }
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])

View File

@@ -1,7 +1,7 @@
package service package service
import model.{Account, Issue, Session} import model._
import util.Implicits.request2Session import service.SystemSettingsService.SystemSettings
/** /**
* This service is used for a view helper mainly. * This service is used for a view helper mainly.
@@ -9,29 +9,29 @@ import util.Implicits.request2Session
* It may be called many times in one request, so each method stores * It may be called many times in one request, so each method stores
* its result into the cache which available during a request. * its result into the cache which available during a request.
*/ */
trait RequestCache extends SystemSettingsService with AccountService with IssuesService { trait RequestCache {
private implicit def context2Session(implicit context: app.Context): Session = def getSystemSettings()(implicit context: app.Context): SystemSettings =
request2Session(context.request) context.cache("system_settings"){
new SystemSettingsService {}.loadSystemSettings()
}
def getIssue(userName: String, repositoryName: String, issueId: String) def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
(implicit context: app.Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){ context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
super.getIssue(userName, repositoryName, issueId) new IssuesService {}.getIssue(userName, repositoryName, issueId)
} }
} }
def getAccountByUserName(userName: String) def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${userName}"){ context.cache(s"account.${userName}"){
super.getAccountByUserName(userName) new AccountService {}.getAccountByUserName(userName)
} }
} }
def getAccountByMailAddress(mailAddress: String) def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${mailAddress}"){ context.cache(s"account.${mailAddress}"){
super.getAccountByMailAddress(mailAddress) new AccountService {}.getAccountByMailAddress(mailAddress)
} }
} }
} }

View File

@@ -1,18 +0,0 @@
package service
import model.Profile._
import profile.simple._
import model.SshKey
trait SshKeyService {
def addPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit =
SshKeys insert SshKey(userName = userName, title = title, publicKey = publicKey)
def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] =
SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list
def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit =
SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
}

View File

@@ -1,197 +1,175 @@
package service package service
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
trait SystemSettingsService { trait SystemSettingsService {
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request) def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse {
defining(request.getRequestURL.toString){ url =>
def saveSystemSettings(settings: SystemSettings): Unit = { url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
defining(new java.util.Properties()){ props => }
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) }.replaceFirst("/$", "")
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString) def saveSystemSettings(settings: SystemSettings): Unit = {
props.setProperty(Notification, settings.notification.toString) defining(new java.util.Properties()){ props =>
props.setProperty(Ssh, settings.ssh.toString) settings.baseUrl.foreach(props.setProperty(BaseURL, _))
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
if(settings.notification) { props.setProperty(Gravatar, settings.gravatar.toString)
settings.smtp.foreach { smtp => props.setProperty(Notification, settings.notification.toString)
props.setProperty(SmtpHost, smtp.host) if(settings.notification) {
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) settings.smtp.foreach { smtp =>
smtp.user.foreach(props.setProperty(SmtpUser, _)) props.setProperty(SmtpHost, smtp.host)
smtp.password.foreach(props.setProperty(SmtpPassword, _)) smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) smtp.user.foreach(props.setProperty(SmtpUser, _))
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _)) smtp.password.foreach(props.setProperty(SmtpPassword, _))
smtp.fromName.foreach(props.setProperty(SmtpFromName, _)) smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
} smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
} smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) }
if(settings.ldapAuthentication){ }
settings.ldap.map { ldap => props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
props.setProperty(LdapHost, ldap.host) if(settings.ldapAuthentication){
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) settings.ldap.map { ldap =>
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) props.setProperty(LdapHost, ldap.host)
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
props.setProperty(LdapBaseDN, ldap.baseDN) ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x)) props.setProperty(LdapBaseDN, ldap.baseDN)
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x.toString)) ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
} ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
} }
using(new java.io.FileOutputStream(GitBucketConf)){ out => }
props.store(out, null) props.store(new java.io.FileOutputStream(GitBucketConf), null)
} }
} }
}
def loadSystemSettings(): SystemSettings = {
def loadSystemSettings(): SystemSettings = { defining(new java.util.Properties()){ props =>
defining(new java.util.Properties()){ props => if(GitBucketConf.exists){
if(GitBucketConf.exists){ props.load(new java.io.FileInputStream(GitBucketConf))
using(new java.io.FileInputStream(GitBucketConf)){ in => }
props.load(in) SystemSettings(
} getOptionValue(props, BaseURL, None),
} getValue(props, AllowAccountRegistration, false),
SystemSettings( getValue(props, Gravatar, true),
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getValue(props, Notification, false),
getValue(props, AllowAccountRegistration, false), if(getValue(props, Notification, false)){
getValue(props, Gravatar, true), Some(Smtp(
getValue(props, Notification, false), getValue(props, SmtpHost, ""),
getValue(props, Ssh, false), getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SshPort, Some(DefaultSshPort)), getOptionValue(props, SmtpUser, None),
if(getValue(props, Notification, false)){ getOptionValue(props, SmtpPassword, None),
Some(Smtp( getOptionValue[Boolean](props, SmtpSsl, None),
getValue(props, SmtpHost, ""), getOptionValue(props, SmtpFromAddress, None),
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)), getOptionValue(props, SmtpFromName, None)))
getOptionValue(props, SmtpUser, None), } else {
getOptionValue(props, SmtpPassword, None), None
getOptionValue[Boolean](props, SmtpSsl, None), },
getOptionValue(props, SmtpFromAddress, None), getValue(props, LdapAuthentication, false),
getOptionValue(props, SmtpFromName, None))) if(getValue(props, LdapAuthentication, false)){
} else { Some(Ldap(
None getValue(props, LdapHost, ""),
}, getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
getValue(props, LdapAuthentication, false), getOptionValue(props, LdapBindDN, None),
if(getValue(props, LdapAuthentication, false)){ getOptionValue(props, LdapBindPassword, None),
Some(Ldap( getValue(props, LdapBaseDN, ""),
getValue(props, LdapHost, ""), getValue(props, LdapUserNameAttribute, ""),
getOptionValue(props, LdapPort, Some(DefaultLdapPort)), getOptionValue(props, LdapFullNameAttribute, None),
getOptionValue(props, LdapBindDN, None), getValue(props, LdapMailAddressAttribute, ""),
getOptionValue(props, LdapBindPassword, None), getOptionValue[Boolean](props, LdapTls, None),
getValue(props, LdapBaseDN, ""), getOptionValue(props, LdapKeystore, None)))
getValue(props, LdapUserNameAttribute, ""), } else {
getOptionValue(props, LdapAdditionalFilterCondition, None), None
getOptionValue(props, LdapFullNameAttribute, None), }
getOptionValue(props, LdapMailAddressAttribute, None), )
getOptionValue[Boolean](props, LdapTls, None), }
getOptionValue(props, LdapKeystore, None))) }
} else {
None }
}
) object SystemSettingsService {
} import scala.reflect.ClassTag
}
case class SystemSettings(
} baseUrl: Option[String],
allowAccountRegistration: Boolean,
object SystemSettingsService { gravatar: Boolean,
import scala.reflect.ClassTag notification: Boolean,
smtp: Option[Smtp],
case class SystemSettings( ldapAuthentication: Boolean,
baseUrl: Option[String], ldap: Option[Ldap])
allowAccountRegistration: Boolean,
gravatar: Boolean, case class Ldap(
notification: Boolean, host: String,
ssh: Boolean, port: Option[Int],
sshPort: Option[Int], bindDN: Option[String],
smtp: Option[Smtp], bindPassword: Option[String],
ldapAuthentication: Boolean, baseDN: String,
ldap: Option[Ldap]){ userNameAttribute: String,
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse { fullNameAttribute: Option[String],
defining(request.getRequestURL.toString){ url => mailAttribute: String,
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) tls: Option[Boolean],
} keystore: Option[String])
}.stripSuffix("/")
} case class Smtp(
host: String,
case class Ldap( port: Option[Int],
host: String, user: Option[String],
port: Option[Int], password: Option[String],
bindDN: Option[String], ssl: Option[Boolean],
bindPassword: Option[String], fromAddress: Option[String],
baseDN: String, fromName: Option[String])
userNameAttribute: String,
additionalFilterCondition: Option[String], val DefaultSmtpPort = 25
fullNameAttribute: Option[String], val DefaultLdapPort = 389
mailAttribute: Option[String],
tls: Option[Boolean], private val BaseURL = "base_url"
keystore: Option[String]) private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar"
case class Smtp( private val Notification = "notification"
host: String, private val SmtpHost = "smtp.host"
port: Option[Int], private val SmtpPort = "smtp.port"
user: Option[String], private val SmtpUser = "smtp.user"
password: Option[String], private val SmtpPassword = "smtp.password"
ssl: Option[Boolean], private val SmtpSsl = "smtp.ssl"
fromAddress: Option[String], private val SmtpFromAddress = "smtp.from_address"
fromName: Option[String]) private val SmtpFromName = "smtp.from_name"
private val LdapAuthentication = "ldap_authentication"
val DefaultSshPort = 29418 private val LdapHost = "ldap.host"
val DefaultSmtpPort = 25 private val LdapPort = "ldap.port"
val DefaultLdapPort = 389 private val LdapBindDN = "ldap.bindDN"
private val LdapBindPassword = "ldap.bind_password"
private val BaseURL = "base_url" private val LdapBaseDN = "ldap.baseDN"
private val AllowAccountRegistration = "allow_account_registration" private val LdapUserNameAttribute = "ldap.username_attribute"
private val Gravatar = "gravatar" private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val Notification = "notification" private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val Ssh = "ssh" private val LdapTls = "ldap.tls"
private val SshPort = "ssh.port" private val LdapKeystore = "ldap.keystore"
private val SmtpHost = "smtp.host"
private val SmtpPort = "smtp.port" private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
private val SmtpUser = "smtp.user" defining(props.getProperty(key)){ value =>
private val SmtpPassword = "smtp.password" if(value == null || value.isEmpty) default
private val SmtpSsl = "smtp.ssl" else convertType(value).asInstanceOf[A]
private val SmtpFromAddress = "smtp.from_address" }
private val SmtpFromName = "smtp.from_name"
private val LdapAuthentication = "ldap_authentication" private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
private val LdapHost = "ldap.host" defining(props.getProperty(key)){ value =>
private val LdapPort = "ldap.port" if(value == null || value.isEmpty) default
private val LdapBindDN = "ldap.bindDN" else Some(convertType(value)).asInstanceOf[Option[A]]
private val LdapBindPassword = "ldap.bind_password" }
private val LdapBaseDN = "ldap.baseDN"
private val LdapUserNameAttribute = "ldap.username_attribute" private def convertType[A: ClassTag](value: String) =
private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition" defining(implicitly[ClassTag[A]].runtimeClass){ c =>
private val LdapFullNameAttribute = "ldap.fullname_attribute" if(c == classOf[Boolean]) value.toBoolean
private val LdapMailAddressAttribute = "ldap.mail_attribute" else if(c == classOf[Int]) value.toInt
private val LdapTls = "ldap.tls" else value
private val LdapKeystore = "ldap.keystore" }
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = }
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default
else convertType(value).asInstanceOf[A]
}
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default
else Some(convertType(value)).asInstanceOf[Option[A]]
}
private def convertType[A: ClassTag](value: String) =
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
if(c == classOf[Boolean]) value.toBoolean
else if(c == classOf[Int]) value.toInt
else value
}
// TODO temporary flag
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
}

View File

@@ -1,8 +1,9 @@
package service package service
import model.Profile._ import scala.slick.driver.H2Driver.simple._
import profile.simple._ import Database.threadLocalSession
import model.{WebHook, Account}
import model._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo import service.RepositoryService.RepositoryInfo
import util.JGitUtil import util.JGitUtil
@@ -11,6 +12,7 @@ import util.JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.protocol.HTTP
import org.apache.http.NameValuePair import org.apache.http.NameValuePair
trait WebHookService { trait WebHookService {
@@ -18,14 +20,14 @@ trait WebHookService {
private val logger = LoggerFactory.getLogger(classOf[WebHookService]) private val logger = LoggerFactory.getLogger(classOf[WebHookService])
def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] = def getWebHookURLs(owner: String, repository: String): List[WebHook] =
WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list
def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = def addWebHookURL(owner: String, repository: String, url :String): Unit =
WebHooks insert WebHook(owner, repository, url) WebHooks.insert(WebHook(owner, repository, url))
def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = def deleteWebHookURL(owner: String, repository: String, url :String): Unit =
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = { def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
import org.json4s._ import org.json4s._
@@ -44,7 +46,7 @@ trait WebHookService {
val httpClient = HttpClientBuilder.create.build val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl => webHookURLs.foreach { webHookUrl =>
val f = Future { val f = future {
logger.debug(s"start web hook invocation for ${webHookUrl}") logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url) val httpPost = new HttpPost(webHookUrl.url)
@@ -85,26 +87,26 @@ object WebHookService {
refName, refName,
commits.map { commit => commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false) val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
WebHookCommit( WebHookCommit(
id = commit.id, id = commit.id,
message = commit.fullMessage, message = commit.fullMessage,
timestamp = commit.commitTime.toString, timestamp = commit.time.toString,
url = commitUrl, url = commitUrl,
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath }, added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath }, removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD && modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath }, x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookUser( author = WebHookUser(
name = commit.committerName, name = commit.committer,
email = commit.committerEmailAddress email = commit.mailAddress
) )
) )
}, }.toList,
WebHookRepository( WebHookRepository(
name = repositoryInfo.name, name = repositoryInfo.name,
url = repositoryInfo.httpUrl, url = repositoryInfo.url,
description = repositoryInfo.repository.description.getOrElse(""), description = repositoryInfo.repository.description.getOrElse(""),
watchers = 0, watchers = 0,
forks = repositoryInfo.forkedCount, forks = repositoryInfo.forkedCount,

View File

@@ -1,281 +1,302 @@
package service package service
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util._ import org.apache.commons.io.FileUtils
import _root_.util.ControlUtil._ import util._
import org.eclipse.jgit.treewalk.CanonicalTreeParser import _root_.util.ControlUtil._
import org.eclipse.jgit.lib._ import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.lib._
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter} import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry}
import java.io.ByteArrayInputStream import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.patch._ import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
import org.eclipse.jgit.api.errors.PatchFormatException import java.io.ByteArrayInputStream
import scala.collection.JavaConverters._ import org.eclipse.jgit.patch._
import service.RepositoryService.RepositoryInfo import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._
object WikiService { import scala.Some
/**
* The model for wiki page. object WikiService {
*
* @param name the page name /**
* @param content the page content * The model for wiki page.
* @param committer the last committer *
* @param time the last modified time * @param name the page name
* @param id the latest commit id * @param content the page content
*/ * @param committer the last committer
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String) * @param time the last modified time
* @param id the latest commit id
/** */
* The model for wiki page history. case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
*
* @param name the page name /**
* @param committer the committer the committer * The model for wiki page history.
* @param message the commit message *
* @param date the commit date * @param name the page name
*/ * @param committer the committer the committer
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) * @param message the commit message
* @param date the commit date
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git") */
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") }
}
trait WikiService {
trait WikiService { import WikiService._
import WikiService._
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = LockUtil.lock(s"${owner}/${repository}/wiki"){
LockUtil.lock(s"${owner}/${repository}/wiki"){ defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => if(!dir.exists){
if(!dir.exists){ JGitUtil.initRepository(dir)
JGitUtil.initRepository(dir) saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) }
} }
} }
}
/**
/** * Returns the wiki page.
* Returns the wiki page. */
*/ def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => if(!JGitUtil.isEmpty(git)){
if(!JGitUtil.isEmpty(git)){ JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), file.committer, file.time, file.commitId)
file.author, file.time, file.commitId) }
} } else None
} else None }
} }
}
/**
/** * Returns the content of the specified file.
* Returns the content of the specified file. */
*/ def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => if(!JGitUtil.isEmpty(git)){
if(!JGitUtil.isEmpty(git)){ val index = path.lastIndexOf('/')
val index = path.lastIndexOf('/') val parentPath = if(index < 0) "." else path.substring(0, index)
val parentPath = if(index < 0) "." else path.substring(0, index) val fileName = if(index < 0) path else path.substring(index + 1)
val fileName = if(index < 0) path else path.substring(index + 1)
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file => git.getRepository.open(file.id).getBytes
git.getRepository.open(file.id).getBytes }
} } else None
} else None }
}
/**
/** * Returns the list of wiki page names.
* Returns the list of wiki page names. */
*/ def getWikiPageList(owner: String, repository: String): List[String] = {
def getWikiPageList(owner: String, repository: String): List[String] = { using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => JGitUtil.getFileList(git, "master", ".")
JGitUtil.getFileList(git, "master", ".") .filter(_.name.endsWith(".md"))
.filter(_.name.endsWith(".md")) .map(_.name.replaceFirst("\\.md$", ""))
.map(_.name.stripSuffix(".md")) .sortBy(x => x)
.sortBy(x => x) }
} }
}
/**
/** * Reverts specified changes.
* Reverts specified changes. */
*/ def revertWikiPage(owner: String, repository: String, from: String, to: String,
def revertWikiPage(owner: String, repository: String, from: String, to: String, committer: model.Account, pageName: Option[String]): Boolean = {
committer: model.Account, pageName: Option[String]): Boolean = {
case class RevertInfo(operation: String, filePath: String, source: String)
case class RevertInfo(operation: String, filePath: String, source: String)
try {
try { LockUtil.lock(s"${owner}/${repository}/wiki"){
LockUtil.lock(s"${owner}/${repository}/wiki"){ using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val reader = git.getRepository.newObjectReader
val reader = git.getRepository.newObjectReader val oldTreeIter = new CanonicalTreeParser
val oldTreeIter = new CanonicalTreeParser oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
val newTreeIter = new CanonicalTreeParser
val newTreeIter = new CanonicalTreeParser newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff => pageName match {
pageName match { case Some(x) => diff.getNewPath == x + ".md"
case Some(x) => diff.getNewPath == x + ".md" case None => true
case None => true }
} }
}
val patch = using(new java.io.ByteArrayOutputStream()){ out =>
val patch = using(new java.io.ByteArrayOutputStream()){ out => val formatter = new DiffFormatter(out)
val formatter = new DiffFormatter(out) formatter.setRepository(git.getRepository)
formatter.setRepository(git.getRepository) formatter.format(diffs.asJava)
formatter.format(diffs.asJava) new String(out.toByteArray, "UTF-8")
new String(out.toByteArray, "UTF-8") }
}
val p = new Patch()
val p = new Patch() p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8"))) if(!p.getErrors.isEmpty){
if(!p.getErrors.isEmpty){ throw new PatchFormatException(p.getErrors())
throw new PatchFormatException(p.getErrors()) }
} val revertInfo = (p.getFiles.asScala.map { fh =>
val revertInfo = (p.getFiles.asScala.map { fh => fh.getChangeType match {
fh.getChangeType match { case DiffEntry.ChangeType.MODIFY => {
case DiffEntry.ChangeType.MODIFY => { val source = getWikiPage(owner, repository, fh.getNewPath.replaceFirst("\\.md$", "")).map(_.content).getOrElse("")
val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("") val applied = PatchUtil.apply(source, patch, fh)
val applied = PatchUtil.apply(source, patch, fh) if(applied != null){
if(applied != null){ Seq(RevertInfo("ADD", fh.getNewPath, applied))
Seq(RevertInfo("ADD", fh.getNewPath, applied)) } else Nil
} else Nil }
} case DiffEntry.ChangeType.ADD => {
case DiffEntry.ChangeType.ADD => { val applied = PatchUtil.apply("", patch, fh)
val applied = PatchUtil.apply("", patch, fh) if(applied != null){
if(applied != null){ Seq(RevertInfo("ADD", fh.getNewPath, applied))
Seq(RevertInfo("ADD", fh.getNewPath, applied)) } else Nil
} else Nil }
} case DiffEntry.ChangeType.DELETE => {
case DiffEntry.ChangeType.DELETE => { Seq(RevertInfo("DELETE", fh.getNewPath, ""))
Seq(RevertInfo("DELETE", fh.getNewPath, "")) }
} case DiffEntry.ChangeType.RENAME => {
case DiffEntry.ChangeType.RENAME => { val applied = PatchUtil.apply("", patch, fh)
val applied = PatchUtil.apply("", patch, fh) if(applied != null){
if(applied != null){ Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied)) } else {
} else { Seq(RevertInfo("DELETE", fh.getOldPath, ""))
Seq(RevertInfo("DELETE", fh.getOldPath, "")) }
} }
} case _ => Nil
case _ => Nil }
} }).flatten
}).flatten
if(revertInfo.nonEmpty){
if(revertInfo.nonEmpty){ val builder = DirCache.newInCore.builder()
val builder = DirCache.newInCore.builder() val inserter = git.getRepository.newObjectInserter()
val inserter = git.getRepository.newObjectInserter() val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
using(new RevWalk(git.getRepository)){ revWalk =>
JGitUtil.processTree(git, headId){ (path, tree) => using(new TreeWalk(git.getRepository)){ treeWalk =>
if(revertInfo.find(x => x.filePath == path).isEmpty){ val index = treeWalk.addTree(revWalk.parseTree(headId))
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) treeWalk.setRecursive(true)
} while(treeWalk.next){
} val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
revertInfo.filter(_.operation == "ADD").foreach { x => if(revertInfo.find(x => x.filePath == path).isEmpty){
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8")))) builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} }
builder.finish() }
}
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), }
Constants.HEAD, committer.fullName, committer.mailAddress,
pageName match { revertInfo.filter(_.operation == "ADD").foreach { x =>
case Some(x) => s"Revert ${from} ... ${to} on ${x}" builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
case None => s"Revert ${from} ... ${to}" }
}) builder.finish()
}
} JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
} pageName match {
true case Some(x) => s"Revert ${from} ... ${to} on ${x}"
} catch { case None => s"Revert ${from} ... ${to}"
case e: Exception => { })
e.printStackTrace() }
false }
} }
} true
} } catch {
case e: Exception => {
/** e.printStackTrace()
* Save the wiki page. false
*/ }
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, }
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = { }
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => /**
val builder = DirCache.newInCore.builder() * Save the wiki page.
val inserter = git.getRepository.newObjectInserter() */
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
var created = true content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
var updated = false LockUtil.lock(s"${owner}/${repository}/wiki"){
var removed = false using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder()
if(headId != null){ val inserter = git.getRepository.newObjectInserter()
JGitUtil.processTree(git, headId){ (path, tree) => val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
if(path == currentPageName + ".md" && currentPageName != newPageName){ var created = true
removed = true var updated = false
} else if(path != newPageName + ".md"){ var removed = false
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else { if(headId != null){
created = false using(new RevWalk(git.getRepository)){ revWalk =>
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) using(new TreeWalk(git.getRepository)){ treeWalk =>
} val index = treeWalk.addTree(revWalk.parseTree(headId))
} treeWalk.setRecursive(true)
} while(treeWalk.next){
val path = treeWalk.getPathString
if(created || updated || removed){ val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) if(path == currentPageName + ".md" && currentPageName != newPageName){
builder.finish() removed = true
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), } else if(path != newPageName + ".md"){
Constants.HEAD, committer.fullName, committer.mailAddress, builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
if(message.trim.length == 0) { } else {
if(removed){ created = false
s"Rename ${currentPageName} to ${newPageName}" updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
} else if(created){ }
s"Created ${newPageName}" }
} else { }
s"Updated ${newPageName}" }
} }
} else {
message if(created || updated || removed){
}) builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
Some(newHeadId.getName) val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
} else None if(message.trim.length == 0) {
} if(removed){
} s"Rename ${currentPageName} to ${newPageName}"
} } else if(created){
s"Created ${newPageName}"
/** } else {
* Delete the wiki page. s"Updated ${newPageName}"
*/ }
def deleteWikiPage(owner: String, repository: String, pageName: String, } else {
committer: String, mailAddress: String, message: String): Unit = { message
LockUtil.lock(s"${owner}/${repository}/wiki"){ })
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder() Some(newHeadId)
val inserter = git.getRepository.newObjectInserter() } else None
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") }
var removed = false }
}
JGitUtil.processTree(git, headId){ (path, tree) =>
if(path != pageName + ".md"){ /**
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) * Delete the wiki page.
} else { */
removed = true def deleteWikiPage(owner: String, repository: String, pageName: String,
} committer: String, mailAddress: String, message: String): Unit = {
} LockUtil.lock(s"${owner}/${repository}/wiki"){
if(removed){ using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
builder.finish() val builder = DirCache.newInCore.builder()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), val inserter = git.getRepository.newObjectInserter()
Constants.HEAD, committer, mailAddress, message) val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
} var removed = false
}
} using(new RevWalk(git.getRepository)){ revWalk =>
} using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
} treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path != pageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
removed = true
}
}
}
if(removed){
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
}
}
}
}
}
}

View File

@@ -1,250 +1,164 @@
package servlet package servlet
import java.io.File import java.io.File
import java.sql.{DriverManager, Connection} import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent} import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
import org.apache.commons.io.IOUtils 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 org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory
import plugin.PluginUpdateJob object AutoUpdate {
import service.SystemSettingsService
/**
object AutoUpdate { * Version of GitBucket
*
/** * @param majorVersion the major version
* Version of GitBucket * @param minorVersion the minor version
* */
* @param majorVersion the major version case class Version(majorVersion: Int, minorVersion: Int){
* @param minorVersion the minor version
*/ private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
case class Version(majorVersion: Int, minorVersion: Int){
/**
private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version]) * Execute update/MAJOR_MINOR.sql to update schema to this version.
* If corresponding SQL file does not exist, this method do nothing.
/** */
* Execute update/MAJOR_MINOR.sql to update schema to this version. def update(conn: Connection): Unit = {
* If corresponding SQL file does not exist, this method do nothing. val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
*/
def update(conn: Connection): Unit = { using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" if(in != null){
val sql = IOUtils.toString(in, "UTF-8")
using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in => using(conn.createStatement()){ stmt =>
if(in != null){ logger.debug(sqlPath + "=" + sql)
val sql = IOUtils.toString(in, "UTF-8") stmt.executeUpdate(sql)
using(conn.createStatement()){ stmt => }
logger.debug(sqlPath + "=" + sql) }
stmt.executeUpdate(sql) }
} }
}
} /**
} * MAJOR.MINOR
*/
/** val versionString = s"${majorVersion}.${minorVersion}"
* MAJOR.MINOR }
*/
val versionString = s"${majorVersion}.${minorVersion}" /**
} * The history of versions. A head of this sequence is the current BitBucket version.
*/
/** val versions = Seq(
* The history of versions. A head of this sequence is the current BitBucket version. Version(1, 11),
*/ Version(1, 10),
val versions = Seq( Version(1, 9),
new Version(2, 4), Version(1, 8),
new Version(2, 3) { Version(1, 7),
override def update(conn: Connection): Unit = { Version(1, 6),
super.update(conn) Version(1, 5),
using(conn.createStatement.executeQuery("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'")){ rs => Version(1, 4),
while(rs.next) { new Version(1, 3){
val info = rs.getString("ADDITIONAL_INFO") override def update(conn: Connection): Unit = {
val newInfo = info.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n") super.update(conn)
if (info != newInfo) { // Fix wiki repository configuration
val id = rs.getString("ACTIVITY_ID") using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
using(conn.prepareStatement("UPDATE ACTIVITY SET ADDITIONAL_INFO=? WHERE ACTIVITY_ID=?")) { sql => while(rs.next){
sql.setString(1, newInfo) using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
sql.setLong(2, id.toLong) defining(git.getRepository.getConfig){ config =>
sql.executeUpdate if(!config.getBoolean("http", "receivepack", false)){
} config.setBoolean("http", null, "receivepack", true)
} config.save
} }
} }
FileUtils.deleteDirectory(Directory.getPluginCacheDir()) }
FileUtils.deleteDirectory(new File(Directory.PluginHome)) }
} }
}, }
new Version(2, 2), },
new Version(2, 1), Version(1, 2),
new Version(2, 0){ Version(1, 1),
override def update(conn: Connection): Unit = { Version(1, 0),
import eu.medsea.mimeutil.{MimeUtil2, MimeType} Version(0, 0)
)
val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") /**
* The head version of BitBucket.
super.update(conn) */
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => val headVersion = versions.head
while(rs.next){
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => /**
if(dir.exists && dir.isDirectory){ * The version file (GITBUCKET_HOME/version).
dir.listFiles.foreach { file => */
if(file.getName.indexOf('.') < 0){ lazy val versionFile = new File(GitBucketHome, "version")
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){ /**
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) * Returns the current version from the version file.
} */
} def getCurrentVersion(): Version = {
} if(versionFile.exists){
} FileUtils.readFileToString(versionFile, "UTF-8").split("\\.") match {
} case Array(majorVersion, minorVersion) => {
} versions.find { v =>
} v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
} }.getOrElse(Version(0, 0))
}, }
Version(1, 13), case _ => Version(0, 0)
Version(1, 12), }
Version(1, 11), } else Version(0, 0)
Version(1, 10), }
Version(1, 9),
Version(1, 8), }
Version(1, 7),
Version(1, 6), /**
Version(1, 5), * Update database schema automatically in the context initializing.
Version(1, 4), */
new Version(1, 3){ class AutoUpdateListener extends ServletContextListener {
override def update(conn: Connection): Unit = { import AutoUpdate._
super.update(conn) private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
// Fix wiki repository configuration
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => override def contextInitialized(event: ServletContextEvent): Unit = {
while(rs.next){ val datadir = event.getServletContext.getInitParameter("gitbucket.home")
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => if(datadir != null){
defining(git.getRepository.getConfig){ config => System.setProperty("gitbucket.home", datadir)
if(!config.getBoolean("http", "receivepack", false)){ }
config.setBoolean("http", null, "receivepack", true) org.h2.Driver.load()
config.save event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
}
} logger.debug("Start schema update")
} defining(getConnection(event.getServletContext)){ conn =>
} try {
} defining(getCurrentVersion()){ currentVersion =>
} if(currentVersion == headVersion){
}, logger.debug("No update")
Version(1, 2), } else if(!versions.contains(currentVersion)){
Version(1, 1), logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
Version(1, 0), } else {
Version(0, 0) versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
) FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit()
/** logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
* The head version of BitBucket. }
*/ }
val headVersion = versions.head } catch {
case ex: Throwable => {
/** logger.error("Failed to schema update", ex)
* The version file (GITBUCKET_HOME/version). ex.printStackTrace()
*/ conn.rollback()
lazy val versionFile = new File(GitBucketHome, "version") }
}
/** }
* Returns the current version from the version file. logger.debug("End schema update")
*/ }
def getCurrentVersion(): Version = {
if(versionFile.exists){ def contextDestroyed(sce: ServletContextEvent): Unit = {
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { // Nothing to do.
case Array(majorVersion, minorVersion) => { }
versions.find { v =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt private def getConnection(servletContext: ServletContext): Connection =
}.getOrElse(Version(0, 0)) DriverManager.getConnection(
} servletContext.getInitParameter("db.url"),
case _ => Version(0, 0) servletContext.getInitParameter("db.user"),
} servletContext.getInitParameter("db.password"))
} else Version(0, 0)
} }
}
/**
* Update database schema automatically in the context initializing.
*/
class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory
import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private val scheduler = StdSchedulerFactory.getDefaultScheduler
override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if(dataDir != null){
System.setProperty("gitbucket.home", dataDir)
}
org.h2.Driver.load()
val context = event.getServletContext
context.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
defining(getConnection(event.getServletContext)){ conn =>
logger.debug("Start schema update")
try {
defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){
logger.debug("No update")
} else if(!versions.contains(currentVersion)){
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
}
}
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
}
logger.debug("End schema update")
}
if(SystemSettingsService.enablePluginSystem){
getDatabase(context).withSession { implicit session =>
logger.debug("Starting plugin system...")
try {
plugin.PluginSystem.init()
scheduler.start()
PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
} catch {
case ex: Throwable => {
logger.error("Failed to initialize plugin system", ex)
ex.printStackTrace()
throw ex
}
}
}
}
}
def contextDestroyed(sce: ServletContextEvent): Unit = {
scheduler.shutdown()
}
private def getConnection(servletContext: ServletContext): Connection =
DriverManager.getConnection(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
private def getDatabase(servletContext: ServletContext): scala.slick.jdbc.JdbcBackend.Database =
slick.jdbc.JdbcBackend.Database.forURL(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
}

View File

@@ -3,7 +3,6 @@ package servlet
import javax.servlet._ import javax.servlet._
import javax.servlet.http._ import javax.servlet.http._
import service.{SystemSettingsService, AccountService, RepositoryService} import service.{SystemSettingsService, AccountService, RepositoryService}
import model._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
@@ -21,7 +20,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
def destroy(): Unit = {} def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
implicit val request = req.asInstanceOf[HttpServletRequest] val request = req.asInstanceOf[HttpServletRequest]
val response = res.asInstanceOf[HttpServletResponse] val response = res.asInstanceOf[HttpServletResponse]
val wrappedResponse = new HttpServletResponseWrapper(response){ val wrappedResponse = new HttpServletResponseWrapper(response){
@@ -39,12 +38,9 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
request.getHeader("Authorization") match { request.getHeader("Authorization") match {
case null => requireAuth(response) case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match { case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => getWritableUser(username, password, repository) match { case Array(username, password) if(isWritableUser(username, password, repository)) => {
case Some(account) => { request.setAttribute(Keys.Request.UserName, username)
request.setAttribute(Keys.Request.UserName, account.userName) chain.doFilter(req, wrappedResponse)
chain.doFilter(req, wrappedResponse)
}
case None => requireAuth(response)
} }
case _ => requireAuth(response) case _ => requireAuth(response)
} }
@@ -65,11 +61,10 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
} }
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo) private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
(implicit session: Session): Option[Account] =
authenticate(loadSystemSettings(), username, password) match { authenticate(loadSystemSettings(), username, password) match {
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
case _ => None case None => false
} }
private def requireAuth(response: HttpServletResponse): Unit = { private def requireAuth(response: HttpServletResponse): Unit = {

View File

@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig import javax.servlet.ServletConfig
import javax.servlet.ServletContext import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.HttpServletRequest
import util.{StringUtil, Keys, JGitUtil, Directory} import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._ import util.Implicits._
@@ -16,8 +16,6 @@ import service._
import WebHookService._ import WebHookService._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import service.IssuesService.IssueSearchCondition
import model.Session
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -25,7 +23,7 @@ import model.Session
* This servlet provides only Git repository functionality. * This servlet provides only Git repository functionality.
* Authentication is provided by [[servlet.BasicAuthenticationFilter]]. * Authentication is provided by [[servlet.BasicAuthenticationFilter]].
*/ */
class GitRepositoryServlet extends GitServlet with SystemSettingsService { class GitRepositoryServlet extends GitServlet {
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet]) private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
@@ -49,23 +47,11 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
super.init(config) super.init(config)
} }
override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = {
val agent = req.getHeader("USER-AGENT")
val index = req.getRequestURI.indexOf(".git")
if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){
// redirect for browsers
val paths = req.getRequestURI.substring(0, index).split("/")
res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last)
} else {
// response for git client
super.service(req, res)
}
}
} }
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
override def create(request: HttpServletRequest, db: Repository): ReceivePack = { override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
@@ -77,16 +63,12 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
val repository = paths(2).stripSuffix(".git") val repository = paths(2).replaceFirst("\\.git$", "")
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
if(!repository.endsWith(".wiki")){ if(!repository.endsWith(".wiki")){
defining(request) { implicit r => receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
} }
receivePack receivePack
} }
@@ -95,63 +77,43 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session) class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
existIds = JGitUtil.getAllCommitIds(git)
}
} catch {
case ex: Exception => {
logger.error(ex.toString, ex)
throw ex
}
}
}
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try { try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command => commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val commits = command.getType match {
case ReceiveCommand.Type.DELETE => Nil
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
}
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/") val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") {
Nil // Extract new commit and apply issue comment
val newCommits = if(commits.size > 1000){
val existIds = getAllCommitIds(owner, repository)
commits.flatMap { commit =>
if(!existIds.contains(commit.id)){
createIssueComment(commit)
Some(commit)
} else None
}
} else { } else {
command.getType match { commits.flatMap { commit =>
case ReceiveCommand.Type.DELETE => Nil if(!existsCommitId(owner, repository, commit.id)){
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) createIssueComment(commit)
Some(commit)
} else None
} }
} }
// Retrieve all issue count in the repository // batch insert all new commit id
val issueCount = insertAllCommitIds(owner, repository, newCommits.map(_.id))
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
// Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
val newCommits = commits.flatMap { commit =>
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
if (issueCount > 0) {
pushedIds.add(commit.id)
createIssueComment(commit)
// close issues
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
}
}
Some(commit)
} else None
}
// record activity // record activity
if(refName(1) == "heads"){ if(refName(1) == "heads"){
@@ -205,7 +167,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
private def createIssueComment(commit: CommitInfo) = { private def createIssueComment(commit: CommitInfo) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){ if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.committerEmailAddress).foreach { account => getAccountByMailAddress(commit.mailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
} }
} }
@@ -218,18 +180,14 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
private def updatePullRequests(branch: String) = private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){ if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)), using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) => git.fetch
oldGit.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString) .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true)) .setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
.call .call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName val commitIdTo = git.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit, updateCommitIdTo(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo)
pullreq.userName, pullreq.repositoryName, pullreq.branch,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
} }
} }
} }

View File

@@ -1,192 +0,0 @@
package servlet
import javax.servlet._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.apache.commons.io.IOUtils
import play.twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService}
import model.{Account, Session}
import util.{JGitUtil, Keys}
import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
import service.RepositoryService.RepositoryInfo
import plugin.Security._
class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
(req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => {
Database(req.getServletContext) withTransaction { implicit session =>
val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res)
}
}
}
}
}
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: Session): Boolean = {
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()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
if(authenticate(action.security, context)){
val result = try {
PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, context)
} finally {
PluginConnectionHolder.threadLocal.remove()
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
true
} getOrElse false
}
private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: Session): Boolean = {
val elements = path.split("/")
if(elements.length > 3){
val owner = elements(1)
val name = elements(2)
val remain = elements.drop(3).mkString("/", "/", "")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository =>
plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action =>
if(authenticate(action.security, context, repository)){
val result = try {
PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, context, repository)
} finally {
PluginConnectionHolder.threadLocal.remove()
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
true
}
} getOrElse false
} else false
}
private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
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")
val html = _root_.html.main("GitBucket", None)(Html(body))(context)
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
}
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
response.setContentType(rawData.contentType)
IOUtils.write(rawData.content, response.getOutputStream)
}
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write
implicit val formats = Serialization.formats(NoTypeHints)
val json = write(obj)
response.setContentType("application/json; charset=UTF-8")
IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream)
}
}

View File

@@ -1,16 +1,15 @@
package servlet package servlet
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener} import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
import org.apache.commons.io.FileUtils import app.FileUploadControllerBase
import util.Directory._
/** /**
* Removes session associated temporary files when session is destroyed. * Removes session associated temporary files when session is destroyed.
*/ */
class SessionCleanupListener extends HttpSessionListener { class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
def sessionCreated(se: HttpSessionEvent): Unit = {} def sessionCreated(se: HttpSessionEvent): Unit = {}
def sessionDestroyed(se: HttpSessionEvent): Unit = FileUtils.deleteDirectory(getTemporaryDir(se.getSession.getId)) def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
} }

View File

@@ -1,45 +1,38 @@
package servlet package servlet
import javax.servlet._ import javax.servlet._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import util.Keys
/**
/** * Controls the transaction with the open session in view pattern.
* Controls the transaction with the open session in view pattern. */
*/ class TransactionFilter extends Filter {
class TransactionFilter extends Filter {
private val logger = LoggerFactory.getLogger(classOf[TransactionFilter])
private val logger = LoggerFactory.getLogger(classOf[TransactionFilter])
def init(config: FilterConfig) = {}
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){
if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){ // assets don't need transaction
// assets don't need transaction chain.doFilter(req, res)
chain.doFilter(req, res) } else {
} else { Database(req.getServletContext) withTransaction {
Database(req.getServletContext) withTransaction { session => logger.debug("begin transaction")
logger.debug("begin transaction") chain.doFilter(req, res)
req.setAttribute(Keys.Request.DBSession, session) logger.debug("end transaction")
chain.doFilter(req, res) }
logger.debug("end transaction") }
} }
}
} }
} object Database {
def apply(context: ServletContext): scala.slick.session.Database =
object Database { scala.slick.session.Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"),
def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database = context.getInitParameter("db.password"))
slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"), }
context.getInitParameter("db.user"),
context.getInitParameter("db.password"))
def getSession(req: ServletRequest): slick.jdbc.JdbcBackend#Session =
req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session]
}

View File

@@ -1,133 +0,0 @@
package ssh
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
import org.slf4j.LoggerFactory
import java.io.{InputStream, OutputStream}
import util.ControlUtil._
import org.eclipse.jgit.api.Git
import util.Directory._
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.command.UnknownCommand
import servlet.{Database, CommitLogHook}
import service.{AccountService, RepositoryService, SystemSettingsService}
import org.eclipse.jgit.errors.RepositoryNotFoundException
import javax.servlet.ServletContext
import model.Session
object GitCommand {
val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
}
abstract class GitCommand(val context: ServletContext, val owner: String, val repoName: String) extends Command {
self: RepositoryService with AccountService =>
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
protected var err: OutputStream = null
protected var in: InputStream = null
protected var out: OutputStream = null
protected var callback: ExitCallback = null
protected def runTask(user: String)(implicit session: Session): Unit
private def newTask(user: String): Runnable = new Runnable {
override def run(): Unit = {
Database(context) withSession { implicit session =>
try {
runTask(user)
callback.onExit(0)
} catch {
case e: RepositoryNotFoundException =>
logger.info(e.getMessage)
callback.onExit(1, "Repository Not Found")
case e: Throwable =>
logger.error(e.getMessage, e)
callback.onExit(1)
}
}
}
}
override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER")
val thread = new Thread(newTask(user))
thread.start()
}
override def destroy(): Unit = {}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
}
override def setErrorStream(err: OutputStream): Unit = {
this.err = err
}
override def setOutputStream(out: OutputStream): Unit = {
this.out = out
}
override def setInputStream(in: InputStream): Unit = {
this.in = in
}
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
(implicit session: Session): Boolean =
getAccountByUserName(username) match {
case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account))
case None => false
}
}
class GitUploadPack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository
val upload = new UploadPack(repository)
upload.upload(in, out, err)
}
}
}
}
}
class GitReceivePack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
with SystemSettingsService with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
if(isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository
val receive = new ReceivePack(repository)
if(!repoName.endsWith(".wiki")){
val hook = new CommitLogHook(owner, repoName, user, baseUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
}
receive.receive(in, out, err)
}
}
}
}
}
class GitCommandFactory(context: ServletContext, baseUrl: String) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
logger.debug(s"command: $command")
command match {
case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(context, owner, repoName, baseUrl)
case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(context, owner, repoName, baseUrl)
case _ => new UnknownCommand(command)
}
}
}

View File

@@ -1,62 +0,0 @@
package ssh
import org.apache.sshd.common.Factory
import org.apache.sshd.server.{Environment, ExitCallback, Command}
import java.io.{OutputStream, InputStream}
import org.eclipse.jgit.lib.Constants
import service.SystemSettingsService
class NoShell extends Factory[Command] with SystemSettingsService {
override def create(): Command = new Command() {
private var in: InputStream = null
private var out: OutputStream = null
private var err: OutputStream = null
private var callback: ExitCallback = null
override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER")
val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
val message =
"""
| Welcome to
| _____ _ _ ____ _ _
| / ____| (_) | | | _ \ | | | |
| | | __ _ | |_ | |_) | _ _ ___ | | __ ___ | |_
| | | |_ | | | | __| | _ < | | | | / __| | |/ / / _ \ | __|
| | |__| | | | | |_ | |_) | | |_| | | (__ | < | __/ | |_
| \_____| |_| \__| |____/ \__,_| \___| |_|\_\ \___| \__|
|
| Successfully SSH Access.
| But interactive shell is disabled.
|
| Please use:
|
| git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message))
err.flush()
in.close()
out.close()
err.close()
callback.onExit(127)
}
override def destroy(): Unit = {}
override def setInputStream(in: InputStream): Unit = {
this.in = in
}
override def setOutputStream(out: OutputStream): Unit = {
this.out = out
}
override def setErrorStream(err: OutputStream): Unit = {
this.err = err
}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
}
}
}

View File

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

View File

@@ -1,70 +0,0 @@
package ssh
import javax.servlet.{ServletContext, ServletContextEvent, ServletContextListener}
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
import util.Directory
import service.SystemSettingsService
import java.util.concurrent.atomic.AtomicBoolean
object SshServer {
private val logger = LoggerFactory.getLogger(SshServer.getClass)
private val server = org.apache.sshd.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false)
private def configure(context: ServletContext, port: Int, baseUrl: String) = {
server.setPort(port)
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser"))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(context))
server.setCommandFactory(new GitCommandFactory(context, baseUrl))
server.setShellFactory(new NoShell)
}
def start(context: ServletContext, port: Int, baseUrl: String) = {
if(active.compareAndSet(false, true)){
configure(context, port, baseUrl)
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
}
}
def stop() = {
if(active.compareAndSet(true, false)){
server.stop(true)
logger.info("SSH Server is stopped.")
}
}
def isActive = active.get
}
/*
* Start a SSH Server Daemon
*
* How to use:
* git clone ssh://username@host_or_ip:29418/owner/repository_name.git
*/
class SshServerListener extends ServletContextListener with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[SshServerListener])
override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings()
if(settings.ssh){
settings.baseUrl match {
case None =>
logger.error("Could not start SshServer because the baseUrl is not configured.")
case Some(baseUrl) =>
SshServer.start(sce.getServletContext,
settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
}
}
}
override def contextDestroyed(sce: ServletContextEvent): Unit = {
if(loadSystemSettings().ssh){
SshServer.stop()
}
}
}

View File

@@ -1,36 +0,0 @@
package ssh
import java.security.PublicKey
import org.slf4j.LoggerFactory
import org.apache.commons.codec.binary.Base64
import org.eclipse.jgit.lib.Constants
import org.apache.sshd.common.util.{KeyUtils, Buffer}
object SshUtil {
private val logger = LoggerFactory.getLogger(SshUtil.getClass)
def str2PublicKey(key: String): Option[PublicKey] = {
// TODO RFC 4716 Public Key is not supported...
val parts = key.split(" ")
if (parts.size < 2) {
logger.debug(s"Invalid PublicKey Format: ${key}")
return None
}
try {
val encodedKey = parts(1)
val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey))
Some(new Buffer(decode).getRawPublicKey)
} catch {
case e: Throwable =>
logger.debug(e.getMessage, e)
None
}
}
def fingerPrint(key: String): Option[String] = str2PublicKey(key) match {
case Some(publicKey) => Some(KeyUtils.getFingerPrint(publicKey))
case None => None
}
}

View File

@@ -29,7 +29,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
/** /**
* Allows only the repository owner and administrators. * Allows only the repository owner and administrators.
*/ */
trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService => trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -40,9 +40,6 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists { member =>
member.userName == x.userName && member.isManager == true
}) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
@@ -109,7 +106,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
} }
/** /**
* Allows only the repository owner (or manager for group repository) and administrators. * Allows only the repository owner and administrators.
*/ */
trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
@@ -158,24 +155,3 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
} }
} }
} }
/**
* Allows only the group managers.
*/
trait GroupManagerAuthenticator { self: ControllerBase with AccountService =>
protected def managersOnly(action: => Any) = { authenticate(action) }
protected def managersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) }
private def authenticate(action: => Any) = {
{
defining(request.paths){ paths =>
context.loginAccount match {
case Some(x) if(getGroupMembers(paths(0)).exists { member =>
member.userName == x.userName && member.isManager
}) => action
case _ => Unauthorized()
}
}
}
}
}

View File

@@ -16,7 +16,7 @@ object ControlUtil {
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B = def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally { try f(resource) finally {
if(resource != null){ if(resource != null){
ignoring(classOf[Throwable]) { allCatch {
resource.close() resource.close()
} }
} }
@@ -37,4 +37,15 @@ object ControlUtil {
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T = def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release() try f(treeWalk) finally treeWalk.release()
// def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
// try {
// f(ref)
// } finally {
// val refUpdate = git.getRepository.updateRef(ref.getDestination)
// refUpdate.setForceUpdate(true)
// refUpdate.delete()
// }
// }
} }

View File

@@ -29,14 +29,24 @@ object Directory {
}).getAbsolutePath }).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = s"${GitBucketHome}/repositories" val RepositoryHome = s"${GitBucketHome}/repositories"
val DatabaseHome = s"${GitBucketHome}/data" val DatabaseHome = s"${GitBucketHome}/data"
val PluginHome = s"${GitBucketHome}/plugins" /**
* Repository names of the specified user.
val TemporaryHome = s"${GitBucketHome}/tmp" */
def getRepositories(owner: String): List[String] =
defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
if(dir.exists){
dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git")
}.map(_.getName.replaceFirst("\\.git$", "")).toList
} else {
Nil
}
}
/** /**
* Substance directory of the repository. * Substance directory of the repository.
@@ -44,33 +54,16 @@ object Directory {
def getRepositoryDir(owner: String, repository: String): File = def getRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.git") new File(s"${RepositoryHome}/${owner}/${repository}.git")
/**
* Directory for files which are attached to issue.
*/
def getAttachedDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}/issues")
/** /**
* Directory for uploaded files by the specified user. * Directory for uploaded files by the specified user.
*/ */
def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files") def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files")
/**
* Root of temporary directories for the upload file.
*/
def getTemporaryDir(sessionId: String): File =
new File(s"${TemporaryHome}/_upload/${sessionId}")
/** /**
* Root of temporary directories for the specified repository. * Root of temporary directories for the specified repository.
*/ */
def getTemporaryDir(owner: String, repository: String): File = def getTemporaryDir(owner: String, repository: String): File =
new File(s"${TemporaryHome}/${owner}/${repository}") new File(s"${GitBucketHome}/tmp/${owner}/${repository}")
/**
* Root of plugin cache directory. Plugin repositories are cloned into this directory.
*/
def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins")
/** /**
* Temporary directory which is used to create an archive to download repository contents. * Temporary directory which is used to create an archive to download repository contents.

View File

@@ -4,10 +4,9 @@ import org.apache.commons.io.FileUtils
import java.net.URLConnection import java.net.URLConnection
import java.io.File import java.io.File
import util.ControlUtil._ import util.ControlUtil._
import scala.util.Random
object FileUtil { object FileUtil {
def getMimeType(name: String): String = def getMimeType(name: String): String =
defining(URLConnection.getFileNameMap()){ fileNameMap => defining(URLConnection.getFileNameMap()){ fileNameMap =>
fileNameMap.getContentTypeFor(name) match { fileNameMap.getContentTypeFor(name) match {
@@ -27,12 +26,32 @@ object FileUtil {
} }
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/") def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000) def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0) def isText(content: Array[Byte]): Boolean = !content.contains(0)
def generateFileId: String = System.currentTimeMillis + Random.alphanumeric.take(10).mkString // def createZipFile(dest: File, dir: File): Unit = {
// def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
// dir.listFiles.map { file =>
// if(file.isFile){
// out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
// out.write(FileUtils.readFileToByteArray(file))
// out.closeArchiveEntry
// } else if(file.isDirectory){
// addDirectoryToZip(out, file, path + "/" + file.getName)
// }
// }
// }
//
// using(new ZipArchiveOutputStream(dest)){ out =>
// addDirectoryToZip(out, dir, dir.getName)
// }
// }
def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
if(i >= 0) path.substring(i + 1) else path
}
def getExtension(name: String): String = def getExtension(name: String): String =
name.lastIndexOf('.') match { name.lastIndexOf('.') match {

View File

@@ -2,8 +2,6 @@ package util
import scala.util.matching.Regex import scala.util.matching.Regex
import scala.util.control.Exception._ import scala.util.control.Exception._
import slick.jdbc.JdbcBackend
import servlet.Database
import javax.servlet.http.{HttpSession, HttpServletRequest} import javax.servlet.http.{HttpSession, HttpServletRequest}
/** /**
@@ -11,9 +9,6 @@ import javax.servlet.http.{HttpSession, HttpServletRequest}
*/ */
object Implicits { object Implicits {
// Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit class RichSeq[A](seq: Seq[A]) { implicit class RichSeq[A](seq: Seq[A]) {
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)

View File

@@ -4,7 +4,6 @@ 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._
@@ -12,20 +11,17 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._ 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.MissingObjectException
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
/** /**
* Provides complex JGit operations. * Provides complex JGit operations.
*/ */
object JGitUtil { object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
/** /**
* The repository data. * The repository data.
* *
@@ -36,11 +32,7 @@ object JGitUtil {
* @param branchList the list of branch names * @param branchList the list of branch names
* @param tags the list of tags * @param tags the list of tags
*/ */
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){ case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo])
def this(owner: String, name: String, baseUrl: String) = {
this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil)
}
}
/** /**
* The file data for the file list of the repository viewer. * The file data for the file list of the repository viewer.
@@ -48,55 +40,49 @@ object JGitUtil {
* @param id the object id * @param id the object id
* @param isDirectory whether is it directory * @param isDirectory whether is it directory
* @param name the file (or directory) name * @param name the file (or directory) name
* @param time the last modified time
* @param message the last commit message * @param message the last commit message
* @param commitId the last commit id * @param commitId the last commit id
* @param time the last modified time * @param committer the last committer name
* @param author the last committer name
* @param mailAddress the committer's mail address * @param mailAddress the committer's mail address
* @param linkUrl the url of submodule
*/ */
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, message: String, commitId: String, case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
time: Date, author: String, mailAddress: String, linkUrl: Option[String]) committer: String, mailAddress: String)
/** /**
* The commit data. * The commit data.
* *
* @param id the commit id * @param id the commit id
* @param time the commit time
* @param committer the committer name
* @param mailAddress the mail address of the committer
* @param shortMessage the short message * @param shortMessage the short message
* @param fullMessage the full message * @param fullMessage the full message
* @param parents the list of parent commit id * @param parents the list of parent commit id
* @param authorTime the author time
* @param authorName the author name
* @param authorEmailAddress the mail address of the author
* @param commitTime the commit time
* @param committerName the committer name
* @param committerEmailAddress the mail address of the committer
*/ */
case class CommitInfo(id: String, shortMessage: String, fullMessage: String, parents: List[String], case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String,
authorTime: Date, authorName: String, authorEmailAddress: String, shortMessage: String, fullMessage: String, parents: List[String]){
commitTime: Date, committerName: String, committerEmailAddress: String){
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this( def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
rev.getName, rev.getName,
rev.getShortMessage,
rev.getFullMessage,
rev.getParents().map(_.name).toList,
rev.getAuthorIdent.getWhen,
rev.getAuthorIdent.getName,
rev.getAuthorIdent.getEmailAddress,
rev.getCommitterIdent.getWhen, rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName, rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress) rev.getCommitterIdent.getEmailAddress,
rev.getShortMessage,
rev.getFullMessage,
rev.getParents().map(_.name).toList)
val summary = getSummaryMessage(fullMessage, shortMessage) val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
val description = defining(fullMessage.trim.indexOf("\n")){ i => val description = defining(fullMessage.trim.indexOf("\n")){ i =>
if(i >= 0){ if(i >= 0){
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} else None } else None
} }
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
} }
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -106,14 +92,8 @@ object JGitUtil {
* *
* @param viewType "image", "large" or "other" * @param viewType "image", "large" or "other"
* @param content the string content * @param content the string content
* @param charset the character encoding
*/ */
case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]){ case class ContentInfo(viewType: String, content: Option[String])
/**
* the line separator of this content ("LF" or "CRLF")
*/
val lineSeparator: String = if(content.exists(_.indexOf("\r\n") >= 0)) "CRLF" else "LF"
}
/** /**
* The tag data. * The tag data.
@@ -124,15 +104,6 @@ object JGitUtil {
*/ */
case class TagInfo(name: String, time: Date, id: String) case class TagInfo(name: String, time: Date, id: String)
/**
* The submodule data
*
* @param name the module name
* @param path the path in the repository
* @param url the repository url of this module
*/
case class SubmoduleInfo(name: String, path: String, url: String)
/** /**
* Returns RevCommit from the commit or tag id. * Returns RevCommit from the commit or tag id.
* *
@@ -157,7 +128,7 @@ object JGitUtil {
using(Git.open(getRepositoryDir(owner, repository))){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
RepositoryInfo( RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -165,12 +136,12 @@ object JGitUtil {
commitCount, commitCount,
// branches // branches
git.branchList.call.asScala.map { ref => git.branchList.call.asScala.map { ref =>
ref.getName.stripPrefix("refs/heads/") ref.getName.replaceFirst("^refs/heads/", "")
}.toList, }.toList,
// tags // tags
git.tagList.call.asScala.map { ref => git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId) val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName) TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList }.toList
) )
} catch { } catch {
@@ -181,7 +152,7 @@ object JGitUtil {
} }
} }
} }
/** /**
* Returns the file list of the specified path. * Returns the file list of the specified path.
* *
@@ -191,73 +162,55 @@ 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] = {
var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, 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)
val treeWalk = if (path == ".") { using(new TreeWalk(git.getRepository)){ treeWalk =>
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
treeWalk if(path != "."){
} else { treeWalk.setRecursive(true)
val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree) treeWalk.setFilter(new TreeFilter(){
treeWalk.enterSubtree()
treeWalk
}
using(treeWalk) { treeWalk => var stopRecursive = false
while (treeWalk.next()) {
// submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
} else None
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) def include(walker: TreeWalk): Boolean = {
} val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
list = list.map(tuple => true
if (tuple._2 != FileMode.TREE) } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
tuple stopRecursive = true
else treeWalk.setRecursive(false)
simplifyPath(tuple) true
) } else {
false
@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)) def shouldBeRecursive(): Boolean = !stopRecursive
tuple
else override def clone: TreeFilter = return this
simplifyPath(list(0)) })
}
while (treeWalk.next()) {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
} }
} }
} }
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name, linkUrl) => list.map { case (objectId, fileMode, path, name) =>
defining(commits(path)){ commit => FileInfo(
FileInfo( objectId,
objectId, fileMode == FileMode.TREE,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, name,
name, commits(path).getCommitterIdent.getWhen,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage), commits(path).getShortMessage,
commit.getName, commits(path).getName,
commit.getAuthorIdent.getWhen, commits(path).getCommitterIdent.getName,
commit.getAuthorIdent.getName, commits(path).getCommitterIdent.getEmailAddress)
commit.getAuthorIdent.getEmailAddress,
linkUrl)
}
}.sortWith { (file1, file2) => }.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match { (file1.isDirectory, file2.isDirectory) match {
case (true , false) => true case (true , false) => true
@@ -266,18 +219,7 @@ object JGitUtil {
} }
}.toList }.toList
} }
/**
* Returns the first line of the commit message.
*/
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
defining(fullMessage.trim.indexOf("\n")){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
}
/** /**
* Returns the commit list of the specified branch. * Returns the commit list of the specified branch.
* *
@@ -383,6 +325,27 @@ object JGitUtil {
}.toMap }.toMap
} }
/**
* Get object content of the given id as String from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param large if false then returns None for the large file
* @return the object or None if object does not exist
*/
def getContent(git: Git, id: ObjectId, large: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(large == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
}
}
} catch {
case e: MissingObjectException => None
}
/** /**
* Returns the tuple of diff of the given commit and the previous commit id. * Returns the tuple of diff of the given commit and the previous commit id.
*/ */
@@ -401,12 +364,7 @@ object JGitUtil {
if(commits.length >= 2){ if(commits.length >= 2){
// not initial commit // not initial commit
val oldCommit = if(revCommit.getParentCount >= 2) { val oldCommit = commits(1)
// merge commit
revCommit.getParents.head
} else {
commits(1)
}
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName)) (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
} else { } else {
@@ -419,7 +377,7 @@ object JGitUtil {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None) DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
} else { } else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
})) }))
} }
(buffer.toList, None) (buffer.toList, None)
@@ -442,8 +400,8 @@ object JGitUtil {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else { } else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
} }
}.toList }.toList
} }
@@ -515,7 +473,7 @@ object JGitUtil {
} }
def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId, def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
ref: String, fullName: String, mailAddress: String, message: String): ObjectId = { fullName: String, mailAddress: String, message: String): String = {
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))
@@ -529,149 +487,11 @@ object JGitUtil {
inserter.flush() inserter.flush()
inserter.release() inserter.release()
val refUpdate = git.getRepository.updateRef(ref) val refUpdate = git.getRepository.updateRef(Constants.HEAD)
refUpdate.setNewObjectId(newHeadId) refUpdate.setNewObjectId(newHeadId)
refUpdate.update() refUpdate.update()
newHeadId newHeadId.getName
}
/**
* Read submodule information from .gitmodules
*/
def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = {
val repository = git.getRepository
getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
(try {
val config = new BlobBasedConfig(repository.getConfig(), bytes)
config.getSubsections("submodule").asScala.map { module =>
val path = config.getString("submodule", module, "path")
val url = config.getString("submodule", module, "url")
SubmoduleInfo(module, path, url)
}
} catch {
case e: ConfigInvalidException => {
logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e)
Nil
}
}).toList
} getOrElse Nil
}
/**
* Get object content of the given path as byte array from the Git repository.
*
* @param git the Git object
* @param revTree the rev tree
* @param path the path
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = {
@scala.annotation.tailrec
def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => getPathObjectId(path, walk)
case false => None
}
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
} flatMap { objectId =>
getContentFromId(git, objectId, fetchLargeFile)
}
}
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
} else {
// binary
ContentInfo("binary", None, None)
}
} else {
// image or large
ContentInfo(viewer, None, None)
}
}
/**
* Get object content of the given object id as byte array from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
}
}
} catch {
case e: MissingObjectException => None
}
/**
* Returns all commit id in the specified repository.
*/
def getAllCommitIds(git: Git): Seq[String] = if(isEmpty(git)) {
Nil
} else {
val existIds = new scala.collection.mutable.ListBuffer[String]()
val i = git.log.all.call.iterator
while(i.hasNext){
existIds += i.next.name
}
existIds.toSeq
}
def processTree(git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => Unit) = {
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(id))
treeWalk.setRecursive(true)
while(treeWalk.next){
f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser]))
}
}
}
}
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
def getForkedCommitId(oldGit: Git, newGit: Git,
userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
defining(getAllCommitIds(oldGit)){ existIds =>
getCommitLogs(newGit, requestBranch, true) { commit =>
existIds.contains(commit.name) && getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
}
/**
* Returns the last modified commit of specified path
* @param git the Git object
* @param startCommit the search base commit id
* @param path the path of target file or directory
* @return the last modified commit of specified path
*/
def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = {
return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
} }
} }

View File

@@ -61,11 +61,6 @@ object Keys {
*/ */
object Request { object Request {
/**
* Request key for the Slick Session.
*/
val DBSession = "DB_SESSION"
/** /**
* Request key for the Ajax request flag. * Request key for the Ajax request flag.
*/ */

View File

@@ -7,7 +7,6 @@ import java.security.Security
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import service.SystemSettingsService.Ldap import service.SystemSettingsService.Ldap
import scala.annotation.tailrec import scala.annotation.tailrec
import model.Account
/** /**
* Utility for LDAP authentication. * Utility for LDAP authentication.
@@ -17,26 +16,6 @@ object LDAPUtil {
private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3 private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
private val logger = LoggerFactory.getLogger(getClass().getName()) private val logger = LoggerFactory.getLogger(getClass().getName())
private val LDAP_DUMMY_MAL = "@ldap-devnull"
/**
* Returns true if mail address ends with "@ldap-devnull"
*/
def isDummyMailAddress(account: Account): Boolean = {
account.mailAddress.endsWith(LDAP_DUMMY_MAL)
}
/**
* Creates dummy address (userName@ldap-devnull) for LDAP login.
*
* If mail address is not managed in LDAP server, GitBucket stores this dummy address in first LDAP login.
* GitBucket does not send any mails to this dummy address. And these users must input their mail address
* at the first step after LDAP authentication.
*/
def createDummyMailAddress(userName: String): String = {
userName + LDAP_DUMMY_MAL
}
/** /**
* Try authentication by LDAP using given configuration. * Try authentication by LDAP using given configuration.
* Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage). * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
@@ -51,7 +30,7 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""), keystore = ldapSettings.keystore.getOrElse(""),
error = "System LDAP authentication failed." error = "System LDAP authentication failed."
){ conn => ){ conn =>
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute, ldapSettings.additionalFilterCondition) match { findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password) case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
case None => Left("User does not exist.") case None => Left("User does not exist.")
} }
@@ -68,34 +47,18 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""), keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed." error = "User LDAP Authentication Failed."
){ conn => ){ conn =>
if(ldapSettings.mailAttribute.getOrElse("").isEmpty) { findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
Right(LDAPUserInfo( case Some(mailAddress) => Right(LDAPUserInfo(
userName = userName, userName = userName,
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute) findFullName(conn, userDN, fullNameAttribute)
}.getOrElse(userName), }.getOrElse(userName),
mailAddress = createDummyMailAddress(userName))) mailAddress = mailAddress))
} else { case None => Left("Can't find mail address.")
findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute.get) match {
case Some(mailAddress) => Right(LDAPUserInfo(
userName = getUserNameFromMailAddress(userName),
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
}.getOrElse(userName),
mailAddress = mailAddress))
case None => Left("Can't find mail address.")
}
} }
} }
} }
private def getUserNameFromMailAddress(userName: String): String = {
(userName.indexOf('@') match {
case i if i >= 0 => userName.substring(0, i)
case i => userName
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
}
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String) private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
(f: LDAPConnection => Either[String, A]): Either[String, A] = { (f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) { if (tls) {
@@ -142,7 +105,7 @@ object LDAPUtil {
/** /**
* Search a specified user and returns userDN if exists. * Search a specified user and returns userDN if exists.
*/ */
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String, additionalFilterCondition: Option[String]): Option[String] = { private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
@tailrec @tailrec
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = { def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
if(results.hasMore){ if(results.hasMore){
@@ -155,26 +118,20 @@ object LDAPUtil {
entries.flatten entries.flatten
} }
} }
getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false)).collectFirst {
val filterCond = additionalFilterCondition.getOrElse("") match {
case "" => userNameAttribute + "=" + userName
case x => "(&(" + x + ")(" + userNameAttribute + "=" + userName + "))"
}
getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, filterCond, null, false)).collectFirst {
case x => x.getDN case x => x.getDN
} }
} }
private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] = private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else None } else None
} }
private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] = private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue) Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None } else None

View File

@@ -1,116 +1,116 @@
package util package util
import scala.concurrent._ import scala.concurrent._
import ExecutionContext.Implicits.global import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail} import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import app.Context import app.Context
import model.Session import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} import servlet.Database
import servlet.Database import SystemSettingsService.Smtp
import SystemSettingsService.Smtp import _root_.util.ControlUtil.defining
import _root_.util.ControlUtil.defining
trait Notifier extends RepositoryService with AccountService with IssuesService {
trait Notifier extends RepositoryService with AccountService with IssuesService { def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) (msg: String => String)(implicit context: Context): Unit
(msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) =
protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit session: Session, context: Context) = (
( // individual repository's owner
// individual repository's owner issue.userName ::
issue.userName :: // collaborators
// collaborators getCollaborators(issue.userName, issue.repositoryName) :::
getCollaborators(issue.userName, issue.repositoryName) ::: // participants
// participants issue.openedUserName ::
issue.openedUserName :: getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) )
) .distinct
.distinct .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) )
}
}
object Notifier {
object Notifier { // TODO We want to be able to switch to mock.
// TODO We want to be able to switch to mock. def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { case settings if settings.notification => new Mailer(settings.smtp.get)
case settings if settings.notification => new Mailer(settings.smtp.get) case _ => new MockMailer
case _ => new MockMailer }
}
def msgIssue(url: String) = (content: String) => s"""
def msgIssue(url: String) = (content: String) => s""" |${content}<br/>
|${content}<br/> |--<br/>
|--<br/> |<a href="${url}">View it on GitBucket</a>
|<a href="${url}">View it on GitBucket</a> """.stripMargin
""".stripMargin
def msgPullRequest(url: String) = (content: String) => s"""
def msgPullRequest(url: String) = (content: String) => s""" |${content}<hr/>
|${content}<hr/> |View, comment on, or merge it at:<br/>
|View, comment on, or merge it at:<br/> |<a href="${url}">${url}</a>
|<a href="${url}">${url}</a> """.stripMargin
""".stripMargin
def msgComment(url: String) = (content: String) => s"""
def msgComment(url: String) = (content: String) => s""" |${content}<br/>
|${content}<br/> |--<br/>
|--<br/> |<a href="${url}">View it on GitBucket</a>
|<a href="${url}">View it on GitBucket</a> """.stripMargin
""".stripMargin
def msgStatus(url: String) = (content: String) => s"""
def msgStatus(url: String) = (content: String) => s""" |${content} <a href="${url}">#${url split('/') last}</a>
|${content} <a href="${url}">#${url split('/') last}</a> """.stripMargin
""".stripMargin }
}
class Mailer(private val smtp: Smtp) extends Notifier {
class Mailer(private val smtp: Smtp) extends Notifier { private val logger = LoggerFactory.getLogger(classOf[Mailer])
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) (msg: String => String)(implicit context: Context) = {
(msg: String => String)(implicit context: Context) = { val database = Database(context.request.getServletContext)
val database = Database(context.request.getServletContext)
val f = future {
val f = Future { // TODO Can we use the Database Session in other than Transaction Filter?
database withSession { implicit session => database withSession {
getIssue(r.owner, r.name, issueId.toString) foreach { issue => getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
defining( defining(
s"[${r.name}] ${issue.title} (#${issueId})" -> s"[${r.name}] ${issue.title} (#${issueId})" ->
msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) => msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
recipients(issue) { to => recipients(issue) { to =>
val email = new HtmlEmail val email = new HtmlEmail
email.setHostName(smtp.host) email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get) email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user => smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
} }
smtp.ssl.foreach { ssl => smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl) email.setSSLOnConnect(ssl)
} }
smtp.fromAddress smtp.fromAddress
.map (_ -> smtp.fromName.orNull) .map (_ -> smtp.fromName.orNull)
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) => .foreach { case (address, name) =>
email.setFrom(address, name) email.setFrom(address, name)
} }
email.setCharset("UTF-8") email.setCharset("UTF-8")
email.setSubject(subject) email.setSubject(subject)
email.setHtmlMsg(msg) email.setHtmlMsg(msg)
email.addTo(to).send email.addTo(to).send
} }
} }
} }
} }
"Notifications Successful." "Notifications Successful."
} }
f onSuccess { f onSuccess {
case s => logger.debug(s) case s => logger.debug(s)
} }
f onFailure { f onFailure {
case t => logger.error("Notifications Failed.", t) case t => logger.error("Notifications Failed.", t)
} }
} }
} }
class MockMailer extends Notifier { class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit = {} (msg: String => String)(implicit context: Context): Unit = {}
} }

View File

@@ -31,7 +31,7 @@ object StringUtil {
/** /**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]]. * Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
* And if given bytes contains UTF-8 BOM, it's removed from returned string. * And if given bytes contains UTF-8 BOM, it's removed from returned string..
*/ */
def convertFromByteArray(content: Array[Byte]): String = def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content)) IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
@@ -47,37 +47,12 @@ object StringUtil {
} }
/** /**
* Converts line separator in the given content. * Extract issue id like ````#issueId``` from the given message.
*
* @param content the content
* @param lineSeparator "LF" or "CRLF"
* @return the converted content
*/
def convertLineSeparator(content: String, lineSeparator: String): String = {
val lf = content.replace("\r\n", "\n").replace("\r", "\n")
if(lineSeparator == "CRLF"){
lf.replace("\n", "\r\n")
} else {
lf
}
}
/**
* Extract issue id like ```#issueId``` from the given message.
* *
*@param message the message which may contains issue id *@param message the message which may contains issue id
* @return the iterator of issue id * @return the iterator of issue id
*/ */
def extractIssueId(message: String): Iterator[String] = def extractIssueId(message: String): Iterator[String] =
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2)) "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map { matchData => matchData.group(2) }
/**
* Extract close issue id like ```close #issueId ``` from the given message.
*
* @param message the message which may contains close command
* @return the iterator of issue id
*/
def extractCloseId(message: String): Iterator[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
} }

View File

@@ -10,7 +10,7 @@ trait Validations {
*/ */
def identifier: Constraint = new Constraint(){ def identifier: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(!value.matches("[a-zA-Z0-9\\-_.]+")){ if(!value.matches("^[a-zA-Z0-9\\-_.]+$")){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.") Some(s"${name} starts with invalid character.")

View File

@@ -1,7 +1,7 @@
package view package view
import service.RequestCache import service.RequestCache
import play.twirl.api.Html import twirl.api.Html
import util.StringUtil import util.StringUtil
trait AvatarImageProvider { self: RequestCache => trait AvatarImageProvider { self: RequestCache =>
@@ -16,8 +16,8 @@ trait AvatarImageProvider { self: RequestCache =>
val src = if(mailAddress.isEmpty){ val src = if(mailAddress.isEmpty){
// by user name // by user name
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
if(account.image.isEmpty && context.settings.gravatar){ if(account.image.isEmpty && getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else { } else {
s"""${context.path}/${account.userName}/_avatar""" s"""${context.path}/${account.userName}/_avatar"""
} }
@@ -27,14 +27,14 @@ trait AvatarImageProvider { self: RequestCache =>
} else { } else {
// by mail address // by mail address
getAccountByMailAddress(mailAddress).map { account => getAccountByMailAddress(mailAddress).map { account =>
if(account.image.isEmpty && context.settings.gravatar){ if(account.image.isEmpty && getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else { } else {
s"""${context.path}/${account.userName}/_avatar""" s"""${context.path}/${account.userName}/_avatar"""
} }
} getOrElse { } getOrElse {
if(context.settings.gravatar){ if(getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
} else { } else {
s"""${context.path}/_unknown/_avatar""" s"""${context.path}/_unknown/_avatar"""
} }
@@ -42,9 +42,9 @@ trait AvatarImageProvider { self: RequestCache =>
} }
if(tooltip){ if(tooltip){
Html(s"""<img src="${src}" class="${if(size > 20){"avatar"} else {"avatar-mini"}}" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""") Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else { } else {
Html(s"""<img src="${src}" class="${if(size > 20){"avatar"} else {"avatar-mini"}}" style="width: ${size}px; height: ${size}px;" />""") Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
} }
} }

View File

@@ -45,7 +45,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
(text, text) (text, text)
} }
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page) val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){ if(getWikiPage(repository.owner, repository.name, page).isDefined){
new Rendering(url, label) new Rendering(url, label)
@@ -88,10 +88,8 @@ class GitBucketHtmlSerializer(
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache { ) with LinkConverter with RequestCache {
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = { override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url, true)).print("\">") printer.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/>")
.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')
@@ -102,23 +100,11 @@ class GitBucketHtmlSerializer(
printer.print('>').print(rendering.text).print("</a>") printer.print('>').print(rendering.text).print("</a>")
} }
private def fixUrl(url: String, isImage: Boolean = false): String = { private def fixUrl(url: String): String = {
if(!enableWikiLink){ if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://")){
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){ url
url
} else 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.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
} }
} }

View File

@@ -1,7 +1,7 @@
package view package view
import java.util.{Date, TimeZone} import java.util.Date
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import play.twirl.api.Html import twirl.api.Html
import util.StringUtil import util.StringUtil
import service.RequestCache import service.RequestCache
@@ -15,15 +15,6 @@ 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)
/**
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
*/
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".
*/ */
@@ -36,14 +27,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def plural(count: Int, singular: String, plural: String = ""): String = def plural(count: Int, singular: String, plural: String = ""): String =
if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] =
Seq(
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context))
)
def renderableSuffixes: Seq[String] = renderersBySuffix.map(_._1)
/** /**
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
@@ -51,21 +34,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
val fileNameLower = filePath.reverse.head.toLowerCase
renderersBySuffix.find { case (suffix, _) => fileNameLower.endsWith(suffix) } match {
case Some((_, handler)) => handler(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context)
case None => Html(
s"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
}
}
/** /**
* Returns &lt;img&gt; which displays the avatar icon for the given user name. * Returns &lt;img&gt; which displays the avatar icon for the given user name.
* This method looks up Gravatar if avatar icon has not been configured in user settings. * This method looks up Gravatar if avatar icon has not been configured in user settings.
@@ -78,7 +46,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* This method looks up Gravatar if avatar icon has not been configured in user settings. * This method looks up Gravatar if avatar icon has not been configured in user settings.
*/ */
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html = def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
getAvatarImageHtml(commit.authorName, size, commit.authorEmailAddress) getAvatarImageHtml(commit.committer, size, commit.mailAddress)
/** /**
* Converts commit id, issue id and username to the link. * Converts commit id, issue id and username to the link.
@@ -167,44 +135,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/ */
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/**
* Returns file type for AceEditor.
*/
def editorType(fileName: String): String = {
fileName.toLowerCase match {
case x if(x.endsWith(".bat")) => "batchfile"
case x if(x.endsWith(".java")) => "java"
case x if(x.endsWith(".scala")) => "scala"
case x if(x.endsWith(".js")) => "javascript"
case x if(x.endsWith(".css")) => "css"
case x if(x.endsWith(".md")) => "markdown"
case x if(x.endsWith(".html")) => "html"
case x if(x.endsWith(".xml")) => "xml"
case x if(x.endsWith(".c")) => "c_cpp"
case x if(x.endsWith(".cpp")) => "c_cpp"
case x if(x.endsWith(".coffee")) => "coffee"
case x if(x.endsWith(".ejs")) => "ejs"
case x if(x.endsWith(".hs")) => "haskell"
case x if(x.endsWith(".json")) => "json"
case x if(x.endsWith(".jsp")) => "jsp"
case x if(x.endsWith(".jsx")) => "jsx"
case x if(x.endsWith(".cl")) => "lisp"
case x if(x.endsWith(".clojure")) => "lisp"
case x if(x.endsWith(".lua")) => "lua"
case x if(x.endsWith(".php")) => "php"
case x if(x.endsWith(".py")) => "python"
case x if(x.endsWith(".rdoc")) => "rdoc"
case x if(x.endsWith(".rhtml")) => "rhtml"
case x if(x.endsWith(".ruby")) => "ruby"
case x if(x.endsWith(".sh")) => "sh"
case x if(x.endsWith(".sql")) => "sql"
case x if(x.endsWith(".tcl")) => "tcl"
case x if(x.endsWith(".vbs")) => "vbscript"
case x if(x.endsWith(".yml")) => "yaml"
case _ => "plain_text"
}
}
/** /**
* Implicit conversion to add mkHtml() to Seq[Html]. * Implicit conversion to add mkHtml() to Seq[Html].
*/ */

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