mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 10:45:51 +02:00
Compare commits
309 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db5395ddbc | ||
|
|
1e8224536b | ||
|
|
a846c77c7e | ||
|
|
29812f4a82 | ||
|
|
a863951d97 | ||
|
|
146be677ba | ||
|
|
03b5f7feb8 | ||
|
|
6d54361a6d | ||
|
|
f440421ed1 | ||
|
|
e57464fc5e | ||
|
|
2a4b0f5ddb | ||
|
|
bb66e2201f | ||
|
|
4dc60e887f | ||
|
|
f6eb2e2dc8 | ||
|
|
9ecc10ab21 | ||
|
|
d7037a43c6 | ||
|
|
2471b8dfe0 | ||
|
|
0430cb49f9 | ||
|
|
7811926779 | ||
|
|
9bb66a4297 | ||
|
|
70772f0d74 | ||
|
|
728b00e4c3 | ||
|
|
97008ef984 | ||
|
|
6b86406e94 | ||
|
|
4252c364a4 | ||
|
|
4f4bc0321b | ||
|
|
6ecabe4588 | ||
|
|
93fa8484c5 | ||
|
|
ff2e55e82c | ||
|
|
259637ce3c | ||
|
|
743b9b759a | ||
|
|
73ba0b348b | ||
|
|
e93769cc81 | ||
|
|
68f9739eed | ||
|
|
c3d25b7a71 | ||
|
|
aaa582ff1a | ||
|
|
debc798aec | ||
|
|
6042f0e1e0 | ||
|
|
e10d02f45c | ||
|
|
aebf4ff728 | ||
|
|
1a2e89c9ed | ||
|
|
e10e2748b9 | ||
|
|
f422936e34 | ||
|
|
4e87f21405 | ||
|
|
dc2d79b16c | ||
|
|
88a3100563 | ||
|
|
8d3433a0e7 | ||
|
|
0fe30e5629 | ||
|
|
ea1e9037c4 | ||
|
|
24feeb17be | ||
|
|
6a7fc55572 | ||
|
|
cf047a8cee | ||
|
|
896420f8dc | ||
|
|
619f72d929 | ||
|
|
dc21e8388e | ||
|
|
642e8bbb7c | ||
|
|
3ee4143235 | ||
|
|
c136823170 | ||
|
|
92631fbfcf | ||
|
|
5a1b1a4485 | ||
|
|
3e82534c78 | ||
|
|
dd694d27b5 | ||
|
|
1900aefe32 | ||
|
|
2fe6b8c1e7 | ||
|
|
ecfaa0247a | ||
|
|
9a0cc9e043 | ||
|
|
b0360db105 | ||
|
|
0f9c95c15a | ||
|
|
8efd1da7e6 | ||
|
|
52ebba43d5 | ||
|
|
790eee7443 | ||
|
|
9f325290e8 | ||
|
|
93bf0a9a47 | ||
|
|
bdd0af21a9 | ||
|
|
aae5fe387b | ||
|
|
257c5aef51 | ||
|
|
3cae337487 | ||
|
|
779df30ec8 | ||
|
|
5609507991 | ||
|
|
1c24090c14 | ||
|
|
7da2c650d2 | ||
|
|
27fa9df2ee | ||
|
|
63c4e12259 | ||
|
|
1f66670819 | ||
|
|
a7b4f8de8d | ||
|
|
ad0d57fbf9 | ||
|
|
cfc594805b | ||
|
|
52461e673c | ||
|
|
a97edb7ef5 | ||
|
|
7a1c872861 | ||
|
|
0e5591017a | ||
|
|
a104157c9a | ||
|
|
ad244adbfa | ||
|
|
3721b328a6 | ||
|
|
dd688f48b7 | ||
|
|
296a0b2124 | ||
|
|
b9cc46e5ef | ||
|
|
375211fc30 | ||
|
|
b8b59f9dcd | ||
|
|
6760ff34ef | ||
|
|
c5de7811c4 | ||
|
|
82ef5457b0 | ||
|
|
d558476cd2 | ||
|
|
644701d995 | ||
|
|
1382d59206 | ||
|
|
b60e2c07c7 | ||
|
|
86f0307633 | ||
|
|
1db891a771 | ||
|
|
c9fa3291f5 | ||
|
|
e0f1658120 | ||
|
|
da105b7180 | ||
|
|
9c4f7cc530 | ||
|
|
d7eef8bd25 | ||
|
|
7b7c0e1eee | ||
|
|
2ae7798591 | ||
|
|
3f76453f34 | ||
|
|
8fbbe7f31e | ||
|
|
92a43b4f99 | ||
|
|
c128086778 | ||
|
|
cc4fb8bf79 | ||
|
|
c3ac0f3d9f | ||
|
|
dfa4816633 | ||
|
|
06978a4fc4 | ||
|
|
3a2ecf6896 | ||
|
|
b357d52ec5 | ||
|
|
f8b6b1ebf8 | ||
|
|
91bd9d1111 | ||
|
|
1ec825050d | ||
|
|
a6a08d13e9 | ||
|
|
9a47c4a990 | ||
|
|
5063294177 | ||
|
|
b14917e2c6 | ||
|
|
c1bbec2a1c | ||
|
|
6227a4643a | ||
|
|
5d3365a944 | ||
|
|
84ac2974fb | ||
|
|
c9a1515d1f | ||
|
|
5317ac5e03 | ||
|
|
9df1467ddf | ||
|
|
df79bd4515 | ||
|
|
cbb14f2ba8 | ||
|
|
1fe649e70f | ||
|
|
0d918add28 | ||
|
|
3926c98338 | ||
|
|
3bff6a1949 | ||
|
|
ec0c964ceb | ||
|
|
b4fd90c6d3 | ||
|
|
7dfd63cfa2 | ||
|
|
a562e5ca14 | ||
|
|
2885eef4ab | ||
|
|
087297d14c | ||
|
|
6e0fb95ac3 | ||
|
|
61e28146fb | ||
|
|
40d3f0ef9e | ||
|
|
99db825114 | ||
|
|
7341b377fe | ||
|
|
7f78a98de0 | ||
|
|
a64207f0ec | ||
|
|
d86f40e3a2 | ||
|
|
b74417f393 | ||
|
|
f5883abf04 | ||
|
|
02a367fd99 | ||
|
|
4870533710 | ||
|
|
8170a1b01d | ||
|
|
d1c6c763e2 | ||
|
|
0c683f7243 | ||
|
|
63de780527 | ||
|
|
c5ccbf2d1f | ||
|
|
8777535431 | ||
|
|
70192ce420 | ||
|
|
02d79cb16a | ||
|
|
78ca9b3f1a | ||
|
|
017631e337 | ||
|
|
f9078dff2c | ||
|
|
b66381d677 | ||
|
|
49bf88f7a7 | ||
|
|
f93ceaa91d | ||
|
|
0fe122dc63 | ||
|
|
3d251fa8ad | ||
|
|
af0b52448a | ||
|
|
f78cdb637d | ||
|
|
845f2d6faa | ||
|
|
525edbab80 | ||
|
|
c422b1c9a5 | ||
|
|
1043b13228 | ||
|
|
5e6c33df6c | ||
|
|
9541771703 | ||
|
|
f99d37cfad | ||
|
|
0cfe31ccd9 | ||
|
|
8fc1a5473b | ||
|
|
049b12b908 | ||
|
|
45f992b2bc | ||
|
|
9e2c66c341 | ||
|
|
2d0f59b6f2 | ||
|
|
fbba29e810 | ||
|
|
07a108760c | ||
|
|
b641bfb56a | ||
|
|
c65d80bc72 | ||
|
|
e0d266bf16 | ||
|
|
b62f7c5aee | ||
|
|
c89f04b926 | ||
|
|
ff8b4b4a88 | ||
|
|
07d63ae63a | ||
|
|
c0f5cb1641 | ||
|
|
50d84835cb | ||
|
|
8cdf4ef618 | ||
|
|
eff3a7acb4 | ||
|
|
716eddac7b | ||
|
|
9b15af3bb7 | ||
|
|
b732e0d55a | ||
|
|
d92a1cee1c | ||
|
|
10a40bfcaf | ||
|
|
af397ba150 | ||
|
|
c7a2ec8290 | ||
|
|
145c155ba5 | ||
|
|
6f9ef32d96 | ||
|
|
aa5b9dbbbd | ||
|
|
f11be44c02 | ||
|
|
4276c8f23e | ||
|
|
9e1352c8b1 | ||
|
|
d46589ad29 | ||
|
|
09b7e67c52 | ||
|
|
79e1abe624 | ||
|
|
3db3bf1b74 | ||
|
|
9bd1f0a492 | ||
|
|
7a2c82461e | ||
|
|
21f7888f55 | ||
|
|
e3fd564efd | ||
|
|
5cf96134d5 | ||
|
|
607c477e7d | ||
|
|
17920e1195 | ||
|
|
721454aa90 | ||
|
|
d870896cfb | ||
|
|
270eb7cf1d | ||
|
|
527fd94145 | ||
|
|
04e4572088 | ||
|
|
0961eb5976 | ||
|
|
0311359922 | ||
|
|
ec09adf03e | ||
|
|
b031103df8 | ||
|
|
7701521a2e | ||
|
|
0c683d4f75 | ||
|
|
200d095034 | ||
|
|
94576a876a | ||
|
|
0fa1922bb0 | ||
|
|
c557905858 | ||
|
|
31b21d74b1 | ||
|
|
153244c390 | ||
|
|
e97b5c3c89 | ||
|
|
374893a5ae | ||
|
|
17f581f654 | ||
|
|
590b431ec1 | ||
|
|
98266fe0e1 | ||
|
|
2e236e90ba | ||
|
|
c5aee0810c | ||
|
|
f13d757976 | ||
|
|
7a0a62af2d | ||
|
|
ceab1d2fd2 | ||
|
|
89601305f6 | ||
|
|
4600b5a3bf | ||
|
|
b620307983 | ||
|
|
891ca70ade | ||
|
|
9ed2a50d26 | ||
|
|
cbf615d699 | ||
|
|
97b1a0090d | ||
|
|
9078aa6d08 | ||
|
|
8677146a8d | ||
|
|
2c14dfb781 | ||
|
|
057c5f073c | ||
|
|
e902da6595 | ||
|
|
8b5414c8f7 | ||
|
|
c86ece4dc0 | ||
|
|
1f71619b6b | ||
|
|
5b34b9c795 | ||
|
|
99d15899f6 | ||
|
|
c114a8b507 | ||
|
|
0dd37c2481 | ||
|
|
b5d7c96bba | ||
|
|
a76792ced4 | ||
|
|
39091240ff | ||
|
|
0ccb753892 | ||
|
|
63dda84c8b | ||
|
|
7ba1f85d48 | ||
|
|
bb9a23fe0f | ||
|
|
8536824d7e | ||
|
|
78073babe4 | ||
|
|
521d15219c | ||
|
|
7469a3c349 | ||
|
|
153a32e340 | ||
|
|
f155d4f150 | ||
|
|
d683dd2c38 | ||
|
|
7ebba741a8 | ||
|
|
d10f683098 | ||
|
|
0270133ecf | ||
|
|
d7b479d97d | ||
|
|
4366c512fe | ||
|
|
229a773ed2 | ||
|
|
d882f20436 | ||
|
|
9d7235af20 | ||
|
|
c2eb53d154 | ||
|
|
7629e347df | ||
|
|
2764caae29 | ||
|
|
a87bd2a928 | ||
|
|
202c920064 | ||
|
|
a08316bba0 | ||
|
|
520e5ebb7a | ||
|
|
5d5a4cacb1 | ||
|
|
05a91565dc | ||
|
|
dc78dc9b0d |
80
README.md
80
README.md
@@ -1,12 +1,15 @@
|
||||
GitBucket
|
||||
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
|
||||
=========
|
||||
|
||||
GitBucket is the easily installable Github clone written with Scala.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
The current version of GitBucket provides a basic features below:
|
||||
|
||||
- Public / Private Git repository (http access only)
|
||||
- Repository viewer (some advanced features such as online file editing are not implemented)
|
||||
- Public / Private Git repository (http and ssh access)
|
||||
- Repository viewer and online file editing
|
||||
- Repository search (Code and Issues)
|
||||
- Wiki
|
||||
- Issues
|
||||
@@ -20,10 +23,9 @@ The current version of GitBucket provides a basic features below:
|
||||
|
||||
Following features are not implemented, but we will make them in the future release!
|
||||
|
||||
- File editing in repository viewer
|
||||
- Comment for the changeset
|
||||
- Network graph
|
||||
- Statics
|
||||
- Statistics
|
||||
- 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).
|
||||
@@ -35,21 +37,40 @@ Installation
|
||||
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.
|
||||
|
||||
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**.
|
||||
|
||||
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.
|
||||
|
||||
- --port=[NUMBER]
|
||||
- --prefix=[CONTEXTPATH]
|
||||
- --host=[HOSTNAME]
|
||||
- --https=true
|
||||
- --gitbucket.home=[DATA_DIR]
|
||||
|
||||
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
|
||||
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
|
||||
|
||||
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
|
||||
#### 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/`
|
||||
|
||||
Run the following commands in `Terminal` to
|
||||
@@ -59,6 +80,45 @@ Run the following commands in `Terminal` to
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 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
|
||||
- Bug fix
|
||||
|
||||
### 1.11 - 01 Mar 2014
|
||||
- Base URL for redirection, notification and repository URL box is configurable
|
||||
- Remove ```--https``` option because it's possible to substitute in the base url
|
||||
- Headline anchor is available for Markdown contents such as Wiki page
|
||||
- Improve H2 connectivity
|
||||
- Label is available for pull requests not only issues
|
||||
- Delete branch button is added
|
||||
- Repository icons are updated
|
||||
- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
|
||||
- Display reference to issue from others in comment list
|
||||
- Fix some bugs
|
||||
|
||||
### 1.10 - 01 Feb 2014
|
||||
- Rename repository
|
||||
- Transfer repository owner
|
||||
- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
|
||||
- Add LDAP display name attribute
|
||||
- Response performance improvement
|
||||
- Fix some bugs
|
||||
|
||||
### 1.9 - 28 Dec 2013
|
||||
- Display GITBUCKET_HOME on the system settings page
|
||||
- Fix some bugs
|
||||
@@ -74,8 +134,8 @@ Release Notes
|
||||
|
||||
### 1.7 - 26 Oct 2013
|
||||
- Support working on Java6 in embedded Jetty mode
|
||||
- Add ```--host``` option to bind specified host name in embedded Jetty mode
|
||||
- Add ```--https=true``` option to force https scheme when using embedded Jetty mode at the back of https proxy
|
||||
- Add `--host` option to bind specified host name in embedded Jetty mode
|
||||
- Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy
|
||||
- Add full name as user property
|
||||
- Change link color for absent Wiki pages
|
||||
- Add ZIP download button to the repository viewer tab
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
# Server port
|
||||
#GITBUCKET_PORT=8080
|
||||
|
||||
# Force HTTPS scheme
|
||||
#GITBUCKET_HTTPS=false
|
||||
|
||||
# Data directory (GITBUCKET_HOME/gitbucket)
|
||||
#GITBUCKET_HOME=/var/lib/gitbucket
|
||||
|
||||
|
||||
@@ -39,9 +39,6 @@ start() {
|
||||
if [ $GITBUCKET_HOST ]; then
|
||||
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
|
||||
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 &
|
||||
|
||||
964
etc/icons.svg
964
etc/icons.svg
@@ -2,6 +2,7 @@
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
@@ -16,7 +17,16 @@
|
||||
inkscape:version="0.48.4 r9939"
|
||||
sodipodi:docname="icons.svg">
|
||||
<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
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
@@ -24,18 +34,18 @@
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="629.30023"
|
||||
inkscape:cy="281.44758"
|
||||
inkscape:zoom="0.7"
|
||||
inkscape:cx="482.58197"
|
||||
inkscape:cy="-83.92636"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1-9"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-height="715"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true"
|
||||
inkscape:snap-global="false"
|
||||
inkscape:snap-grids="false"
|
||||
inkscape:snap-page="false"
|
||||
inkscape:snap-bbox="true"
|
||||
@@ -746,6 +756,948 @@
|
||||
d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z"
|
||||
id="rect2995-0-2-7-7"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="rect3083"
|
||||
width="170.93134"
|
||||
height="207.72536"
|
||||
x="38.526306"
|
||||
y="1299.8645" />
|
||||
<rect
|
||||
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="rect3083-7"
|
||||
width="171.86089"
|
||||
height="167.53221"
|
||||
x="38.061527"
|
||||
y="1300.4821" />
|
||||
<rect
|
||||
id="rect2995-0-4"
|
||||
y="1301.3412"
|
||||
x="42.553577"
|
||||
height="163.64935"
|
||||
width="29.769083"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-0"
|
||||
y="1321.9025"
|
||||
x="85.732407"
|
||||
height="17.555511"
|
||||
width="16.782965"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-0-9"
|
||||
y="1356.7848"
|
||||
x="85.732407"
|
||||
height="17.555511"
|
||||
width="16.782965"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-0-9-4"
|
||||
y="1391.6671"
|
||||
x="85.732407"
|
||||
height="17.555511"
|
||||
width="16.782965"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-0-9-4-8"
|
||||
y="1426.5494"
|
||||
x="85.732407"
|
||||
height="17.555511"
|
||||
width="16.782965"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-8"
|
||||
y="1482.7141"
|
||||
x="70.149086"
|
||||
height="30.541632"
|
||||
width="42.755199"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="path4002"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="235.71429"
|
||||
sodipodi:cy="1000.2193"
|
||||
sodipodi:r1="15.016997"
|
||||
sodipodi:r2="7.5084987"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
|
||||
transform="matrix(1.0346242,0,0,1.5150471,-165.95814,-2.7851671)"
|
||||
inkscape:transform-center-x="-2.5637799" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="path4002-2"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="235.71429"
|
||||
sodipodi:cy="1000.2193"
|
||||
sodipodi:r1="15.016997"
|
||||
sodipodi:r2="7.5084987"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
|
||||
transform="matrix(-0.93510984,0,0,1.5150471,326.24502,-2.7851671)"
|
||||
inkscape:transform-center-x="3.5106467" />
|
||||
<rect
|
||||
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="rect3083-4"
|
||||
width="170.93134"
|
||||
height="207.72536"
|
||||
x="280.50113"
|
||||
y="1299.152" />
|
||||
<rect
|
||||
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="rect3083-7-5"
|
||||
width="171.86087"
|
||||
height="167.53221"
|
||||
x="280.03638"
|
||||
y="1299.7695" />
|
||||
<rect
|
||||
id="rect2995-0-4-5"
|
||||
y="1300.6287"
|
||||
x="284.52841"
|
||||
height="163.64934"
|
||||
width="29.769083"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-8-5"
|
||||
y="1482.0016"
|
||||
x="312.12393"
|
||||
height="30.541632"
|
||||
width="42.755199"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="path4002-27"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="235.71429"
|
||||
sodipodi:cy="1000.2193"
|
||||
sodipodi:r1="15.016997"
|
||||
sodipodi:r2="7.5084987"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
|
||||
transform="matrix(1.0346242,0,0,1.5150471,76.016678,-3.496726)"
|
||||
inkscape:transform-center-x="-3.8842459"
|
||||
inkscape:transform-center-y="-1.5464308e-005" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="path4002-2-6"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="235.71429"
|
||||
sodipodi:cy="1000.2193"
|
||||
sodipodi:r1="15.016997"
|
||||
sodipodi:r2="7.5084987"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
|
||||
transform="matrix(-0.93510984,0,0,1.5150471,568.21986,-3.496726)"
|
||||
inkscape:transform-center-x="5.318797"
|
||||
inkscape:transform-center-y="-1.5464308e-005" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-7"
|
||||
y="1392.2405"
|
||||
x="365.67133"
|
||||
height="58.049755"
|
||||
width="29.769083"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-7-6"
|
||||
y="1319.5453"
|
||||
x="326.67615"
|
||||
height="49.632401"
|
||||
width="29.769083"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-7-8"
|
||||
y="1179.0293"
|
||||
x="-767.54126"
|
||||
height="58.049755"
|
||||
width="29.769083"
|
||||
style="fill:#b3b3b3;stroke:none"
|
||||
transform="matrix(0.68860063,-0.7251408,0.7251408,0.68860063,0,0)" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-7-6-9"
|
||||
y="1319.5453"
|
||||
x="403.28595"
|
||||
height="49.632404"
|
||||
width="29.769083"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-7-8-2"
|
||||
y="623.14606"
|
||||
x="-1287.8975"
|
||||
height="55.681484"
|
||||
width="28.564859"
|
||||
style="fill:#b3b3b3;stroke:none"
|
||||
transform="matrix(-0.68607628,-0.72752961,-0.72274236,0.69111755,0,0)" />
|
||||
<rect
|
||||
style="color:#000000;fill:#ffffff;stroke:#b3b3b3;stroke-width:7.29121827999999980;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;fill-opacity:1"
|
||||
id="rect3083-7-5-7"
|
||||
width="172.98204"
|
||||
height="125.03616"
|
||||
x="529.78156"
|
||||
y="1383.6165" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-9"
|
||||
y="1385.3533"
|
||||
x="663.37042"
|
||||
height="123.85819"
|
||||
width="38.18644"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-9-5"
|
||||
y="1401.4539"
|
||||
x="552.03174"
|
||||
height="15.96297"
|
||||
width="117.00352"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-9-5-4"
|
||||
y="1437.4023"
|
||||
x="551.16083"
|
||||
height="15.96297"
|
||||
width="117.00352"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<rect
|
||||
id="rect2995-0-4-5-9-5-4-3"
|
||||
y="1473.7642"
|
||||
x="551.16083"
|
||||
height="15.96297"
|
||||
width="117.00352"
|
||||
style="fill:#b3b3b3;stroke:none" />
|
||||
<path
|
||||
style="fill:none;stroke:#b3b3b3;stroke-width:23.0681076;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="m 558.62308,1380.7989 0,-45.237 c 0,0 13.52904,-35.6384 56.38304,-36.1894 40.81922,-0.5248 55.47363,34.6931 55.47363,34.6931 l 0.17276,48.4719"
|
||||
id="path4310"
|
||||
inkscape:connector-curvature="0"
|
||||
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 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 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 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 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 a 10,10 0 1 1 -20,0 10,10 0 1 1 20,0 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 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 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 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 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 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 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>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 87 KiB |
@@ -1 +1 @@
|
||||
sbt.version=0.12.3
|
||||
sbt.version=0.13.1
|
||||
|
||||
@@ -15,6 +15,7 @@ object MyBuild extends Build {
|
||||
"gitbucket",
|
||||
file("."),
|
||||
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
|
||||
sourcesInBase := false,
|
||||
organization := Organization,
|
||||
name := Name,
|
||||
version := Version,
|
||||
@@ -30,12 +31,13 @@ object MyBuild extends Build {
|
||||
"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",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.0.14",
|
||||
"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" % "1.0.1",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.3.173",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
|
||||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
|
||||
|
||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
|
||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
|
||||
|
||||
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
|
||||
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
|
||||
|
||||
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
|
||||
resolvers += "spray repo" at "http://repo.spray.io"
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")
|
||||
addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
|
||||
|
||||
Binary file not shown.
BIN
sbt-launch-0.13.1.jar
Normal file
BIN
sbt-launch-0.13.1.jar
Normal file
Binary file not shown.
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
||||
set SCRIPT_DIR=%~dp0
|
||||
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\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 "%SCRIPT_DIR%\sbt-launch-0.13.1.jar" %*
|
||||
|
||||
2
sbt.sh
2
sbt.sh
@@ -1 +1 @@
|
||||
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.1.jar "$@"
|
||||
|
||||
@@ -25,8 +25,6 @@ public class JettyLauncher {
|
||||
port = Integer.parseInt(dim[1]);
|
||||
} else if(dim[0].equals("--prefix")) {
|
||||
contextPath = dim[1];
|
||||
} else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
|
||||
forceHttps = true;
|
||||
} else if(dim[0].equals("--gitbucket.home")){
|
||||
System.setProperty("gitbucket.home", dim[1]);
|
||||
}
|
||||
@@ -36,7 +34,7 @@ public class JettyLauncher {
|
||||
|
||||
Server server = new Server();
|
||||
|
||||
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps);
|
||||
SelectChannelConnector connector = new SelectChannelConnector();
|
||||
if(host != null) {
|
||||
connector.setHost(host);
|
||||
}
|
||||
@@ -53,25 +51,12 @@ public class JettyLauncher {
|
||||
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
|
||||
context.setServer(server);
|
||||
context.setWar(location.toExternalForm());
|
||||
if (forceHttps) {
|
||||
context.setInitParameter("org.scalatra.ForceHttps", "true");
|
||||
}
|
||||
|
||||
server.setHandler(context);
|
||||
server.start();
|
||||
server.join();
|
||||
}
|
||||
}
|
||||
|
||||
class HttpsSupportConnector extends SelectChannelConnector {
|
||||
private boolean forceHttps;
|
||||
|
||||
public HttpsSupportConnector(boolean forceHttps) {
|
||||
this.forceHttps = forceHttps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(final EndPoint endpoint, final Request request) throws IOException {
|
||||
if (this.forceHttps) {
|
||||
request.setScheme("https");
|
||||
super.customize(endpoint, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
src/main/resources/update/1_12.sql
Normal file
11
src/main/resources/update/1_12.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
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);
|
||||
1
src/main/resources/update/1_13.sql
Normal file
1
src/main/resources/update/1_13.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE COMMIT_LOG;
|
||||
@@ -1,6 +1,6 @@
|
||||
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
|
||||
import app._
|
||||
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||
import org.scalatra._
|
||||
import javax.servlet._
|
||||
import java.util.EnumSet
|
||||
@@ -20,7 +20,6 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
context.mount(new DashboardController, "/*")
|
||||
context.mount(new UserManagementController, "/*")
|
||||
context.mount(new SystemSettingsController, "/*")
|
||||
context.mount(new CreateRepositoryController, "/*")
|
||||
context.mount(new AccountController, "/*")
|
||||
context.mount(new RepositoryViewerController, "/*")
|
||||
context.mount(new WikiController, "/*")
|
||||
@@ -29,7 +28,6 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
context.mount(new IssuesController, "/*")
|
||||
context.mount(new PullRequestsController, "/*")
|
||||
context.mount(new RepositorySettingsController, "/*")
|
||||
context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*")
|
||||
|
||||
// Create GITBUCKET_HOME directory if it does not exist
|
||||
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.{FileUtil, OneselfAuthenticator}
|
||||
import util._
|
||||
import util.StringUtil._
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import ssh.SshUtil
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.FlashMapSupport
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import model.GroupMember
|
||||
|
||||
class AccountController extends AccountControllerBase
|
||||
with SystemSettingsService with AccountService with RepositoryService with ActivityService
|
||||
with OneselfAuthenticator
|
||||
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||
|
||||
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
|
||||
self: SystemSettingsService with AccountService with RepositoryService with ActivityService
|
||||
with OneselfAuthenticator =>
|
||||
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
|
||||
|
||||
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||
url: Option[String], fileId: Option[String])
|
||||
@@ -22,6 +28,8 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
||||
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
|
||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||
|
||||
case class SshKeyForm(title: String, publicKey: String)
|
||||
|
||||
val newForm = mapping(
|
||||
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||
@@ -40,6 +48,45 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
)(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.
|
||||
*/
|
||||
@@ -54,18 +101,30 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
||||
getActivitiesByUser(userName, true))
|
||||
|
||||
// Members
|
||||
case "members" if(account.isGroupAccount) =>
|
||||
_root_.account.html.members(account, getGroupMembers(account.userName))
|
||||
case "members" if(account.isGroupAccount) => {
|
||||
val members = 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
|
||||
case _ =>
|
||||
case _ => {
|
||||
val members = getGroupMembers(account.userName)
|
||||
_root_.account.html.repositories(account,
|
||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
|
||||
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
|
||||
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
|
||||
get("/:userName.atom") {
|
||||
val userName = params("userName")
|
||||
contentType = "application/atom+xml; type=feed"
|
||||
helper.xml.feed(getActivitiesByUser(userName, true))
|
||||
}
|
||||
|
||||
get("/:userName/_avatar"){
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
||||
@@ -79,7 +138,9 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
||||
|
||||
get("/:userName/_edit")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
|
||||
getAccountByUserName(userName).map { x =>
|
||||
account.html.edit(x, flash.get("info"))
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:userName/_edit", editForm)(oneselfOnly { form =>
|
||||
@@ -119,22 +180,254 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
||||
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"){
|
||||
if(loadSystemSettings().allowAccountRegistration){
|
||||
if(context.settings.allowAccountRegistration){
|
||||
if(context.loginAccount.isDefined){
|
||||
redirect("/")
|
||||
} else {
|
||||
account.html.edit(None, None)
|
||||
account.html.register()
|
||||
}
|
||||
} else NotFound
|
||||
}
|
||||
|
||||
post("/register", newForm){ form =>
|
||||
if(loadSystemSettings().allowAccountRegistration){
|
||||
if(context.settings.allowAccountRegistration){
|
||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/signin")
|
||||
} 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}/create"){
|
||||
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),
|
||||
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 || 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,8 @@ import org.json4s._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import model.Account
|
||||
import scala.Some
|
||||
import service.AccountService
|
||||
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
|
||||
import java.text.SimpleDateFormat
|
||||
import service.{SystemSettingsService, AccountService}
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||
import org.scalatra.i18n._
|
||||
|
||||
@@ -21,14 +19,15 @@ import org.scalatra.i18n._
|
||||
* Provides generic features for controller implementations.
|
||||
*/
|
||||
abstract class ControllerBase extends ScalatraFilter
|
||||
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations {
|
||||
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
|
||||
with SystemSettingsService {
|
||||
|
||||
implicit val jsonFormats = DefaultFormats
|
||||
|
||||
// Don't set content type via Accept header.
|
||||
override def format(implicit request: HttpServletRequest) = ""
|
||||
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
|
||||
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||
val httpResponse = response.asInstanceOf[HttpServletResponse]
|
||||
val context = request.getServletContext.getContextPath
|
||||
@@ -36,15 +35,16 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
|
||||
if(path.startsWith("/console/")){
|
||||
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||
val baseUrl = this.baseUrl(httpRequest)
|
||||
if(account == null){
|
||||
// Redirect to login form
|
||||
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
|
||||
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
|
||||
} else if(account.isAdmin){
|
||||
// H2 Console (administrators only)
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
// Redirect to dashboard
|
||||
httpResponse.sendRedirect(context + "/")
|
||||
httpResponse.sendRedirect(baseUrl + "/")
|
||||
}
|
||||
} else if(path.startsWith("/git/")){
|
||||
// Git repository
|
||||
@@ -53,15 +53,24 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
// Scalatra actions
|
||||
super.doFilter(request, response, chain)
|
||||
}
|
||||
} finally {
|
||||
contextCache.remove();
|
||||
}
|
||||
|
||||
private val contextCache = new java.lang.ThreadLocal[Context]()
|
||||
|
||||
/**
|
||||
* Returns the context object for the request.
|
||||
*/
|
||||
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
|
||||
|
||||
private def currentURL: String = defining(request.getQueryString){ queryString =>
|
||||
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
|
||||
implicit def context: Context = {
|
||||
contextCache.get match {
|
||||
case null => {
|
||||
val context = Context(loadSystemSettings(), LoginAccount, request)
|
||||
contextCache.set(context)
|
||||
context
|
||||
}
|
||||
case context => context
|
||||
}
|
||||
}
|
||||
|
||||
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
|
||||
@@ -107,27 +116,32 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
if(request.getMethod.toUpperCase == "POST"){
|
||||
org.scalatra.Unauthorized(redirect("/signin"))
|
||||
} else {
|
||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(currentURL)))
|
||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
|
||||
defining(request.getQueryString){ queryString =>
|
||||
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
|
||||
}
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected def baseUrl = defining(request.getRequestURL.toString){ url =>
|
||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
||||
}
|
||||
override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
|
||||
includeContextPath: Boolean = true, includeServletPath: Boolean = true)
|
||||
(implicit request: HttpServletRequest, response: HttpServletResponse) =
|
||||
if (path.startsWith("http")) path
|
||||
else baseUrl + url(path, params, false, false, false)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Context object for the current request.
|
||||
*/
|
||||
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
|
||||
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
|
||||
|
||||
def redirectUrl = if(request.getParameter("redirect") != null){
|
||||
request.getParameter("redirect")
|
||||
} else {
|
||||
currentUrl
|
||||
}
|
||||
val path = settings.baseUrl.getOrElse(request.getContextPath)
|
||||
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.
|
||||
@@ -149,7 +163,7 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
|
||||
/**
|
||||
* Base trait for controllers which manages account information.
|
||||
*/
|
||||
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
|
||||
trait AccountManagementControllerBase extends ControllerBase {
|
||||
self: AccountService =>
|
||||
|
||||
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
|
||||
@@ -160,9 +174,9 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
|
||||
}
|
||||
} else {
|
||||
fileId.map { fileId =>
|
||||
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
|
||||
val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get)
|
||||
FileUtils.moveFile(
|
||||
getTemporaryFile(fileId),
|
||||
new java.io.File(getTemporaryDir(session.getId), fileId),
|
||||
new java.io.File(getUserUploadDir(userName), filename)
|
||||
)
|
||||
updateAvatarImage(userName, Some(filename))
|
||||
@@ -182,28 +196,3 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,7 +49,7 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
)
|
||||
|
||||
val userName = context.loginAccount.get.userName
|
||||
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
|
||||
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
|
||||
val filterUser = Map(filter -> userName)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
//
|
||||
@@ -80,7 +80,7 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
}.copy(repo = repository))
|
||||
|
||||
val userName = context.loginAccount.get.userName
|
||||
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
|
||||
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
|
||||
val filterUser = Map(filter -> userName)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
package app
|
||||
|
||||
import _root_.util.{Keys, FileUtil}
|
||||
import util.{Keys, FileUtil}
|
||||
import util.ControlUtil._
|
||||
import util.Directory._
|
||||
import org.scalatra._
|
||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
|
||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
/**
|
||||
* Provides Ajax based file upload functionality.
|
||||
*
|
||||
* 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.
|
||||
* This servlet saves uploaded file.
|
||||
*/
|
||||
class FileUploadController extends ScalatraServlet
|
||||
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
|
||||
class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
||||
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||
|
||||
post("/image"){
|
||||
fileParams.get("file") match {
|
||||
case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
|
||||
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
|
||||
session += Keys.Session.Upload(fileId) -> file.name
|
||||
Ok(fileId)
|
||||
}
|
||||
case None => BadRequest
|
||||
execute { (file, fileId) =>
|
||||
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), 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)
|
||||
}
|
||||
case _ => BadRequest
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package app
|
||||
|
||||
import util._
|
||||
import util.Implicits._
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class IndexController extends IndexControllerBase
|
||||
with RepositoryService with SystemSettingsService with ActivityService with AccountService
|
||||
with UsersAuthenticator
|
||||
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
|
||||
|
||||
trait IndexControllerBase extends ControllerBase {
|
||||
self: RepositoryService with SystemSettingsService with ActivityService with AccountService with UsersAuthenticator =>
|
||||
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
|
||||
|
||||
case class SignInForm(userName: String, password: String)
|
||||
|
||||
@@ -23,22 +21,21 @@ trait IndexControllerBase extends ControllerBase {
|
||||
val loginAccount = context.loginAccount
|
||||
|
||||
html.index(getRecentActivities(),
|
||||
getVisibleRepositories(loginAccount, baseUrl),
|
||||
loadSystemSettings(),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
|
||||
getVisibleRepositories(loginAccount, context.baseUrl),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl) }.getOrElse(Nil)
|
||||
)
|
||||
}
|
||||
|
||||
get("/signin"){
|
||||
val redirect = params.get("redirect")
|
||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||
session.setAttribute(Keys.Session.Redirect, redirect.get)
|
||||
flash += Keys.Flash.Redirect -> redirect.get
|
||||
}
|
||||
html.signin(loadSystemSettings())
|
||||
html.signin()
|
||||
}
|
||||
|
||||
post("/signin", form){ form =>
|
||||
authenticate(loadSystemSettings(), form.userName, form.password) match {
|
||||
authenticate(context.settings, form.userName, form.password) match {
|
||||
case Some(account) => signin(account)
|
||||
case None => redirect("/signin")
|
||||
}
|
||||
@@ -49,6 +46,11 @@ trait IndexControllerBase extends ControllerBase {
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
get("/activities.atom"){
|
||||
contentType = "application/atom+xml; type=feed"
|
||||
helper.xml.feed(getRecentActivities())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set account information into HttpSession and redirect.
|
||||
*/
|
||||
@@ -56,7 +58,7 @@ trait IndexControllerBase extends ControllerBase {
|
||||
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
|
||||
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
|
||||
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
|
||||
redirect("/")
|
||||
} else {
|
||||
|
||||
@@ -4,10 +4,11 @@ import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
import service._
|
||||
import IssuesService._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
|
||||
import util._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import org.scalatra.Ok
|
||||
import model.Issue
|
||||
|
||||
class IssuesController extends IssuesControllerBase
|
||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||
@@ -110,9 +111,14 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
// record activity
|
||||
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(""))
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||
@@ -123,7 +129,11 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
getIssue(owner, name, params("id")).map { issue =>
|
||||
if(isEditable(owner, name, issue.openedUserName)){
|
||||
// update issue
|
||||
updateIssue(owner, name, issue.issueId, form.title, form.content)
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||
} else Unauthorized
|
||||
} getOrElse NotFound
|
||||
@@ -263,6 +273,17 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
|
||||
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
||||
case dir if(dir.exists && dir.isDirectory) =>
|
||||
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
||||
contentType = FileUtil.getMimeType(file.getName)
|
||||
file
|
||||
}
|
||||
case _ => None
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
||||
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
|
||||
|
||||
@@ -274,6 +295,15 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
}
|
||||
|
||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||
if(getIssue(owner, repository, issueId).isDefined){
|
||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
|
||||
fromIssue.issueId + ":" + fromIssue.title, "refer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||
*/
|
||||
@@ -313,18 +343,23 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
||||
|
||||
// extract references and create refer comment
|
||||
content.map { content =>
|
||||
createReferComment(owner, name, issue, content)
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier() match {
|
||||
case f =>
|
||||
content foreach {
|
||||
f.toNotify(repository, issueId, _){
|
||||
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
|
||||
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
|
||||
}
|
||||
}
|
||||
action foreach {
|
||||
f.toNotify(repository, issueId, _){
|
||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import service.WebHookService.WebHookPayload
|
||||
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService with WebHookService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||
with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
|
||||
trait PullRequestsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService with WebHookService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||
with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||
|
||||
@@ -79,8 +79,10 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
pulls.html.pullreq(
|
||||
issue, pullreq,
|
||||
getComments(owner, name, issueId),
|
||||
getIssueLabels(owner, name, issueId),
|
||||
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||
getMilestonesWithIssueCount(owner, name),
|
||||
getLabels(owner, name),
|
||||
commits,
|
||||
diffs,
|
||||
hasWritePermission(owner, name, context.loginAccount),
|
||||
@@ -98,11 +100,26 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
pulls.html.mergeguide(
|
||||
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
|
||||
pullreq,
|
||||
s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
|
||||
params("id").toIntOpt.map { issueId =>
|
||||
val branchName = multiParams("splat").head
|
||||
val userName = context.loginAccount.get.userName
|
||||
if(repository.repository.defaultBranch != branchName){
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.branchDelete().setForce(true).setBranchNames(branchName).call()
|
||||
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
|
||||
}
|
||||
}
|
||||
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
||||
params("id").toIntOpt.flatMap { issueId =>
|
||||
val owner = repository.owner
|
||||
@@ -160,12 +177,18 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
commits.flatten.foreach { commit =>
|
||||
if(!existsCommitId(owner, name, commit.id)){
|
||||
insertCommitId(owner, name, commit.id)
|
||||
// close issue by content of pull request
|
||||
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
|
||||
if(pullreq.branch == defaultBranch){
|
||||
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
|
||||
getWebHookURLs(owner, name) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
@@ -178,7 +201,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
@@ -191,7 +214,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
|
||||
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
|
||||
using(
|
||||
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
@@ -199,16 +222,16 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
|
||||
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
|
||||
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
case _ => {
|
||||
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
|
||||
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||
} getOrElse {
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,7 +251,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
@@ -237,7 +260,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
val forkedId = getForkedCommitId(oldGit, newGit,
|
||||
val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||
originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||
|
||||
@@ -280,7 +303,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
@@ -333,7 +356,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
@@ -409,24 +432,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
(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,
|
||||
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
|
||||
|
||||
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
|
||||
using(
|
||||
Git.open(getRepositoryDir(userName, repositoryName)),
|
||||
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
|
||||
@@ -444,7 +451,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
(commits, diffs)
|
||||
}
|
||||
}
|
||||
|
||||
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
|
||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||
|
||||
@@ -5,7 +5,6 @@ import util.Directory._
|
||||
import util.{UsersAuthenticator, OwnerAuthenticator}
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.FlashMapSupport
|
||||
import org.scalatra.i18n.Messages
|
||||
import service.WebHookService.WebHookPayload
|
||||
import util.JGitUtil.CommitInfo
|
||||
@@ -16,7 +15,7 @@ class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
with RepositoryService with AccountService with WebHookService
|
||||
with OwnerAuthenticator with UsersAuthenticator
|
||||
|
||||
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with WebHookService
|
||||
with OwnerAuthenticator with UsersAuthenticator =>
|
||||
|
||||
@@ -73,7 +72,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
repository.owner,
|
||||
repository.name,
|
||||
form.description,
|
||||
form.defaultBranch,
|
||||
if(repository.branchList.isEmpty) "master" else form.defaultBranch,
|
||||
repository.repository.parentUserName.map { _ =>
|
||||
repository.repository.isPrivate
|
||||
} getOrElse form.isPrivate
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package app
|
||||
|
||||
import _root_.util.JGitUtil.CommitInfo
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
|
||||
import _root_.util.ControlUtil._
|
||||
import _root_.util._
|
||||
import service._
|
||||
import org.scalatra._
|
||||
import java.io.File
|
||||
@@ -12,15 +13,52 @@ import org.eclipse.jgit.lib._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.treewalk._
|
||||
import java.util.zip.{ZipEntry, ZipOutputStream}
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
|
||||
|
||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
with RepositoryService with AccountService with ReferrerAuthenticator
|
||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
|
||||
/**
|
||||
* The repository viewer.
|
||||
*/
|
||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with ReferrerAuthenticator =>
|
||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
|
||||
case class EditorForm(
|
||||
branch: String,
|
||||
path: String,
|
||||
content: String,
|
||||
message: Option[String],
|
||||
charset: 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))),
|
||||
"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.
|
||||
@@ -70,6 +108,68 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||
repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
|
||||
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, form.content, 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, form.content, 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.
|
||||
*/
|
||||
@@ -79,46 +179,18 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||
|
||||
@scala.annotation.tailrec
|
||||
def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match {
|
||||
case true if(walk.getPathString == path) => walk.getObjectId(0)
|
||||
case true => getPathObjectId(path, walk)
|
||||
}
|
||||
|
||||
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)
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
if(raw){
|
||||
// Download
|
||||
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
|
||||
contentType = FileUtil.getContentType(path, bytes)
|
||||
bytes
|
||||
}
|
||||
} else {
|
||||
// image or large
|
||||
JGitUtil.ContentInfo(viewer, None)
|
||||
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
|
||||
new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
}
|
||||
|
||||
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
@@ -150,10 +222,25 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
|
||||
(branchName, revCommit.getCommitterIdent.getWhen)
|
||||
}
|
||||
repo.html.branches(branchInfo, repository)
|
||||
repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Deletes branch.
|
||||
*/
|
||||
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
|
||||
val branchName = multiParams("splat").head
|
||||
val userName = context.loginAccount.get.userName
|
||||
if(repository.repository.defaultBranch != branchName){
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.branchDelete().setForce(true).setBranchNames(branchName).call()
|
||||
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
|
||||
}
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/branches")
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays tags.
|
||||
*/
|
||||
@@ -164,8 +251,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
/**
|
||||
* Download repository contents as an archive.
|
||||
*/
|
||||
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
|
||||
val name = params("name")
|
||||
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
|
||||
val name = multiParams("splat").head
|
||||
|
||||
if(name.endsWith(".zip")){
|
||||
val revision = name.replaceFirst("\\.zip$", "")
|
||||
@@ -176,7 +263,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
workDir.mkdirs
|
||||
|
||||
val zipFile = new File(workDir, repository.name + "-" +
|
||||
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
|
||||
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + ".zip")
|
||||
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||
@@ -191,7 +278,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
while(walk.next){
|
||||
val name = walk.getPathString
|
||||
val mode = walk.getFileMode(0)
|
||||
if(mode != FileMode.TREE){
|
||||
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
|
||||
walk.getObjectId(objectId, 0)
|
||||
val entry = new ZipEntry(name)
|
||||
val loader = reader.open(objectId)
|
||||
@@ -217,13 +304,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
getRepository(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name),
|
||||
baseUrl),
|
||||
context.baseUrl),
|
||||
getForkedRepositories(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||
repository)
|
||||
})
|
||||
|
||||
|
||||
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
|
||||
val id = repository.branchList.collectFirst {
|
||||
case branch if(path == branch || path.startsWith(branch + "/")) => branch
|
||||
@@ -235,11 +322,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
|
||||
private val readmeFiles = Seq("readme.md", "readme.markdown")
|
||||
private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
|
||||
|
||||
/**
|
||||
* Provides HTML of the file list.
|
||||
*
|
||||
*
|
||||
* @param repository the repository information
|
||||
* @param revstr the branch name or commit id(optional)
|
||||
* @param path the directory path (optional)
|
||||
@@ -247,30 +334,99 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
*/
|
||||
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
|
||||
if(repository.commitCount == 0){
|
||||
repo.html.guide(repository)
|
||||
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} else {
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
|
||||
//val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
|
||||
// get specified commit
|
||||
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
|
||||
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
|
||||
// get files
|
||||
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
val parentPath = if (path == ".") Nil else path.split("/").toList
|
||||
// process README.md or README.markdown
|
||||
val readme = files.find { file =>
|
||||
readmeFiles.contains(file.name.toLowerCase)
|
||||
}.map { file =>
|
||||
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
val path = (file.name :: parentPath.reverse).reverse
|
||||
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
|
||||
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
}
|
||||
|
||||
repo.html.files(revision, repository,
|
||||
if(path == ".") Nil else path.split("/").toList, // current path
|
||||
new JGitUtil.CommitInfo(revCommit), // latest commit
|
||||
files, readme)
|
||||
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
}
|
||||
} 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(s"refs/heads/${branch}")
|
||||
|
||||
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),
|
||||
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))))
|
||||
|
||||
// TODO invoke hook
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,13 +6,10 @@ import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class SearchController extends SearchControllerBase
|
||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
||||
with RepositorySearchService with IssuesService
|
||||
with ReferrerAuthenticator
|
||||
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
|
||||
|
||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||
with SystemSettingsService with ActivityService with RepositorySearchService
|
||||
with ReferrerAuthenticator =>
|
||||
with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
|
||||
|
||||
val searchForm = mapping(
|
||||
"query" -> trim(text(required)),
|
||||
|
||||
@@ -4,18 +4,21 @@ import service.{AccountService, SystemSettingsService}
|
||||
import SystemSettingsService._
|
||||
import util.AdminAuthenticator
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.FlashMapSupport
|
||||
import ssh.SshServer
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with SystemSettingsService with AccountService with AdminAuthenticator
|
||||
with AccountService with AdminAuthenticator
|
||||
|
||||
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
self: SystemSettingsService with AccountService with AdminAuthenticator =>
|
||||
trait SystemSettingsControllerBase extends ControllerBase {
|
||||
self: AccountService with AdminAuthenticator =>
|
||||
|
||||
private val form = mapping(
|
||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"ssh" -> trim(label("SSH access", boolean())),
|
||||
"sshPort" -> trim(label("SSH port", optional(number()))),
|
||||
"smtp" -> optionalIfNotChecked("notification", mapping(
|
||||
"host" -> trim(label("SMTP Host", text(required))),
|
||||
"port" -> trim(label("SMTP Port", optional(number()))),
|
||||
@@ -38,15 +41,32 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
"tls" -> trim(label("Enable TLS", optional(boolean()))),
|
||||
"keystore" -> trim(label("Keystore", optional(text())))
|
||||
)(Ldap.apply))
|
||||
)(SystemSettings.apply)
|
||||
)(SystemSettings.apply).verifying { settings =>
|
||||
if(settings.ssh && settings.baseUrl.isEmpty){
|
||||
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
||||
} else Nil
|
||||
}
|
||||
|
||||
|
||||
get("/admin/system")(adminOnly {
|
||||
admin.html.system(loadSystemSettings(), flash.get("info"))
|
||||
admin.html.system(flash.get("info"))
|
||||
})
|
||||
|
||||
post("/admin/system", form)(adminOnly { form =>
|
||||
saveSystemSettings(form)
|
||||
|
||||
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
|
||||
SshServer.stop()
|
||||
}
|
||||
|
||||
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
|
||||
SshServer.start(request.getServletContext,
|
||||
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
|
||||
form.baseUrl.get)
|
||||
} else if(!form.ssh && SshServer.isActive){
|
||||
SshServer.stop()
|
||||
}
|
||||
|
||||
flash += "info" -> "System settings has been updated."
|
||||
redirect("/admin/system")
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import util.AdminAuthenticator
|
||||
import util.StringUtil._
|
||||
import util.ControlUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.Directory._
|
||||
|
||||
@@ -23,10 +24,10 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String])
|
||||
members: String)
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
members: String, clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
val newUserForm = mapping(
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
@@ -51,28 +52,28 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
)(EditUserForm.apply)
|
||||
|
||||
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()))),
|
||||
"memberNames" -> trim(label("Member Names" ,optional(text())))
|
||||
"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()))),
|
||||
"memberNames" -> trim(label("Member Names" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
"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())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||
val users = getAllUsers(includeRemoved)
|
||||
|
||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName)
|
||||
val users = getAllUsers(includeRemoved)
|
||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||
}.toMap
|
||||
|
||||
admin.users.html.list(users, members, includeRemoved)
|
||||
})
|
||||
|
||||
@@ -127,7 +128,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||
createGroup(form.groupName, form.url)
|
||||
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
|
||||
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
}
|
||||
}.toList)
|
||||
updateImage(form.groupName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
@@ -139,7 +144,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
})
|
||||
|
||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
|
||||
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, form.isRemoved)
|
||||
|
||||
@@ -155,11 +164,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
} else {
|
||||
// Update GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, memberNames)
|
||||
updateGroupMembers(form.groupName, members)
|
||||
// Update COLLABORATOR for group repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
memberNames.foreach { userName =>
|
||||
members.foreach { case (userName, isManager) =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
}
|
||||
}
|
||||
@@ -172,8 +181,17 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
post("/admin/users/_usercheck")(adminOnly {
|
||||
// TODO Move to other generic controller?
|
||||
post("/admin/users/_usercheck"){
|
||||
getAccountByUserName(params("userName")).isDefined
|
||||
})
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,18 +6,15 @@ import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.scalatra.FlashMapSupport
|
||||
import org.scalatra.i18n.Messages
|
||||
import scala.Some
|
||||
import java.util.ResourceBundle
|
||||
|
||||
class WikiController extends WikiControllerBase
|
||||
with WikiService with RepositoryService with AccountService with ActivityService
|
||||
with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||
|
||||
trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
self: WikiService with RepositoryService with ActivityService
|
||||
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
||||
trait WikiControllerBase extends ControllerBase {
|
||||
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
||||
|
||||
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
|
||||
|
||||
@@ -39,7 +36,8 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
|
||||
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
||||
getWikiPage(repository.owner, repository.name, "Home").map { page =>
|
||||
wiki.html.page("Home", page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
wiki.html.page("Home", page, getWikiPageList(repository.owner, repository.name),
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
|
||||
})
|
||||
|
||||
@@ -47,7 +45,8 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
|
||||
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
||||
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
wiki.html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
|
||||
})
|
||||
|
||||
|
||||
@@ -13,12 +13,6 @@ object Activities extends Table[Activity]("ACTIVITY") with BasicTemplate {
|
||||
def autoInc = userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate returning activityId
|
||||
}
|
||||
|
||||
object CommitLog extends Table[(String, String, String)]("COMMIT_LOG") with BasicTemplate {
|
||||
def commitId = column[String]("COMMIT_ID")
|
||||
def * = userName ~ repositoryName ~ commitId
|
||||
def byPrimaryKey(userName: String, repositoryName: String, commitId: String) = byRepository(userName, repositoryName) && (this.commitId is commitId.bind)
|
||||
}
|
||||
|
||||
case class Activity(
|
||||
activityId: Int,
|
||||
userName: String,
|
||||
|
||||
@@ -5,10 +5,12 @@ import scala.slick.driver.H2Driver.simple._
|
||||
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
|
||||
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
|
||||
def userName = column[String]("USER_NAME", O PrimaryKey)
|
||||
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
|
||||
def isManager = column[Boolean]("MANAGER")
|
||||
def * = groupName ~ userName ~ isManager <> (GroupMember, GroupMember.unapply _)
|
||||
}
|
||||
|
||||
case class GroupMember(
|
||||
groupName: String,
|
||||
userName: String
|
||||
userName: String,
|
||||
isManager: Boolean
|
||||
)
|
||||
22
src/main/scala/model/SshKey.scala
Normal file
22
src/main/scala/model/SshKey.scala
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object SshKeys extends Table[SshKey]("SSH_KEY") {
|
||||
def userName = column[String]("USER_NAME")
|
||||
def sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc)
|
||||
def title = column[String]("TITLE")
|
||||
def publicKey = column[String]("PUBLIC_KEY")
|
||||
|
||||
def ins = userName ~ title ~ publicKey returning sshKeyId
|
||||
def * = userName ~ sshKeyId ~ title ~ publicKey <> (SshKey, SshKey.unapply _)
|
||||
|
||||
def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName is userName.bind) && (this.sshKeyId is sshKeyId.bind)
|
||||
}
|
||||
|
||||
case class SshKey(
|
||||
userName: String,
|
||||
sshKeyId: Int,
|
||||
title: String,
|
||||
publicKey: String
|
||||
)
|
||||
@@ -34,19 +34,34 @@ trait AccountService {
|
||||
/**
|
||||
* Authenticate by LDAP.
|
||||
*/
|
||||
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
|
||||
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String): Option[Account] = {
|
||||
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
|
||||
case Right(ldapUserInfo) => {
|
||||
// Create or update account by LDAP information
|
||||
getAccountByUserName(userName, true) match {
|
||||
case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
|
||||
getAccountByUserName(ldapUserInfo.userName, true) match {
|
||||
case Some(x) if(!x.isRemoved) => {
|
||||
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
|
||||
getAccountByUserName(ldapUserInfo.userName)
|
||||
}
|
||||
case Some(x) if(x.isRemoved) => {
|
||||
logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..")
|
||||
defaultAuthentication(userName, password)
|
||||
}
|
||||
case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
|
||||
case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
|
||||
case Some(x) if(!x.isRemoved) => {
|
||||
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
||||
getAccountByUserName(ldapUserInfo.userName)
|
||||
}
|
||||
case Some(x) if(x.isRemoved) => {
|
||||
logger.info(s"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) => {
|
||||
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
|
||||
@@ -59,7 +74,7 @@ trait AccountService {
|
||||
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
|
||||
|
||||
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
|
||||
Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
|
||||
Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
|
||||
|
||||
def getAllUsers(includeRemoved: Boolean = true): List[Account] =
|
||||
if(includeRemoved){
|
||||
@@ -83,14 +98,14 @@ trait AccountService {
|
||||
isGroupAccount = false,
|
||||
isRemoved = false)
|
||||
|
||||
def updateAccount(account: Account): Unit =
|
||||
def updateAccount(account: Account): Unit =
|
||||
Accounts
|
||||
.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 }
|
||||
.update (
|
||||
account.password,
|
||||
account.fullName,
|
||||
account.mailAddress,
|
||||
account.password,
|
||||
account.fullName,
|
||||
account.mailAddress,
|
||||
account.isAdmin,
|
||||
account.url,
|
||||
account.registeredDate,
|
||||
@@ -122,18 +137,17 @@ trait AccountService {
|
||||
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
|
||||
Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
|
||||
|
||||
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
|
||||
def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = {
|
||||
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
|
||||
members.foreach { userName =>
|
||||
GroupMembers insert GroupMember (groupName, userName)
|
||||
members.foreach { case (userName, isManager) =>
|
||||
GroupMembers insert GroupMember (groupName, userName, isManager)
|
||||
}
|
||||
}
|
||||
|
||||
def getGroupMembers(groupName: String): List[String] =
|
||||
def getGroupMembers(groupName: String): List[GroupMember] =
|
||||
Query(GroupMembers)
|
||||
.filter(_.groupName is groupName.bind)
|
||||
.sortBy(_.userName)
|
||||
.map(_.userName)
|
||||
.list
|
||||
|
||||
def getGroupsByUserName(userName: String): List[String] =
|
||||
|
||||
@@ -153,19 +153,6 @@ trait ActivityService {
|
||||
Some(message),
|
||||
currentDate)
|
||||
|
||||
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 =
|
||||
private def cut(value: String, length: Int): String =
|
||||
if(value.length > length) value.substring(0, length) + "..." else value
|
||||
}
|
||||
|
||||
@@ -119,16 +119,10 @@ trait IssuesService {
|
||||
// get issues and comment count and labels
|
||||
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
|
||||
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
|
||||
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
|
||||
.map { case (((t1, t2), t3), t4) =>
|
||||
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
|
||||
}
|
||||
.sortBy(_._4) // labelName
|
||||
.sortBy { case (t1, commentCount, _,_,_) =>
|
||||
.sortBy { case (t1, t2) =>
|
||||
(condition.sort match {
|
||||
case "created" => t1.registeredDate
|
||||
case "comments" => commentCount
|
||||
case "comments" => t2.commentCount
|
||||
case "updated" => t1.updatedDate
|
||||
}) match {
|
||||
case sort => condition.direction match {
|
||||
@@ -138,6 +132,11 @@ trait IssuesService {
|
||||
}
|
||||
}
|
||||
.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) }
|
||||
.map { case (((t1, t2), t3), t4) =>
|
||||
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
|
||||
}
|
||||
.list
|
||||
.splitWith { (c1, c2) =>
|
||||
c1._1.userName == c2._1.userName &&
|
||||
@@ -314,6 +313,14 @@ trait IssuesService {
|
||||
}.toList
|
||||
}
|
||||
|
||||
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = {
|
||||
extractCloseId(message).foreach { issueId =>
|
||||
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
|
||||
createComment(owner, repository, userName, issue.issueId, "Close", "close")
|
||||
updateClosed(owner, repository, issue.issueId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object IssuesService {
|
||||
@@ -331,7 +338,7 @@ object IssuesService {
|
||||
|
||||
def toURL: String =
|
||||
"?" + List(
|
||||
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
|
||||
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
|
||||
milestoneId.map { id => "milestone=" + (id match {
|
||||
case Some(x) => x.toString
|
||||
case None => "none"
|
||||
@@ -352,7 +359,7 @@ object IssuesService {
|
||||
|
||||
def apply(request: HttpServletRequest): IssueSearchCondition =
|
||||
IssueSearchCondition(
|
||||
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
|
||||
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
|
||||
param(request, "milestone").map{
|
||||
case "none" => None
|
||||
case x => x.toIntOpt
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
import model._
|
||||
import util.ControlUtil._
|
||||
|
||||
trait PullRequestService { self: IssuesService =>
|
||||
import PullRequestService._
|
||||
@@ -15,8 +14,10 @@ trait PullRequestService { self: IssuesService =>
|
||||
}
|
||||
}
|
||||
|
||||
def updateCommitIdTo(owner: String, repository: String, issueId: Int, commitIdTo: String): Unit =
|
||||
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdTo).update(commitIdTo)
|
||||
def updateCommitId(owner: String, repository: String, issueId: Int, commitIdTo: String, commitIdFrom: String): Unit =
|
||||
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId))
|
||||
.map(pr => pr.commitIdTo ~ pr.commitIdFrom)
|
||||
.update((commitIdTo, commitIdFrom))
|
||||
|
||||
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
|
||||
Query(PullRequests)
|
||||
|
||||
@@ -63,8 +63,9 @@ RepositorySearchService { self: IssuesService =>
|
||||
val list = new ListBuffer[(String, String)]
|
||||
|
||||
while (treeWalk.next()) {
|
||||
if(treeWalk.getFileMode(0) != FileMode.TREE){
|
||||
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
|
||||
val mode = treeWalk.getFileMode(0)
|
||||
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
|
||||
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes =>
|
||||
if(FileUtil.isText(bytes)){
|
||||
val text = StringUtil.convertFromByteArray(bytes)
|
||||
val lowerText = text.toLowerCase
|
||||
|
||||
@@ -40,69 +40,72 @@ trait RepositoryService { self: AccountService =>
|
||||
}
|
||||
|
||||
def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String): Unit = {
|
||||
(Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
|
||||
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
|
||||
getAccountByUserName(newUserName).foreach { account =>
|
||||
(Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
|
||||
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
|
||||
|
||||
val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val commitLog = Query(CommitLog ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
|
||||
Repositories.filter { t =>
|
||||
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
|
||||
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
|
||||
Repositories.filter { t =>
|
||||
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
|
||||
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
|
||||
|
||||
Repositories.filter { t =>
|
||||
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
|
||||
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
|
||||
Repositories.filter { t =>
|
||||
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
|
||||
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
|
||||
|
||||
PullRequests.filter { t =>
|
||||
t.requestRepositoryName is oldRepositoryName.bind
|
||||
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
||||
PullRequests.filter { t =>
|
||||
t.requestRepositoryName is oldRepositoryName.bind
|
||||
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
||||
|
||||
deleteRepository(oldUserName, oldRepositoryName)
|
||||
deleteRepository(oldUserName, oldRepositoryName)
|
||||
|
||||
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = 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)) :_*)
|
||||
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = 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)) :_*)
|
||||
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
if(account.isGroupAccount){
|
||||
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
|
||||
} else {
|
||||
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
|
||||
// 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 is 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}#")
|
||||
)
|
||||
updateActivities.foreach { case (activityId, message) =>
|
||||
Activities.filter(_.activityId is 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): Unit = {
|
||||
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
@@ -147,7 +150,8 @@ trait RepositoryService { self: AccountService =>
|
||||
getForkedCount(
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
))
|
||||
),
|
||||
getRepositoryManagers(repository.userName))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +166,8 @@ trait RepositoryService { self: AccountService =>
|
||||
getForkedCount(
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
))
|
||||
),
|
||||
getRepositoryManagers(repository.userName))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,10 +200,18 @@ trait RepositoryService { self: AccountService =>
|
||||
getForkedCount(
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
))
|
||||
),
|
||||
getRepositoryManagers(repository.userName))
|
||||
}
|
||||
}
|
||||
|
||||
private def getRepositoryManagers(userName: String): 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.
|
||||
*/
|
||||
@@ -278,21 +291,25 @@ trait RepositoryService { self: AccountService =>
|
||||
|
||||
object RepositoryService {
|
||||
|
||||
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
|
||||
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
|
||||
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
|
||||
branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
|
||||
branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){
|
||||
|
||||
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.
|
||||
*/
|
||||
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)
|
||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
|
||||
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
|
||||
|
||||
/**
|
||||
* Creates instance without issue count and pull request count.
|
||||
*/
|
||||
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)
|
||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
|
||||
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
|
||||
}
|
||||
|
||||
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
import service.SystemSettingsService.SystemSettings
|
||||
|
||||
/**
|
||||
* This service is used for a view helper mainly.
|
||||
@@ -9,28 +8,23 @@ import service.SystemSettingsService.SystemSettings
|
||||
* It may be called many times in one request, so each method stores
|
||||
* its result into the cache which available during a request.
|
||||
*/
|
||||
trait RequestCache {
|
||||
|
||||
def getSystemSettings()(implicit context: app.Context): SystemSettings =
|
||||
context.cache("system_settings"){
|
||||
new SystemSettingsService {}.loadSystemSettings()
|
||||
}
|
||||
trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
|
||||
|
||||
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
|
||||
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
|
||||
new IssuesService {}.getIssue(userName, repositoryName, issueId)
|
||||
super.getIssue(userName, repositoryName, issueId)
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
|
||||
context.cache(s"account.${userName}"){
|
||||
new AccountService {}.getAccountByUserName(userName)
|
||||
super.getAccountByUserName(userName)
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
|
||||
context.cache(s"account.${mailAddress}"){
|
||||
new AccountService {}.getAccountByMailAddress(mailAddress)
|
||||
super.getAccountByMailAddress(mailAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
src/main/scala/service/SshKeyService.scala
Normal file
19
src/main/scala/service/SshKeyService.scala
Normal file
@@ -0,0 +1,19 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
|
||||
trait SshKeyService {
|
||||
|
||||
def addPublicKey(userName: String, title: String, publicKey: String): Unit =
|
||||
SshKeys.ins insert (userName, title, publicKey)
|
||||
|
||||
def getPublicKeys(userName: String): List[SshKey] =
|
||||
Query(SshKeys).filter(_.userName is userName.bind).sortBy(_.sshKeyId).list
|
||||
|
||||
def deletePublicKey(userName: String, sshKeyId: Int): Unit =
|
||||
SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
|
||||
|
||||
|
||||
}
|
||||
@@ -3,14 +3,20 @@ package service
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import SystemSettingsService._
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
trait SystemSettingsService {
|
||||
|
||||
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request)
|
||||
|
||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||
defining(new java.util.Properties()){ props =>
|
||||
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
props.setProperty(Ssh, settings.ssh.toString)
|
||||
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
||||
if(settings.notification) {
|
||||
settings.smtp.foreach { smtp =>
|
||||
props.setProperty(SmtpHost, smtp.host)
|
||||
@@ -48,9 +54,12 @@ trait SystemSettingsService {
|
||||
props.load(new java.io.FileInputStream(GitBucketConf))
|
||||
}
|
||||
SystemSettings(
|
||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, Gravatar, true),
|
||||
getValue(props, Notification, false),
|
||||
getValue(props, Ssh, false),
|
||||
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
||||
if(getValue(props, Notification, false)){
|
||||
Some(Smtp(
|
||||
getValue(props, SmtpHost, ""),
|
||||
@@ -89,12 +98,21 @@ object SystemSettingsService {
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
case class SystemSettings(
|
||||
baseUrl: Option[String],
|
||||
allowAccountRegistration: Boolean,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
ssh: Boolean,
|
||||
sshPort: Option[Int],
|
||||
smtp: Option[Smtp],
|
||||
ldapAuthentication: Boolean,
|
||||
ldap: Option[Ldap])
|
||||
ldap: Option[Ldap]){
|
||||
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
|
||||
defining(request.getRequestURL.toString){ url =>
|
||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
||||
}
|
||||
}.replaceFirst("/$", "")
|
||||
}
|
||||
|
||||
case class Ldap(
|
||||
host: String,
|
||||
@@ -117,12 +135,16 @@ object SystemSettingsService {
|
||||
fromAddress: Option[String],
|
||||
fromName: Option[String])
|
||||
|
||||
val DefaultSshPort = 29418
|
||||
val DefaultSmtpPort = 25
|
||||
val DefaultLdapPort = 389
|
||||
|
||||
private val BaseURL = "base_url"
|
||||
private val AllowAccountRegistration = "allow_account_registration"
|
||||
private val Gravatar = "gravatar"
|
||||
private val Notification = "notification"
|
||||
private val Ssh = "ssh"
|
||||
private val SshPort = "ssh.port"
|
||||
private val SmtpHost = "smtp.host"
|
||||
private val SmtpPort = "smtp.port"
|
||||
private val SmtpUser = "smtp.user"
|
||||
|
||||
@@ -87,7 +87,7 @@ object WebHookService {
|
||||
refName,
|
||||
commits.map { commit =>
|
||||
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||
val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
|
||||
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
|
||||
|
||||
WebHookCommit(
|
||||
id = commit.id,
|
||||
@@ -106,7 +106,7 @@ object WebHookService {
|
||||
}.toList,
|
||||
WebHookRepository(
|
||||
name = repositoryInfo.name,
|
||||
url = repositoryInfo.url,
|
||||
url = repositoryInfo.httpUrl,
|
||||
description = repositoryInfo.repository.description.getOrElse(""),
|
||||
watchers = 0,
|
||||
forks = repositoryInfo.forkedCount,
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.eclipse.jgit.patch._
|
||||
import org.eclipse.jgit.api.errors.PatchFormatException
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.Some
|
||||
import service.RepositoryService.RepositoryInfo
|
||||
|
||||
|
||||
object WikiService {
|
||||
@@ -40,6 +41,10 @@ object WikiService {
|
||||
*/
|
||||
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
|
||||
|
||||
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
|
||||
|
||||
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
|
||||
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
|
||||
}
|
||||
|
||||
trait WikiService {
|
||||
@@ -170,17 +175,9 @@ trait WikiService {
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
|
||||
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(revertInfo.find(x => x.filePath == path).isEmpty){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
}
|
||||
}
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(revertInfo.find(x => x.filePath == path).isEmpty){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,22 +218,14 @@ trait WikiService {
|
||||
var removed = false
|
||||
|
||||
if(headId != null){
|
||||
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 == currentPageName + ".md" && currentPageName != newPageName){
|
||||
removed = true
|
||||
} else if(path != newPageName + ".md"){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
} else {
|
||||
created = false
|
||||
updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
|
||||
}
|
||||
}
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(path == currentPageName + ".md" && currentPageName != newPageName){
|
||||
removed = true
|
||||
} else if(path != newPageName + ".md"){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
} else {
|
||||
created = false
|
||||
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +246,7 @@ trait WikiService {
|
||||
message
|
||||
})
|
||||
|
||||
Some(newHeadId)
|
||||
Some(newHeadId.getName)
|
||||
} else None
|
||||
}
|
||||
}
|
||||
@@ -268,35 +257,26 @@ trait WikiService {
|
||||
*/
|
||||
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
||||
committer: String, mailAddress: String, message: String): Unit = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
var removed = false
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
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)
|
||||
}
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import util.Directory
|
||||
|
||||
object AutoUpdate {
|
||||
|
||||
@@ -50,6 +51,35 @@ object AutoUpdate {
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(2, 0){
|
||||
override def update(conn: Connection): Unit = {
|
||||
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
|
||||
|
||||
val mimeUtil = new MimeUtil2()
|
||||
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
||||
|
||||
super.update(conn)
|
||||
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
|
||||
while(rs.next){
|
||||
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
||||
if(dir.exists && dir.isDirectory){
|
||||
dir.listFiles.foreach { file =>
|
||||
if(file.getName.indexOf('.') < 0){
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Version(1, 13),
|
||||
Version(1, 12),
|
||||
Version(1, 11),
|
||||
Version(1, 10),
|
||||
Version(1, 9),
|
||||
Version(1, 8),
|
||||
@@ -96,7 +126,7 @@ object AutoUpdate {
|
||||
*/
|
||||
def getCurrentVersion(): Version = {
|
||||
if(versionFile.exists){
|
||||
FileUtils.readFileToString(versionFile, "UTF-8").split("\\.") match {
|
||||
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
|
||||
case Array(majorVersion, minorVersion) => {
|
||||
versions.find { v =>
|
||||
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
|
||||
@@ -122,7 +152,7 @@ class AutoUpdateListener extends ServletContextListener {
|
||||
System.setProperty("gitbucket.home", datadir)
|
||||
}
|
||||
org.h2.Driver.load()
|
||||
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
|
||||
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
|
||||
|
||||
logger.debug("Start schema update")
|
||||
defining(getConnection(event.getServletContext)){ conn =>
|
||||
|
||||
@@ -3,6 +3,7 @@ package servlet
|
||||
import javax.servlet._
|
||||
import javax.servlet.http._
|
||||
import service.{SystemSettingsService, AccountService, RepositoryService}
|
||||
import model.Account
|
||||
import org.slf4j.LoggerFactory
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
@@ -38,9 +39,12 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
request.getHeader("Authorization") match {
|
||||
case null => requireAuth(response)
|
||||
case auth => decodeAuthHeader(auth).split(":") match {
|
||||
case Array(username, password) if(isWritableUser(username, password, repository)) => {
|
||||
request.setAttribute(Keys.Request.UserName, username)
|
||||
chain.doFilter(req, wrappedResponse)
|
||||
case Array(username, password) => getWritableUser(username, password, repository) match {
|
||||
case Some(account) => {
|
||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||
chain.doFilter(req, wrappedResponse)
|
||||
}
|
||||
case None => requireAuth(response)
|
||||
}
|
||||
case _ => requireAuth(response)
|
||||
}
|
||||
@@ -61,10 +65,10 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
}
|
||||
}
|
||||
|
||||
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
|
||||
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Option[Account] =
|
||||
authenticate(loadSystemSettings(), username, password) match {
|
||||
case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
|
||||
case None => false
|
||||
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
|
||||
case _ => None
|
||||
}
|
||||
|
||||
private def requireAuth(response: HttpServletResponse): Unit = {
|
||||
|
||||
@@ -8,14 +8,15 @@ import org.slf4j.LoggerFactory
|
||||
|
||||
import javax.servlet.ServletConfig
|
||||
import javax.servlet.ServletContext
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import util.{Keys, JGitUtil, Directory}
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
import util.{StringUtil, Keys, JGitUtil, Directory}
|
||||
import util.ControlUtil._
|
||||
import util.Implicits._
|
||||
import service._
|
||||
import WebHookService._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import util.JGitUtil.CommitInfo
|
||||
import service.IssuesService.IssueSearchCondition
|
||||
|
||||
/**
|
||||
* Provides Git repository via HTTP.
|
||||
@@ -23,7 +24,7 @@ import util.JGitUtil.CommitInfo
|
||||
* This servlet provides only Git repository functionality.
|
||||
* Authentication is provided by [[servlet.BasicAuthenticationFilter]].
|
||||
*/
|
||||
class GitRepositoryServlet extends GitServlet {
|
||||
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
|
||||
|
||||
@@ -47,13 +48,25 @@ class GitRepositoryServlet extends GitServlet {
|
||||
|
||||
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] {
|
||||
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
|
||||
|
||||
|
||||
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
||||
val receivePack = new ReceivePack(db)
|
||||
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
||||
@@ -64,13 +77,13 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
defining(request.paths){ paths =>
|
||||
val owner = paths(1)
|
||||
val repository = paths(2).replaceFirst("\\.git$", "")
|
||||
val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
|
||||
|
||||
logger.debug("repository:" + owner + "/" + repository)
|
||||
logger.debug("baseURL:" + baseURL)
|
||||
|
||||
if(!repository.endsWith(".wiki")){
|
||||
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL))
|
||||
val hook = new CommitLogHook(owner, repository, pusher, baseUrl(request))
|
||||
receivePack.setPreReceiveHook(hook)
|
||||
receivePack.setPostReceiveHook(hook)
|
||||
}
|
||||
receivePack
|
||||
}
|
||||
@@ -79,43 +92,55 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseURL: String) extends PostReceiveHook
|
||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook with PreReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
|
||||
|
||||
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 = {
|
||||
try {
|
||||
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||
commands.asScala.foreach { command =>
|
||||
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 branchName = refName.drop(2).mkString("/")
|
||||
|
||||
// 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
|
||||
}
|
||||
val commits = if (refName(1) == "tags") {
|
||||
Nil
|
||||
} else {
|
||||
commits.flatMap { commit =>
|
||||
if(!existsCommitId(owner, repository, commit.id)){
|
||||
createIssueComment(commit)
|
||||
Some(commit)
|
||||
} else None
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.DELETE => Nil
|
||||
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
|
||||
}
|
||||
}
|
||||
|
||||
// batch insert all new commit id
|
||||
insertAllCommitIds(owner, repository, newCommits.map(_.id))
|
||||
// Retrieve all issue count in the repository
|
||||
val issueCount =
|
||||
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
|
||||
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
|
||||
|
||||
// Extract new commit and apply issue comment
|
||||
val newCommits = commits.flatMap { commit =>
|
||||
if (!existIds.contains(commit.id)) {
|
||||
if (issueCount > 0) {
|
||||
createIssueComment(commit)
|
||||
}
|
||||
Some(commit)
|
||||
} else None
|
||||
}
|
||||
|
||||
// record activity
|
||||
if(refName(1) == "heads"){
|
||||
@@ -143,12 +168,23 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
|
||||
}
|
||||
}
|
||||
|
||||
// close issues
|
||||
if(issueCount > 0) {
|
||||
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
|
||||
if (refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE) {
|
||||
git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach {
|
||||
commit =>
|
||||
closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call web hook
|
||||
getWebHookURLs(owner, repository) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(pusherAccount <- getAccountByUserName(pusher);
|
||||
ownerAccount <- getAccountByUserName(owner);
|
||||
repositoryInfo <- getRepository(owner, repository, baseURL)){
|
||||
repositoryInfo <- getRepository(owner, repository, baseUrl)){
|
||||
callWebHook(owner, repository, webHookURLs,
|
||||
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
|
||||
}
|
||||
@@ -167,8 +203,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
|
||||
}
|
||||
|
||||
private def createIssueComment(commit: CommitInfo) = {
|
||||
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
|
||||
val issueId = matchData.group(2)
|
||||
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
||||
if(getIssue(owner, repository, issueId).isDefined){
|
||||
getAccountByMailAddress(commit.mailAddress).foreach { account =>
|
||||
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
||||
@@ -182,15 +217,19 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
|
||||
*/
|
||||
private def updatePullRequests(branch: String) =
|
||||
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
|
||||
if(getRepository(pullreq.userName, pullreq.repositoryName, baseURL).isDefined){
|
||||
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
|
||||
git.fetch
|
||||
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
|
||||
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
|
||||
Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
|
||||
oldGit.fetch
|
||||
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
|
||||
.call
|
||||
|
||||
val commitIdTo = git.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
|
||||
updateCommitIdTo(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo)
|
||||
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
|
||||
val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||
pullreq.userName, pullreq.repositoryName, pullreq.branch,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
|
||||
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package servlet
|
||||
|
||||
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
|
||||
import app.FileUploadControllerBase
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.Directory._
|
||||
|
||||
/**
|
||||
* Removes session associated temporary files when session is destroyed.
|
||||
*/
|
||||
class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
|
||||
class SessionCleanupListener extends HttpSessionListener {
|
||||
|
||||
def sessionCreated(se: HttpSessionEvent): Unit = {}
|
||||
|
||||
def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
|
||||
def sessionDestroyed(se: HttpSessionEvent): Unit = FileUtils.deleteDirectory(getTemporaryDir(se.getSession.getId))
|
||||
|
||||
}
|
||||
|
||||
132
src/main/scala/ssh/GitCommand.scala
Normal file
132
src/main/scala/ssh/GitCommand.scala
Normal file
@@ -0,0 +1,132 @@
|
||||
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
|
||||
|
||||
|
||||
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): Unit
|
||||
|
||||
private def newTask(user: String): Runnable = new Runnable {
|
||||
override def run(): Unit = {
|
||||
Database(context) withTransaction {
|
||||
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): 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): 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): 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/main/scala/ssh/NoShell.scala
Normal file
62
src/main/scala/ssh/NoShell.scala
Normal file
@@ -0,0 +1,62 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/main/scala/ssh/PublicKeyAuthenticator.scala
Normal file
23
src/main/scala/ssh/PublicKeyAuthenticator.scala
Normal file
@@ -0,0 +1,23 @@
|
||||
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) withTransaction {
|
||||
getPublicKeys(username).exists { sshKey =>
|
||||
SshUtil.str2PublicKey(sshKey.publicKey) match {
|
||||
case Some(publicKey) => key.equals(publicKey)
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
70
src/main/scala/ssh/SshServerListener.scala
Normal file
70
src/main/scala/ssh/SshServerListener.scala
Normal file
@@ -0,0 +1,70 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
36
src/main/scala/ssh/SshUtil.scala
Normal file
36
src/main/scala/ssh/SshUtil.scala
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
|
||||
/**
|
||||
* Allows only the repository owner and administrators.
|
||||
*/
|
||||
trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
|
||||
trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
|
||||
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
|
||||
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
|
||||
|
||||
@@ -40,6 +40,9 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
|
||||
context.loginAccount match {
|
||||
case Some(x) if(x.isAdmin) => 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()
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
@@ -106,7 +109,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows only the repository owner and administrators.
|
||||
* Allows only the repository owner (or manager for group repository) and administrators.
|
||||
*/
|
||||
trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
|
||||
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
|
||||
@@ -155,3 +158,24 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package util
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.treewalk.TreeWalk
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import scala.util.control.Exception._
|
||||
import scala.language.reflectiveCalls
|
||||
|
||||
/**
|
||||
@@ -16,10 +16,8 @@ object ControlUtil {
|
||||
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
|
||||
try f(resource) finally {
|
||||
if(resource != null){
|
||||
try {
|
||||
ignoring(classOf[Throwable]) {
|
||||
resource.close()
|
||||
} catch {
|
||||
case e: Throwable => // ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,15 +37,4 @@ object ControlUtil {
|
||||
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
|
||||
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()
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -29,24 +29,10 @@ object Directory {
|
||||
}).getAbsolutePath
|
||||
|
||||
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
|
||||
|
||||
|
||||
val RepositoryHome = s"${GitBucketHome}/repositories"
|
||||
|
||||
val DatabaseHome = s"${GitBucketHome}/data"
|
||||
|
||||
/**
|
||||
* Repository names of the specified user.
|
||||
*/
|
||||
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.
|
||||
@@ -54,11 +40,23 @@ object Directory {
|
||||
def getRepositoryDir(owner: String, repository: String): File =
|
||||
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.
|
||||
*/
|
||||
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"${GitBucketHome}/tmp/_upload/${sessionId}")
|
||||
|
||||
/**
|
||||
* Root of temporary directories for the specified repository.
|
||||
*/
|
||||
|
||||
@@ -4,9 +4,10 @@ import org.apache.commons.io.FileUtils
|
||||
import java.net.URLConnection
|
||||
import java.io.File
|
||||
import util.ControlUtil._
|
||||
import scala.util.Random
|
||||
|
||||
object FileUtil {
|
||||
|
||||
|
||||
def getMimeType(name: String): String =
|
||||
defining(URLConnection.getFileNameMap()){ fileNameMap =>
|
||||
fileNameMap.getContentTypeFor(name) match {
|
||||
@@ -26,32 +27,12 @@ object FileUtil {
|
||||
}
|
||||
|
||||
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
|
||||
|
||||
|
||||
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
|
||||
|
||||
|
||||
def isText(content: Array[Byte]): Boolean = !content.contains(0)
|
||||
|
||||
// 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 generateFileId: String = System.currentTimeMillis + Random.alphanumeric.take(10).mkString
|
||||
|
||||
def getExtension(name: String): String =
|
||||
name.lastIndexOf('.') match {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import scala.util.matching.Regex
|
||||
import scala.util.control.Exception._
|
||||
import javax.servlet.http.{HttpSession, HttpServletRequest}
|
||||
|
||||
/**
|
||||
@@ -42,10 +43,8 @@ object Implicits {
|
||||
sb.toString
|
||||
}
|
||||
|
||||
def toIntOpt: Option[Int] = try {
|
||||
Option(Integer.parseInt(value))
|
||||
} catch {
|
||||
case e: NumberFormatException => None
|
||||
def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt {
|
||||
Integer.parseInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,20 @@ import org.eclipse.jgit.revwalk.filter._
|
||||
import org.eclipse.jgit.treewalk._
|
||||
import org.eclipse.jgit.treewalk.filter._
|
||||
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||
import org.eclipse.jgit.errors.MissingObjectException
|
||||
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.errors.NoHeadException
|
||||
import service.RepositoryService
|
||||
import org.eclipse.jgit.dircache.DirCacheEntry
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Provides complex JGit operations.
|
||||
*/
|
||||
object JGitUtil {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
|
||||
|
||||
/**
|
||||
* The repository data.
|
||||
*
|
||||
@@ -45,9 +48,10 @@ object JGitUtil {
|
||||
* @param commitId the last commit id
|
||||
* @param committer the last committer name
|
||||
* @param mailAddress the committer's mail address
|
||||
* @param linkUrl the url of submodule
|
||||
*/
|
||||
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
|
||||
committer: String, mailAddress: String)
|
||||
committer: String, mailAddress: String, linkUrl: Option[String])
|
||||
|
||||
/**
|
||||
* The commit data.
|
||||
@@ -72,11 +76,7 @@ object JGitUtil {
|
||||
rev.getFullMessage,
|
||||
rev.getParents().map(_.name).toList)
|
||||
|
||||
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 summary = getSummaryMessage(fullMessage, shortMessage)
|
||||
|
||||
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
|
||||
if(i >= 0){
|
||||
@@ -92,8 +92,9 @@ object JGitUtil {
|
||||
*
|
||||
* @param viewType "image", "large" or "other"
|
||||
* @param content the string content
|
||||
* @param charset the character encoding
|
||||
*/
|
||||
case class ContentInfo(viewType: String, content: Option[String])
|
||||
case class ContentInfo(viewType: String, content: Option[String], charset: Option[String])
|
||||
|
||||
/**
|
||||
* The tag data.
|
||||
@@ -104,6 +105,15 @@ object JGitUtil {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@@ -128,7 +138,7 @@ object JGitUtil {
|
||||
using(Git.open(getRepositoryDir(owner, repository))){ git =>
|
||||
try {
|
||||
// get commit count
|
||||
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
|
||||
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
|
||||
|
||||
RepositoryInfo(
|
||||
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
|
||||
@@ -152,7 +162,7 @@ object JGitUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the file list of the specified path.
|
||||
*
|
||||
@@ -162,7 +172,7 @@ object JGitUtil {
|
||||
* @return HTML of the file list
|
||||
*/
|
||||
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
|
||||
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
|
||||
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
|
||||
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
@@ -195,22 +205,30 @@ object JGitUtil {
|
||||
})
|
||||
}
|
||||
while (treeWalk.next()) {
|
||||
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
|
||||
list.map { case (objectId, fileMode, path, name) =>
|
||||
FileInfo(
|
||||
objectId,
|
||||
fileMode == FileMode.TREE,
|
||||
name,
|
||||
commits(path).getCommitterIdent.getWhen,
|
||||
commits(path).getShortMessage,
|
||||
commits(path).getName,
|
||||
commits(path).getCommitterIdent.getName,
|
||||
commits(path).getCommitterIdent.getEmailAddress)
|
||||
list.map { case (objectId, fileMode, path, name, linkUrl) =>
|
||||
defining(commits(path)){ commit =>
|
||||
FileInfo(
|
||||
objectId,
|
||||
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
|
||||
name,
|
||||
commit.getCommitterIdent.getWhen,
|
||||
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
|
||||
commit.getName,
|
||||
commit.getCommitterIdent.getName,
|
||||
commit.getCommitterIdent.getEmailAddress,
|
||||
linkUrl)
|
||||
}
|
||||
}.sortWith { (file1, file2) =>
|
||||
(file1.isDirectory, file2.isDirectory) match {
|
||||
case (true , false) => true
|
||||
@@ -219,7 +237,18 @@ object JGitUtil {
|
||||
}
|
||||
}.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.
|
||||
*
|
||||
@@ -325,27 +354,6 @@ object JGitUtil {
|
||||
}.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.
|
||||
*/
|
||||
@@ -364,7 +372,12 @@ object JGitUtil {
|
||||
|
||||
if(commits.length >= 2){
|
||||
// not initial commit
|
||||
val oldCommit = commits(1)
|
||||
val oldCommit = if(revCommit.getParentCount >= 2) {
|
||||
// merge commit
|
||||
revCommit.getParents.head
|
||||
} else {
|
||||
commits(1)
|
||||
}
|
||||
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
|
||||
|
||||
} else {
|
||||
@@ -377,7 +390,7 @@ object JGitUtil {
|
||||
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
|
||||
} else {
|
||||
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
|
||||
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||
}))
|
||||
}
|
||||
(buffer.toList, None)
|
||||
@@ -400,8 +413,8 @@ object JGitUtil {
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
|
||||
} else {
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
|
||||
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||
}
|
||||
}.toList
|
||||
}
|
||||
@@ -473,7 +486,7 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
|
||||
fullName: String, mailAddress: String, message: String): String = {
|
||||
fullName: String, mailAddress: String, message: String): ObjectId = {
|
||||
val newCommit = new CommitBuilder()
|
||||
newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
|
||||
newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
|
||||
@@ -491,7 +504,134 @@ object JGitUtil {
|
||||
refUpdate.setNewObjectId(newHeadId)
|
||||
refUpdate.update()
|
||||
|
||||
newHeadId.getName
|
||||
newHeadId
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,12 +13,7 @@ object Keys {
|
||||
/**
|
||||
* Session key for the logged in account information.
|
||||
*/
|
||||
val LoginAccount = "LOGIN_ACCOUNT"
|
||||
|
||||
/**
|
||||
* Session key for the redirect URL.
|
||||
*/
|
||||
val Redirect = "REDIRECT"
|
||||
val LoginAccount = "loginAccount"
|
||||
|
||||
/**
|
||||
* Session key for the issue search condition in dashboard.
|
||||
@@ -47,6 +42,20 @@ object Keys {
|
||||
|
||||
}
|
||||
|
||||
object Flash {
|
||||
|
||||
/**
|
||||
* Flash key for the redirect URL.
|
||||
*/
|
||||
val Redirect = "redirect"
|
||||
|
||||
/**
|
||||
* Flash key for the information message.
|
||||
*/
|
||||
val Info = "info"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Define request keys.
|
||||
*/
|
||||
|
||||
@@ -49,7 +49,7 @@ object LDAPUtil {
|
||||
){ conn =>
|
||||
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
|
||||
case Some(mailAddress) => Right(LDAPUserInfo(
|
||||
userName = userName,
|
||||
userName = getUserNameFromMailAddress(userName),
|
||||
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
|
||||
findFullName(conn, userDN, fullNameAttribute)
|
||||
}.getOrElse(userName),
|
||||
@@ -59,6 +59,13 @@ object LDAPUtil {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
|
||||
if (tls) {
|
||||
|
||||
@@ -31,7 +31,7 @@ object StringUtil {
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
|
||||
@@ -45,4 +45,23 @@ object StringUtil {
|
||||
case e => e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract issue id like ```#issueId``` from the given message.
|
||||
*
|
||||
*@param message the message which may contains issue id
|
||||
* @return the iterator of issue id
|
||||
*/
|
||||
def extractIssueId(message: String): Iterator[String] =
|
||||
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.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))
|
||||
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
val src = if(mailAddress.isEmpty){
|
||||
// by user name
|
||||
getAccountByUserName(userName).map { account =>
|
||||
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
if(account.image.isEmpty && context.settings.gravatar){
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||
} else {
|
||||
s"""${context.path}/${account.userName}/_avatar"""
|
||||
}
|
||||
@@ -27,14 +27,14 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
} else {
|
||||
// by mail address
|
||||
getAccountByMailAddress(mailAddress).map { account =>
|
||||
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
if(account.image.isEmpty && context.settings.gravatar){
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||
} else {
|
||||
s"""${context.path}/${account.userName}/_avatar"""
|
||||
}
|
||||
} getOrElse {
|
||||
if(getSystemSettings().gravatar){
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
|
||||
if(context.settings.gravatar){
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||
} else {
|
||||
s"""${context.path}/_unknown/_avatar"""
|
||||
}
|
||||
@@ -42,9 +42,9 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
}
|
||||
|
||||
if(tooltip){
|
||||
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
|
||||
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}"/>""")
|
||||
} else {
|
||||
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
|
||||
Html(s"""<img src="${src}" class="${if(size > 20){"avatar"} else {"avatar-mini"}}" style="width: ${size}px; height: ${size}px;" />""")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.parboiled.common.StringUtils
|
||||
import org.pegdown._
|
||||
import org.pegdown.ast._
|
||||
import org.pegdown.LinkRenderer.Rendering
|
||||
import java.text.Normalizer
|
||||
import java.util.Locale
|
||||
import scala.collection.JavaConverters._
|
||||
import service.{RequestCache, WikiService}
|
||||
|
||||
@@ -43,7 +45,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
|
||||
(text, text)
|
||||
}
|
||||
|
||||
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
|
||||
val url = repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
|
||||
|
||||
if(getWikiPage(repository.owner, repository.name, page).isDefined){
|
||||
new Rendering(url, label)
|
||||
@@ -87,7 +89,8 @@ class GitBucketHtmlSerializer(
|
||||
) with LinkConverter with RequestCache {
|
||||
|
||||
override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
|
||||
printer.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/>")
|
||||
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url)).print("\">")
|
||||
.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
|
||||
|
||||
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
|
||||
printer.print('<').print('a')
|
||||
@@ -99,10 +102,10 @@ class GitBucketHtmlSerializer(
|
||||
}
|
||||
|
||||
private def fixUrl(url: String): String = {
|
||||
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://")){
|
||||
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){
|
||||
url
|
||||
} else {
|
||||
repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
|
||||
repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +113,21 @@ class GitBucketHtmlSerializer(
|
||||
printer.print(' ').print(name).print('=').print('"').print(value).print('"')
|
||||
}
|
||||
|
||||
private def printHeaderTag(node: HeaderNode): Unit = {
|
||||
val tag = s"h${node.getLevel}"
|
||||
val headerTextString = printChildrenToString(node)
|
||||
val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
|
||||
printer.print(s"""<$tag class="markdown-head">""")
|
||||
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
|
||||
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
|
||||
visitChildren(node)
|
||||
printer.print(s"</$tag>")
|
||||
}
|
||||
|
||||
override def visit(node: HeaderNode): Unit = {
|
||||
printHeaderTag(node)
|
||||
}
|
||||
|
||||
override def visit(node: TextNode): Unit = {
|
||||
// convert commit id and username to link.
|
||||
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
|
||||
@@ -120,5 +138,16 @@ class GitBucketHtmlSerializer(
|
||||
printWithAbbreviations(text)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object GitBucketHtmlSerializer {
|
||||
|
||||
private val Whitespace = "[\\s]".r
|
||||
|
||||
def generateAnchorName(text: String): String = {
|
||||
val noWhitespace = Whitespace.replaceAllIn(text, "-")
|
||||
val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD)
|
||||
val noSpecialChars = StringUtil.urlEncode(normalized)
|
||||
noSpecialChars.toLowerCase(Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
*/
|
||||
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 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'").format(date).replaceAll("(\\d\\d)(\\d\\d)$","$1:$2")
|
||||
|
||||
/**
|
||||
* Format java.util.Date to "yyyy-MM-dd".
|
||||
*/
|
||||
@@ -27,6 +32,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
def plural(count: Int, singular: String, plural: String = ""): String =
|
||||
if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural
|
||||
|
||||
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] =
|
||||
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.
|
||||
*/
|
||||
@@ -34,6 +47,21 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
|
||||
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 <img> 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.
|
||||
@@ -135,6 +163,45 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
*/
|
||||
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(".tcl")) => "tcl"
|
||||
case x if(x.endsWith(".yml")) => "yaml"
|
||||
case _ => "plain_text"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implicit conversion to add mkHtml() to Seq[Html].
|
||||
*/
|
||||
|
||||
@@ -2,5 +2,8 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main(account, groupNames, "activity"){
|
||||
<div class="pull-right">
|
||||
<a href="@path/@{account.userName}.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
|
||||
</div>
|
||||
@helper.html.activities(activities)
|
||||
}
|
||||
|
||||
@@ -1,71 +1,64 @@
|
||||
@(account: Option[model.Account], info: Option[Any])(implicit context: app.Context)
|
||||
@(account: model.Account, info: Option[Any])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main((if(account.isDefined) "Edit your profile" else "Create your account")){
|
||||
@if(account.isDefined){
|
||||
<h3>Edit your profile</h3>
|
||||
} else {
|
||||
<h3>Create your account</h3>
|
||||
}
|
||||
@helper.html.information(info)
|
||||
<form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{@path/register}" method="POST" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
@if(account.isEmpty){
|
||||
<fieldset>
|
||||
<label for="userName" class="strong">Username:</label>
|
||||
<input type="text" name="userName" id="userName" value=""/>
|
||||
<span id="error-userName" class="error"></span>
|
||||
</fieldset>
|
||||
}
|
||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||
<fieldset>
|
||||
<label for="password" class="strong">
|
||||
Password
|
||||
@if(account.nonEmpty){
|
||||
(input to change password)
|
||||
}
|
||||
:
|
||||
</label>
|
||||
<input type="password" name="password" id="password" value=""/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
}
|
||||
<fieldset>
|
||||
<label for="fullName" class="strong">Full Name:</label>
|
||||
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
|
||||
<span id="error-fullName" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="mailAddress" class="strong">Mail Address:</label>
|
||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="url" class="strong">URL (optional):</label>
|
||||
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
||||
<span id="error-url" class="error"></span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<fieldset>
|
||||
<label for="avatar" class="strong">Image (optional):</label>
|
||||
@helper.html.uploadavatar(account)
|
||||
</fieldset>
|
||||
</div>
|
||||
@html.main("Edit your profile"){
|
||||
<div class="container">
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
@menu("profile", settings.ssh)
|
||||
</div>
|
||||
<fieldset class="margin">
|
||||
@if(account.isDefined){
|
||||
<div class="pull-right">
|
||||
<a href="@path/@account.get.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
|
||||
<div class="span9">
|
||||
@helper.html.information(info)
|
||||
<form action="@url(account.userName)/_edit" method="POST" validate="true">
|
||||
<div class="box">
|
||||
<div class="box-header">Profile</div>
|
||||
<div class="box-content">
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
@if(account.password.nonEmpty){
|
||||
<fieldset>
|
||||
<label for="password" class="strong">
|
||||
Password (input to change password):
|
||||
</label>
|
||||
<input type="password" name="password" id="password" value=""/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
}
|
||||
<fieldset>
|
||||
<label for="fullName" class="strong">Full Name:</label>
|
||||
<input type="text" name="fullName" id="fullName" value="@account.fullName"/>
|
||||
<span id="error-fullName" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="mailAddress" class="strong">Mail Address:</label>
|
||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.mailAddress"/>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="url" class="strong">URL (optional):</label>
|
||||
<input type="text" name="url" id="url" style="width: 300px;" value="@account.url"/>
|
||||
<span id="error-url" class="error"></span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<fieldset>
|
||||
<label for="avatar" class="strong">Image (optional):</label>
|
||||
@helper.html.uploadavatar(Some(account))
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div class="pull-right">
|
||||
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-success" value="Save"/>
|
||||
<a href="@url(account.userName)" class="btn">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-success" value="Save"/>
|
||||
<a href="@url(account.get.userName)" class="btn">Cancel</a>
|
||||
} else {
|
||||
<input type="submit" class="btn btn-success" value="Create account"/>
|
||||
}
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
140
src/main/twirl/account/group.scala.html
Normal file
140
src/main/twirl/account/group.scala.html
Normal file
@@ -0,0 +1,140 @@
|
||||
@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
|
||||
<div class="container">
|
||||
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span5">
|
||||
<fieldset>
|
||||
<label for="groupName" class="strong">Group name</label>
|
||||
<div>
|
||||
<span id="error-groupName" class="error"></span>
|
||||
</div>
|
||||
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label class="strong">URL (Optional)</label>
|
||||
<div>
|
||||
<span id="error-url" class="error"></span>
|
||||
</div>
|
||||
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="avatar" class="strong">Image (Optional)</label>
|
||||
@helper.html.uploadavatar(account)
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="span7">
|
||||
<fieldset>
|
||||
<label class="strong">Members</label>
|
||||
<ul id="member-list" class="collaborator">
|
||||
</ul>
|
||||
@helper.html.account("memberName", 200)
|
||||
<input type="button" class="btn" value="Add" id="addMember"/>
|
||||
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
|
||||
<div>
|
||||
<span class="error" id="error-members"></span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="margin">
|
||||
@if(account.isDefined){
|
||||
<div class="pull-right">
|
||||
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
|
||||
</div>
|
||||
}
|
||||
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
|
||||
@if(account.isDefined){
|
||||
<a href="@url(account.get.userName)" class="btn">Cancel</a>
|
||||
}
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('input[type=submit]').click(function(){
|
||||
updateMembers();
|
||||
});
|
||||
|
||||
$('#addMember').click(function(){
|
||||
$('#error-memberName').text('');
|
||||
var userName = $('#memberName').val();
|
||||
|
||||
// check empty
|
||||
if($.trim(userName) == ''){
|
||||
return false;
|
||||
}
|
||||
|
||||
// check duplication
|
||||
var exists = $('#member-list li').filter(function(){
|
||||
return $(this).data('name') == userName;
|
||||
}).length > 0;
|
||||
if(exists){
|
||||
$('#error-memberName').text('User has been already added.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// check existence
|
||||
$.post('@path/admin/users/_usercheck', {
|
||||
'userName': userName
|
||||
}, function(data, status){
|
||||
if(data == 'true'){
|
||||
addMemberHTML(userName, false);
|
||||
} else {
|
||||
$('#error-memberName').text('User does not exist.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove', function(){
|
||||
$(this).parent().remove();
|
||||
});
|
||||
|
||||
// Don't submit form by ENTER key
|
||||
$('#memberName').keypress(function(e){
|
||||
return !(e.keyCode == 13);
|
||||
});
|
||||
|
||||
$('#delete').click(function(){
|
||||
return confirm('Once you delete this group, there is no going back.\nAre you sure?');
|
||||
});
|
||||
|
||||
@members.map { member =>
|
||||
addMemberHTML('@member.userName', @member.isManager);
|
||||
}
|
||||
|
||||
function addMemberHTML(userName, isManager){
|
||||
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
|
||||
if(!isManager){
|
||||
memberButton.addClass('active');
|
||||
}
|
||||
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
|
||||
if(isManager){
|
||||
managerButton.addClass('active');
|
||||
}
|
||||
|
||||
$('#member-list').append($('<li>')
|
||||
.data('name', userName)
|
||||
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
|
||||
.append(memberButton)
|
||||
.append(managerButton))
|
||||
.append(' ')
|
||||
.append($('<a>').attr('href', '@path/' + userName).text(userName))
|
||||
.append(' ')
|
||||
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
|
||||
}
|
||||
|
||||
function updateMembers(){
|
||||
var members = $('#member-list li').map(function(i, e){
|
||||
var userName = $(e).data('name');
|
||||
return userName + ':' + $('button.active').filter(function(i, e){
|
||||
return $(e).data('name') == userName;
|
||||
}).attr('value');
|
||||
}).get().join(',');
|
||||
$('#members').val(members);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,48 +1,58 @@
|
||||
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
|
||||
@(account: model.Account, groupNames: List[String], active: String,
|
||||
isGroupManager: Boolean = false)(body: Html)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(account.userName){
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<div class="block">
|
||||
<div class="account-image">@avatar(account.userName, 200)</div>
|
||||
<div class="account-fullname">@account.fullName</div>
|
||||
<div class="account-username">@account.userName</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
@if(account.url.isDefined){
|
||||
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
|
||||
}
|
||||
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
||||
</div>
|
||||
@if(groupNames.nonEmpty){
|
||||
<div>
|
||||
<div>Groups</div>
|
||||
@groupNames.map { groupName =>
|
||||
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
|
||||
}
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<div class="block">
|
||||
<div class="account-image">@avatar(account.userName, 200)</div>
|
||||
<div class="account-fullname">@account.fullName</div>
|
||||
<div class="account-username">@account.userName</div>
|
||||
</div>
|
||||
}
|
||||
<div class="block">
|
||||
@if(account.url.isDefined){
|
||||
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
|
||||
}
|
||||
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
||||
</div>
|
||||
@if(groupNames.nonEmpty){
|
||||
<div>
|
||||
<div>Groups</div>
|
||||
@groupNames.map { groupName =>
|
||||
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div class="span8">
|
||||
<ul class="nav nav-tabs">
|
||||
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
|
||||
@if(account.isGroupAccount){
|
||||
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
|
||||
} else {
|
||||
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
|
||||
}
|
||||
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
|
||||
<li class="pull-right">
|
||||
<div class="button-group">
|
||||
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@body
|
||||
</div>
|
||||
<div class="span8">
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 5px;">
|
||||
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
|
||||
@if(account.isGroupAccount){
|
||||
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
|
||||
} else {
|
||||
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
|
||||
}
|
||||
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
|
||||
<li class="pull-right">
|
||||
<div class="button-group">
|
||||
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
|
||||
<li class="pull-right">
|
||||
<div class="button-group">
|
||||
<a href="@url(account.userName)/_editgroup" class="btn">Edit Group</a>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@body
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@(account: model.Account, members: List[String])(implicit context: app.Context)
|
||||
@(account: model.Account, members: List[String], isGroupManager: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main(account, Nil, "members"){
|
||||
@main(account, Nil, "members", isGroupManager){
|
||||
@if(members.isEmpty){
|
||||
No members
|
||||
} else {
|
||||
|
||||
14
src/main/twirl/account/menu.scala.html
Normal file
14
src/main/twirl/account/menu.scala.html
Normal file
@@ -0,0 +1,14 @@
|
||||
@(active: String, ssh: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
<div class="box">
|
||||
<ul class="nav nav-tabs nav-stacked side-menu">
|
||||
<li@if(active=="profile"){ class="active"}>
|
||||
<a href="@path/@loginAccount.get.userName/_edit">Profile</a>
|
||||
</li>
|
||||
@if(ssh){
|
||||
<li@if(active=="ssh"){ class="active"}>
|
||||
<a href="@path/@loginAccount.get.userName/_ssh">SSH Keys</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
@(groupNames: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main("Create a New Repository"){
|
||||
@html.main("Create a New Repository"){
|
||||
<div style="width: 600px; margin: 10px auto;">
|
||||
<form id="form" method="post" action="@path/new" validate="true">
|
||||
<fieldset>
|
||||
@@ -30,7 +30,7 @@
|
||||
<fieldset class="margin">
|
||||
<label class="radio">
|
||||
<input type="radio" name="isPrivate" value="false" checked>
|
||||
<span class="strong"><i class="icon-eye-open"> </i> Public</span><br>
|
||||
<span class="strong"><img src="@assets/common/images/repo_public.png"/> </i> Public</span><br>
|
||||
<div>
|
||||
<span>All users and guests can read this repository.</span>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isPrivate" value="true">
|
||||
<span class="strong"><i class="icon-lock"> </i> Private</span><br>
|
||||
<span class="strong"><img src="@assets/common/images/repo_private.png"/> </i> Private</span><br>
|
||||
<div>
|
||||
<span>Only collaborators can read this repository.</span>
|
||||
</div>
|
||||
50
src/main/twirl/account/register.scala.html
Normal file
50
src/main/twirl/account/register.scala.html
Normal file
@@ -0,0 +1,50 @@
|
||||
@()(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Create your account"){
|
||||
<div class="container">
|
||||
<h3>Create your account</h3>
|
||||
<form action="@path/register" method="POST" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<fieldset>
|
||||
<label for="userName" class="strong">Username:</label>
|
||||
<input type="text" name="userName" id="userName" value=""/>
|
||||
<span id="error-userName" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="password" class="strong">
|
||||
Password:
|
||||
</label>
|
||||
<input type="password" name="password" id="password" value=""/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="fullName" class="strong">Full Name:</label>
|
||||
<input type="text" name="fullName" id="fullName" value=""/>
|
||||
<span id="error-fullName" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="mailAddress" class="strong">Mail Address:</label>
|
||||
<input type="text" name="mailAddress" id="mailAddress" value=""/>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="url" class="strong">URL (optional):</label>
|
||||
<input type="text" name="url" id="url" style="width: 400px;" value=""/>
|
||||
<span id="error-url" class="error"></span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<fieldset>
|
||||
<label for="avatar" class="strong">Image (optional):</label>
|
||||
@helper.html.uploadavatar(None)
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="margin">
|
||||
<input type="submit" class="btn btn-success" value="Create account"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
@@ -1,25 +1,32 @@
|
||||
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
|
||||
@(account: model.Account, groupNames: List[String],
|
||||
repositories: List[service.RepositoryService.RepositoryInfo],
|
||||
isGroupManager: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main(account, groupNames, "repositories"){
|
||||
@main(account, groupNames, "repositories", isGroupManager){
|
||||
@if(repositories.isEmpty){
|
||||
No repositories
|
||||
} else {
|
||||
@repositories.map { repository =>
|
||||
<div class="block">
|
||||
<div class="block-header">
|
||||
<a href="@url(repository)">@repository.name</a>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
<div class="repository-icon">
|
||||
@helper.html.repositoryicon(repository, true)
|
||||
</div>
|
||||
<div class="repository-content">
|
||||
<div class="block-header">
|
||||
<a href="@url(repository)">@repository.name</a>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
</div>
|
||||
@if(repository.repository.originUserName.isDefined){
|
||||
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
|
||||
}
|
||||
@if(repository.repository.description.isDefined){
|
||||
<div>@repository.repository.description</div>
|
||||
}
|
||||
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
|
||||
</div>
|
||||
@if(repository.repository.originUserName.isDefined){
|
||||
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
|
||||
}
|
||||
@if(repository.repository.description.isDefined){
|
||||
<div>@repository.repository.description</div>
|
||||
}
|
||||
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
47
src/main/twirl/account/ssh.scala.html
Normal file
47
src/main/twirl/account/ssh.scala.html
Normal file
@@ -0,0 +1,47 @@
|
||||
@(account: model.Account, sshKeys: List[model.SshKey])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("SSH Keys"){
|
||||
<div class="container">
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
@menu("ssh", settings.ssh)
|
||||
</div>
|
||||
<div class="span9">
|
||||
<div class="box">
|
||||
<div class="box-header">SSH Keys</div>
|
||||
<div class="box-content">
|
||||
@if(sshKeys.isEmpty){
|
||||
No keys
|
||||
}
|
||||
@sshKeys.zipWithIndex.map { case (key, i) =>
|
||||
@if(i != 0){
|
||||
<hr>
|
||||
}
|
||||
<strong>@key.title</strong> (@_root_.ssh.SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
|
||||
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-mini btn-danger pull-right">Delete</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
||||
<div class="box">
|
||||
<div class="box-header">Add an SSH Key</div>
|
||||
<div class="box-content">
|
||||
<fieldset>
|
||||
<label for="title" class="strong">Title</label>
|
||||
<div><span id="error-title" class="error"></span></div>
|
||||
<input type="text" name="title" id="title" style="width: 400px;"/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="publicKey" class="strong">Key</label>
|
||||
<div><span id="error-publicKey" class="error"></span></div>
|
||||
<textarea name="publicKey" id="publicKey" style="width: 600px; height: 250px;"></textarea>
|
||||
</fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Add"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
@(active: String)(body: Html)(implicit context: app.Context)
|
||||
@import context._
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<div class="box">
|
||||
<ul class="nav nav-tabs nav-stacked side-menu">
|
||||
<li@if(active=="users"){ class="active"}>
|
||||
<a href="@path/admin/users">User Management</a>
|
||||
</li>
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<div class="box">
|
||||
<ul class="nav nav-tabs nav-stacked side-menu">
|
||||
<li@if(active=="users"){ class="active"}>
|
||||
<a href="@path/admin/users">User Management</a>
|
||||
</li>
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span9">
|
||||
@body
|
||||
</div>
|
||||
</div>
|
||||
<div class="span9">
|
||||
@body
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
|
||||
@(info: Option[Any])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import util.Directory._
|
||||
@import view.helpers._
|
||||
@@ -15,6 +15,22 @@
|
||||
<label class="strong">GITBUCKET_HOME</label>
|
||||
@GitBucketHome
|
||||
<!--====================================================================-->
|
||||
<!-- Base URL -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
|
||||
<fieldset>
|
||||
<div class="controls">
|
||||
<input type="text" name="baseUrl" id="baseUrl" style="width: 400px" value="@settings.baseUrl"/>
|
||||
<span id="error-baseUrl" class="error"></span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p class="muted">
|
||||
The base URL is used for redirect, notification email, git repository URL box and more.
|
||||
If the base URL is empty, GitBucket generates URL from request information.
|
||||
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
|
||||
</p>
|
||||
<!--====================================================================-->
|
||||
<!-- Account registration -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
@@ -41,6 +57,29 @@
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- SSH access -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">SSH access</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="ssh" name="ssh"@if(settings.ssh){ checked}/>
|
||||
Enable SSH access to git repository
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="form-horizontal ssh">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="sshPort">SSH Port</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="sshPort" name="sshPort" value="@settings.sshPort"/>
|
||||
<span id="error-sshPort" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Base URL is required if SSH access is enabled.
|
||||
</p>
|
||||
<!--====================================================================-->
|
||||
<!-- Authentication -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
@@ -191,6 +230,10 @@
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#ssh').change(function(){
|
||||
$('.ssh input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
|
||||
$('#notification').change(function(){
|
||||
$('.notification input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
|
||||
@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(if(account.isEmpty) "New Group" else "Update Group"){
|
||||
@admin.html.menu("users"){
|
||||
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span7">
|
||||
<div class="span5">
|
||||
<fieldset>
|
||||
<label for="groupName" class="strong">Group name</label>
|
||||
<div>
|
||||
@@ -24,29 +24,23 @@
|
||||
<div>
|
||||
<span id="error-url" class="error"></span>
|
||||
</div>
|
||||
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
|
||||
<input type="text" name="url" id="url" value="@account.map(_.url)"/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="avatar" class="strong">Image (Optional)</label>
|
||||
@helper.html.uploadavatar(account)
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="span5">
|
||||
<div class="span7">
|
||||
<fieldset>
|
||||
<label class="strong">Members</label>
|
||||
<ul id="members" class="collaborator">
|
||||
@members.map { userName =>
|
||||
<li data-name="@userName">
|
||||
<a href="@path/@url(userName)">@userName</a>
|
||||
<a href="#" class="remove">(remove)</a>
|
||||
</li>
|
||||
}
|
||||
<ul id="member-list" class="collaborator">
|
||||
</ul>
|
||||
@helper.html.account("memberName", 200)
|
||||
<input type="button" class="btn" value="Add" id="addMember"/>
|
||||
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
|
||||
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
|
||||
<div>
|
||||
<span class="error" id="error-memberName"></span>
|
||||
<span class="error" id="error-members"></span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -60,6 +54,10 @@
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('input[type=submit]').click(function(){
|
||||
updateMembers();
|
||||
});
|
||||
|
||||
$('#addMember').click(function(){
|
||||
$('#error-memberName').text('');
|
||||
var userName = $('#memberName').val();
|
||||
@@ -70,7 +68,7 @@ $(function(){
|
||||
}
|
||||
|
||||
// check duplication
|
||||
var exists = $('#members li').filter(function(){
|
||||
var exists = $('#member-list li').filter(function(){
|
||||
return $(this).data('name') == userName;
|
||||
}).length > 0;
|
||||
if(exists){
|
||||
@@ -83,19 +81,7 @@ $(function(){
|
||||
'userName': userName
|
||||
}, function(data, status){
|
||||
if(data == 'true'){
|
||||
// add member
|
||||
$('#members').append($('<li>')
|
||||
.data('name', userName)
|
||||
.append($('<a>').attr('href', '@path/' + userName).text(userName))
|
||||
.append(' ')
|
||||
.append($('<a>').attr('href', '#').addClass('remove').text('(remove)')));
|
||||
$('#memberName').val('');
|
||||
|
||||
// update hidden value
|
||||
var userNames = $('#members li').map(function(i, e){
|
||||
return $(e).data('name');
|
||||
}).get().join(',');
|
||||
$('#memberNames').val(userNames);
|
||||
addMemberHTML(userName, false);
|
||||
} else {
|
||||
$('#error-memberName').text('User does not exist.');
|
||||
}
|
||||
@@ -103,20 +89,47 @@ $(function(){
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove', function(){
|
||||
// remove member
|
||||
$(this).parent().remove();
|
||||
|
||||
// update hidden value
|
||||
var userNames = $('#members li').map(function(i, e){
|
||||
return $(e).data('name');
|
||||
}).get().join(',');
|
||||
$('#memberNames').val(userNames);
|
||||
});
|
||||
|
||||
// Don't submit form by ENTER key
|
||||
$('#memberName').keypress(function(e){
|
||||
console.log(e.keyCode);
|
||||
return !(e.keyCode == 13);
|
||||
});
|
||||
|
||||
@members.map { member =>
|
||||
addMemberHTML('@member.userName', @member.isManager);
|
||||
}
|
||||
|
||||
function addMemberHTML(userName, isManager){
|
||||
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
|
||||
if(!isManager){
|
||||
memberButton.addClass('active');
|
||||
}
|
||||
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
|
||||
if(isManager){
|
||||
managerButton.addClass('active');
|
||||
}
|
||||
|
||||
$('#member-list').append($('<li>')
|
||||
.data('name', userName)
|
||||
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
|
||||
.append(memberButton)
|
||||
.append(managerButton))
|
||||
.append(' ')
|
||||
.append($('<a>').attr('href', '@path/' + userName).text(userName))
|
||||
.append(' ')
|
||||
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
|
||||
}
|
||||
|
||||
function updateMembers(){
|
||||
var members = $('#member-list li').map(function(i, e){
|
||||
var userName = $(e).data('name');
|
||||
return userName + ':' + $('button.active').filter(function(i, e){
|
||||
return $(e).data('name') == userName;
|
||||
}).attr('value');
|
||||
}).get().join(',');
|
||||
$('#members').val(members);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -8,41 +8,43 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Your Issues"){
|
||||
@dashboard.html.tab("issues")
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "all"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/repos@condition.toURL">
|
||||
<span class="count-right">@allCount</span>
|
||||
In your repositories
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "assigned"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/assigned@condition.toURL">
|
||||
<span class="count-right">@assignedCount</span>
|
||||
Assigned to you
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/created_by@condition.toURL">
|
||||
<span class="count-right">@createdByCount</span>
|
||||
Created by you
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@repositories.map { case (owner, name, count) =>
|
||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
||||
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
|
||||
<span class="count-right">@count</span>
|
||||
@owner/@name
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="container">
|
||||
@dashboard.html.tab("issues")
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "all"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/repos@condition.toURL">
|
||||
<span class="count-right">@allCount</span>
|
||||
In your repositories
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "assigned"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/assigned@condition.toURL">
|
||||
<span class="count-right">@assignedCount</span>
|
||||
Assigned to you
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/created_by@condition.toURL">
|
||||
<span class="count-right">@createdByCount</span>
|
||||
Created by you
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@repositories.map { case (owner, name, count) =>
|
||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
||||
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
|
||||
<span class="count-right">@count</span>
|
||||
@owner/@name
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@listparts
|
||||
</div>
|
||||
@listparts
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -6,35 +6,37 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Your Issues"){
|
||||
@dashboard.html.tab("pulls")
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/owned@condition.toURL">
|
||||
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
|
||||
Yours
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "not_created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/public@condition.toURL">
|
||||
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
|
||||
Public
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@repositories.map { case (owner, name, count) =>
|
||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/for/@owner/@name">
|
||||
<span class="count-right">@count</span>
|
||||
@owner/@name
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="container">
|
||||
@dashboard.html.tab("pulls")
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/owned@condition.toURL">
|
||||
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
|
||||
Yours
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "not_created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/public@condition.toURL">
|
||||
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
|
||||
Public
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@repositories.map { case (owner, name, count) =>
|
||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/for/@owner/@name">
|
||||
<span class="count-right">@count</span>
|
||||
@owner/@name
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@listparts
|
||||
</div>
|
||||
@listparts
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
@(active: String = "")(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<ul class="nav nav-tabs">
|
||||
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
|
||||
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
|
||||
}
|
||||
@if(active == ""){
|
||||
<li class="pull-right"><a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a></li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
|
||||
@if(repository.commitCount > 0){
|
||||
<div class="pull-right">
|
||||
<div class="input-prepend">
|
||||
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
|
||||
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="head">
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
@if(!repository.repository.isPrivate){
|
||||
<i class="icon-eye-open"></i>
|
||||
}
|
||||
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
|
||||
|
||||
@defining(repository.repository){ x =>
|
||||
@if(repository.repository.originRepositoryName.isDefined){
|
||||
<div class="forked">
|
||||
forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<table class="global-nav box-header">
|
||||
<tr>
|
||||
<th class="box-header@if(active=="code"){ active}">
|
||||
<a href="@url(repository)">Code</a>
|
||||
</th>
|
||||
<th class="box-header@if(active=="issues"){ active}">
|
||||
<a href="@url(repository)/issues">Issues</a>
|
||||
@if(repository.issueCount > 0){
|
||||
<span class="badge">@repository.issueCount</span>
|
||||
}
|
||||
</th>
|
||||
<th class="box-header@if(active=="pulls"){ active}">
|
||||
<a href="@url(repository)/pulls">Pull Requests</a>
|
||||
@if(repository.pullCount > 0){
|
||||
<span class="badge">@repository.pullCount</span>
|
||||
}
|
||||
</th>
|
||||
<th class="box-header@if(active=="wiki"){ active}">
|
||||
<a href="@url(repository)/wiki">Wiki</a>
|
||||
</th>
|
||||
<th class="box-header@if(active=="network"){ active}">
|
||||
<a href="@url(repository)/network/members">Network</a>
|
||||
</th>
|
||||
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){
|
||||
<th class="box-header@if(active=="settings"){ active}">
|
||||
<a href="@url(repository)/settings">Settings</a>
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</table>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
$('table.global-nav th.box-header').click(function(){
|
||||
location.href = $(this).find('a').attr('href');
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
27
src/main/twirl/helper/attached.scala.html
Normal file
27
src/main/twirl/helper/attached.scala.html
Normal file
@@ -0,0 +1,27 @@
|
||||
@(owner: String, repository: String)(textarea: Html)(implicit context: app.Context)
|
||||
@import context._
|
||||
<div class="muted attachable">
|
||||
@textarea
|
||||
<div class="clickable">Attach images by dragging & dropping, or selecting them.</div>
|
||||
</div>
|
||||
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
|
||||
<script>
|
||||
$(function(){
|
||||
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
|
||||
url: '@path/upload/image/@owner/@repository',
|
||||
maxFilesize: 10,
|
||||
acceptedFiles: 'image/*',
|
||||
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.',
|
||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
success: function(file, id) {
|
||||
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
|
||||
$('#@textareaId').val($('#@textareaId').val() + images);
|
||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Adjust clickable area width
|
||||
$('#@textareaId').next('div.clickable').css('width', ($('#@textareaId').width() + 8) + 'px');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@(id: String, value: String)(html: Html)
|
||||
<div class="input-append">
|
||||
<div class="input-append" style="margin-bottom: 0px;">
|
||||
@html
|
||||
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
|
||||
</div>
|
||||
|
||||
@@ -81,56 +81,7 @@
|
||||
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script>
|
||||
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script>
|
||||
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
|
||||
<style type="text/css">
|
||||
table.inlinediff {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.inlinediff thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td.insert, td.equal, td.delete {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function diffUsingJS(oldTextId, newTextId, outputId) {
|
||||
// get the baseText and newText values from the two textboxes, and split them into lines
|
||||
var oldText = document.getElementById(oldTextId).value;
|
||||
if(oldText == ''){
|
||||
var oldLines = [];
|
||||
} else {
|
||||
var oldLines = difflib.stringAsLines(oldText);
|
||||
}
|
||||
|
||||
var newText = document.getElementById(newTextId).value
|
||||
if(newText == ''){
|
||||
var newLines = [];
|
||||
} else {
|
||||
var newLines = difflib.stringAsLines(newText);
|
||||
}
|
||||
|
||||
// create a SequenceMatcher instance that diffs the two sets of lines
|
||||
var sm = new difflib.SequenceMatcher(oldLines, newLines);
|
||||
|
||||
// get the opcodes from the SequenceMatcher instance
|
||||
// opcodes is a list of 3-tuples describing what changes should be made to the base text
|
||||
// in order to yield the new text
|
||||
var opcodes = sm.get_opcodes();
|
||||
var diffoutputdiv = document.getElementById(outputId);
|
||||
while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild);
|
||||
|
||||
// build the diff view and add it to the current DOM
|
||||
diffoutputdiv.appendChild(diffview.buildView({
|
||||
baseTextLines: oldLines,
|
||||
newTextLines: newLines,
|
||||
opcodes: opcodes,
|
||||
contextSize: 4,
|
||||
viewType: 1
|
||||
}));
|
||||
}
|
||||
|
||||
$(function(){
|
||||
@if(showIndex){
|
||||
$('#toggle-file-list').click(function(){
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "")(body: Html)
|
||||
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html)
|
||||
<div class="btn-group"@if(style.nonEmpty){ style="@style"}>
|
||||
<button class="btn dropdown-toggle@if(mini){ btn-mini}" data-toggle="dropdown">
|
||||
<button class="btn dropdown-toggle@if(mini){ btn-mini} else { btn-small}" data-toggle="dropdown">
|
||||
@if(value.isEmpty){
|
||||
<i class="icon-cog"></i>
|
||||
} else {
|
||||
@@ -11,7 +11,7 @@
|
||||
}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<ul class="dropdown-menu@if(right){ pull-right}">
|
||||
@body
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
27
src/main/twirl/helper/feed.scala.xml
Normal file
27
src/main/twirl/helper/feed.scala.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
@(activities: List[model.Activity])(implicit context: app.Context)<?xml version="1.0" encoding="UTF-8"?>
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
|
||||
<id>tag:@context.host,2013:gitbucket</id>
|
||||
<title>Gitbucket's activities</title>
|
||||
<link type="application/atom+xml" rel="self" href="@context.baseUrl/activities.atom"/>
|
||||
<author>
|
||||
<name>Gitbucket</name>
|
||||
<uri>@context.baseUrl</uri>
|
||||
</author>
|
||||
<updated>@datetimeRFC3339(if(activities.isEmpty) new java.util.Date else activities.map(_.activityDate).max)</updated>
|
||||
@activities.map { activity =>
|
||||
<entry>
|
||||
<id>tag:@context.host,@date(activity.activityDate):activity:@activity.activityId</id>
|
||||
<published>@datetimeRFC3339(activity.activityDate)</published>
|
||||
<updated>@datetimeRFC3339(activity.activityDate)</updated>
|
||||
<link type="text/html" rel="alternate" href="@context.baseUrl/@activity.userName/@activity.repositoryName" />
|
||||
<title type="html">@activity.activityType</title>
|
||||
<author>
|
||||
<name>@activity.activityUserName</name>
|
||||
<uri>@url(activity.activityUserName)</uri>
|
||||
</author>
|
||||
<content type="html">@activityMessage(activity.message)</content>
|
||||
</entry>
|
||||
}
|
||||
</feed>
|
||||
@@ -3,19 +3,21 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-tabs">
|
||||
<ul class="nav nav-tabs" style="height: 37px;">
|
||||
<li class="active"><a href="#tab1" data-toggle="tab">Write</a></li>
|
||||
<li><a href="#tab2" data-toggle="tab" id="preview">Preview</a></li>
|
||||
@*
|
||||
<li class="pull-right">
|
||||
<a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Syntax Guide</a>
|
||||
</li>
|
||||
*@
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="tab1">
|
||||
<span id="error-content" class="error"></span>
|
||||
<textarea id="content" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
|
||||
@textarea = {
|
||||
<textarea id="content" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
|
||||
}
|
||||
@if(enableWikiLink){
|
||||
@textarea
|
||||
} else {
|
||||
@helper.html.attached(repository.owner, repository.name)(textarea)
|
||||
}
|
||||
</div>
|
||||
<div class="tab-pane" id="tab2">
|
||||
<div class="markdown-body" id="preview-area">
|
||||
|
||||
12
src/main/twirl/helper/repositoryicon.scala.html
Normal file
12
src/main/twirl/helper/repositoryicon.scala.html
Normal file
@@ -0,0 +1,12 @@
|
||||
@(repository: service.RepositoryService.RepositoryInfo, large: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@if(repository.repository.isPrivate){
|
||||
<img src="@assets/common/images/repo_private@{if(large){"_lg"}}.png"/>
|
||||
} else {
|
||||
@if(repository.repository.originUserName.isDefined){
|
||||
<img src="@assets/common/images/repo_fork@{if(large){"_lg"}}.png"/>
|
||||
} else {
|
||||
<img src="@assets/common/images/repo_public@{if(large){"_lg"}}.png"/>
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
@if(account.nonEmpty && account.get.image.nonEmpty){
|
||||
<img src="@path/@account.get.userName/_avatar" style="with: 120px; height: 120px;"/>
|
||||
} else {
|
||||
<div id="clickable">
|
||||
<a href="https://www.gravatar.com/" target="_blank">Gravatar</a> is used
|
||||
</div>
|
||||
<div id="clickable">Upload Image</div>
|
||||
}
|
||||
</div>
|
||||
@if(account.nonEmpty && account.get.image.nonEmpty){
|
||||
@@ -21,7 +19,6 @@ $(function(){
|
||||
var dropzone = new Dropzone('div#clickable', {
|
||||
url: '@path/upload/image',
|
||||
previewsContainer: 'div#avatar',
|
||||
paramName: 'file',
|
||||
parallelUploads: 1,
|
||||
thumbnailWidth: 120,
|
||||
thumbnailHeight: 120
|
||||
|
||||
@@ -1,81 +1,70 @@
|
||||
@(activities: List[model.Activity],
|
||||
recentRepositories: List[service.RepositoryService.RepositoryInfo],
|
||||
systemSettings: service.SystemSettingsService.SystemSettings,
|
||||
userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main("GitBucket"){
|
||||
@dashboard.html.tab()
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
@helper.html.activities(activities)
|
||||
</div>
|
||||
<div class="span4">
|
||||
@if(loginAccount.isEmpty){
|
||||
@signinform(systemSettings)
|
||||
} else {
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th class="metal">
|
||||
<div class="pull-right">
|
||||
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
|
||||
</div>
|
||||
Your repositories (@userRepositories.size)
|
||||
</th>
|
||||
</tr>
|
||||
@if(userRepositories.isEmpty){
|
||||
<tr>
|
||||
<td>No repositories</td>
|
||||
</tr>
|
||||
<div class="container">
|
||||
@dashboard.html.tab()
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
@helper.html.activities(activities)
|
||||
</div>
|
||||
<div class="span4">
|
||||
@if(loginAccount.isEmpty){
|
||||
@signinform(settings)
|
||||
} else {
|
||||
@userRepositories.map { repository =>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<td>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
@if(!repository.repository.isPrivate){
|
||||
<i class="icon-eye-open"></i>
|
||||
}
|
||||
@if(repository.owner == loginAccount.get.userName){
|
||||
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
||||
} else {
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
}
|
||||
</td>
|
||||
<th class="metal">
|
||||
<div class="pull-right">
|
||||
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
|
||||
</div>
|
||||
Your repositories (@userRepositories.size)
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
@if(userRepositories.isEmpty){
|
||||
<tr>
|
||||
<td>No repositories</td>
|
||||
</tr>
|
||||
} else {
|
||||
@userRepositories.map { repository =>
|
||||
<tr>
|
||||
<td>
|
||||
@helper.html.repositoryicon(repository, false)
|
||||
@if(repository.owner == loginAccount.get.userName){
|
||||
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
||||
} else {
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th class="metal">
|
||||
Recent updated repositories
|
||||
</th>
|
||||
</tr>
|
||||
@if(recentRepositories.isEmpty){
|
||||
<tr>
|
||||
<td>No repositories</td>
|
||||
</tr>
|
||||
} else {
|
||||
@recentRepositories.map { repository =>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<td>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
@if(!repository.repository.isPrivate){
|
||||
<i class="icon-eye-open"></i>
|
||||
}
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
</td>
|
||||
<th class="metal">
|
||||
Recent updated repositories
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
@if(recentRepositories.isEmpty){
|
||||
<tr>
|
||||
<td>No repositories</td>
|
||||
</tr>
|
||||
} else {
|
||||
@recentRepositories.map { repository =>
|
||||
<tr>
|
||||
<td>
|
||||
@helper.html.repositoryicon(repository, false)
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-comment-box">
|
||||
<div class="box-content">
|
||||
@helper.html.preview(repository, "", false, true, "width: 680px; height: 100px; max-height: 150px;", elastic = true)
|
||||
@helper.html.preview(repository, "", false, true, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
|
||||
@@ -6,15 +6,21 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@comments.map { comment =>
|
||||
@if(comment.action != "close" && comment.action != "reopen"){
|
||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
|
||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
||||
<div class="box-header-small">
|
||||
<i class="icon-comment"></i>
|
||||
@user(comment.commentedUserName, styleClass="username strong") commented
|
||||
@user(comment.commentedUserName, styleClass="username strong")
|
||||
@if(comment.action == "comment"){
|
||||
commented
|
||||
} else {
|
||||
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
|
||||
}
|
||||
<span class="pull-right">
|
||||
@datetime(comment.registeredDate)
|
||||
@if(comment.action != "commit" && comment.action != "merge" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
|
||||
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
||||
}
|
||||
@@ -27,7 +33,13 @@
|
||||
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true)
|
||||
}
|
||||
} else {
|
||||
@markdown(comment.content, repository, false, true)
|
||||
@if(comment.action == "refer"){
|
||||
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
|
||||
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
|
||||
}
|
||||
} else {
|
||||
@markdown(comment.content, repository, false, true)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,6 +75,13 @@
|
||||
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "delete_branch"){
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label">Deleted</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @datetime(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
@@ -6,85 +6,86 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.header("issues", repository)
|
||||
@tab("", repository)
|
||||
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span9">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-box">
|
||||
<div class="box-content">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" name="title" value="" placeholder="Title" style="width: 600px;"/>
|
||||
<div>
|
||||
<span id="label-assigned">No one is assigned</span>
|
||||
@if(hasWritePermission){
|
||||
<input type="hidden" name="assignedUserName" value=""/>
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
<div class="pull-right">
|
||||
<span id="label-milestone">No milestone</span>
|
||||
@html.menu("issues", repository){
|
||||
@tab("", true, repository)
|
||||
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span9">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-box">
|
||||
<div class="box-content">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;"/>
|
||||
<div>
|
||||
<span id="label-assigned">No one is assigned</span>
|
||||
@if(hasWritePermission){
|
||||
<input type="hidden" name="milestoneId" value=""/>
|
||||
<input type="hidden" name="assignedUserName" value=""/>
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
|
||||
@milestones.map { milestone =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
<i class="icon-while"></i> @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
<div class="pull-right">
|
||||
<span id="label-milestone">No milestone</span>
|
||||
@if(hasWritePermission){
|
||||
<input type="hidden" name="milestoneId" value=""/>
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
<i class="icon-while"></i> @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
@helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span3">
|
||||
@if(hasWritePermission){
|
||||
<span class="strong">Add Labels</span>
|
||||
<div>
|
||||
<div id="label-list">
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-label" data-label="@label.labelName" data-bgcolor="@label.color" data-fgcolor="@label.fontColor">
|
||||
<span style="background-color: #@label.color;" class="label-color"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<input type="hidden" name="labelNames" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px; max-height: 250px;", elastic = true)
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="span3">
|
||||
@if(hasWritePermission){
|
||||
<span class="strong">Add Labels</span>
|
||||
<div>
|
||||
<div id="label-list">
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-label" data-label="@label.labelName" data-bgcolor="@label.color" data-fgcolor="@label.fontColor">
|
||||
<span style="background-color: #@label.color;" class="label-color"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<input type="hidden" name="labelNames" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
<span id="error-edit-content-@commentId" class="error"></span>
|
||||
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||
@helper.html.attached(owner, repository){
|
||||
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="update-comment-@commentId" class="btn btn-small" value="Update Comment"/>
|
||||
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger pull-right" value="Cancel"/>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
@(title: String, content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
<span id="error-edit-title" class="error"></span>
|
||||
<input type="text" style="width: 680px;" id="edit-title" value="@title"/>
|
||||
<textarea style="width: 680px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
|
||||
<input type="text" style="width: 635px;" id="edit-title" value="@title"/>
|
||||
@helper.html.attached(owner, repository){
|
||||
<textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="update" class="btn btn-small" value="Update Issue"/>
|
||||
<input type="button" id="cancel" class="btn btn-small btn-danger pull-right" value="Cancel"/>
|
||||
|
||||
@@ -9,75 +9,32 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.header("issues", repository)
|
||||
@tab("issues", repository)
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="pull-left"><a href="@url(repository)/issues"><i class="icon-arrow-left"></i> Back to issue list</a></li>
|
||||
<li class="pull-right">Issue #@issue.issueId</li>
|
||||
</ul>
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
|
||||
@commentlist(issue, comments, hasWritePermission, repository)
|
||||
@commentform(issue, hasWritePermission, repository)
|
||||
</div>
|
||||
<div class="span2">
|
||||
@if(issue.closed) {
|
||||
<span class="label label-important issue-status">Closed</span>
|
||||
} else {
|
||||
<span class="label label-success issue-status">Open</span>
|
||||
}
|
||||
<div class="small" style="text-align: center;">
|
||||
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
|
||||
<span class="strong">@count</span> @plural(count, "comment")
|
||||
}
|
||||
@html.menu("issues", repository){
|
||||
@tab("issues", false, repository)
|
||||
<ul class="nav nav-tabs pull-left fill-width">
|
||||
<li class="pull-left"><a href="@url(repository)/issues"><i class="icon-arrow-left"></i> Back to issue list</a></li>
|
||||
<li class="pull-right">Issue #@issue.issueId</li>
|
||||
</ul>
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
|
||||
@commentlist(issue, comments, hasWritePermission, repository)
|
||||
@commentform(issue, hasWritePermission, repository)
|
||||
</div>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="strong">Labels</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown() {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
||||
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
|
||||
<span class="label" style="background-color: #@label.color;"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<div class="span2">
|
||||
@if(issue.closed) {
|
||||
<span class="label label-important issue-status">Closed</span>
|
||||
} else {
|
||||
<span class="label label-success issue-status">Open</span>
|
||||
}
|
||||
<div class="small" style="text-align: center;">
|
||||
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
|
||||
<span class="strong">@count</span> @plural(count, "comment")
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<hr/>
|
||||
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labellist(issueLabels)
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('a.toggle-label').click(function(){
|
||||
var path, icon;
|
||||
var i = $(this).children('i');
|
||||
if(i.hasClass('icon-ok')){
|
||||
path = 'delete';
|
||||
icon = 'icon-white';
|
||||
} else {
|
||||
path = 'new';
|
||||
icon = 'icon-ok';
|
||||
}
|
||||
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
|
||||
{
|
||||
labelId : $(this).data('label-id')
|
||||
},
|
||||
function(data){
|
||||
i.removeClass().addClass(icon);
|
||||
$('ul.label-list').empty().html(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -54,7 +54,7 @@
|
||||
@if(hasWritePermission){
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
|
||||
@milestones.map { case (milestone, _, _) =>
|
||||
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
|
||||
|
||||
51
src/main/twirl/issues/labels.scala.html
Normal file
51
src/main/twirl/issues/labels.scala.html
Normal file
@@ -0,0 +1,51 @@
|
||||
@(issue: model.Issue,
|
||||
issueLabels: List[model.Label],
|
||||
labels: List[model.Label],
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import view.helpers._
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="strong">Labels</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown(right = true) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
||||
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
|
||||
<span class="label" style="background-color: #@label.color;"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labellist(issueLabels)
|
||||
</ul>
|
||||
<script>
|
||||
$(function(){
|
||||
$('a.toggle-label').click(function(){
|
||||
var path, icon;
|
||||
var i = $(this).children('i');
|
||||
if(i.hasClass('icon-ok')){
|
||||
path = 'delete';
|
||||
icon = 'icon-white';
|
||||
} else {
|
||||
path = 'new';
|
||||
icon = 'icon-ok';
|
||||
}
|
||||
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
|
||||
{
|
||||
labelId : $(this).data('label-id')
|
||||
},
|
||||
function(data){
|
||||
i.removeClass().addClass(icon);
|
||||
$('ul.label-list').empty().html(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -16,129 +16,130 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.header("issues", repository)
|
||||
@tab("issues", repository)
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "all"){ class="active"}>
|
||||
<a href="@url(repository)/issues@condition.toURL">
|
||||
<span class="count-right">@allCount</span>
|
||||
Everyone's Issues
|
||||
</a>
|
||||
</li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li@if(filter == "assigned"){ class="active"}>
|
||||
<a href="@url(repository)/issues/assigned/@loginAccount.map(_.userName)@condition.toURL">
|
||||
<span class="count-right">@assignedCount</span>
|
||||
Assigned to you
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@url(repository)/issues/created_by/@loginAccount.map(_.userName)@condition.toURL">
|
||||
<span class="count-right">@createdByCount</span>
|
||||
Created by you
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<hr/>
|
||||
@if(condition.milestoneId.isEmpty){
|
||||
<span class="muted small">No milestone selected</span>
|
||||
} else {
|
||||
@if(condition.milestoneId.get.isEmpty){
|
||||
<span class="muted small">Issues with no milestone</span>
|
||||
@html.menu("issues", repository){
|
||||
@tab("issues", false, repository)
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "all"){ class="active"}>
|
||||
<a href="@url(repository)/issues@condition.toURL">
|
||||
<span class="count-right">@allCount</span>
|
||||
Everyone's Issues
|
||||
</a>
|
||||
</li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li@if(filter == "assigned"){ class="active"}>
|
||||
<a href="@url(repository)/issues/assigned/@loginAccount.map(_.userName)@condition.toURL">
|
||||
<span class="count-right">@assignedCount</span>
|
||||
Assigned to you
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@url(repository)/issues/created_by/@loginAccount.map(_.userName)@condition.toURL">
|
||||
<span class="count-right">@createdByCount</span>
|
||||
Created by you
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<hr/>
|
||||
@if(condition.milestoneId.isEmpty){
|
||||
<span class="muted small">No milestone selected</span>
|
||||
} else {
|
||||
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown() {
|
||||
@if(condition.milestoneId.isDefined){
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = None).toURL">
|
||||
<i class="icon-remove-circle"></i> Clear milestone filter
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(None)).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
|
||||
</a>
|
||||
</li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
|
||||
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
|
||||
<div style="margin-top: 4px;">
|
||||
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
|
||||
</div>
|
||||
<span class="muted small">@openCount open issues</span>
|
||||
@if(milestone.closedDate.isDefined){
|
||||
@milestone.closedDate.map { closedDate =>
|
||||
<span class="small">Closed in @date(closedDate)</span>
|
||||
}
|
||||
@if(condition.milestoneId.get.isEmpty){
|
||||
<span class="muted small">Issues with no milestone</span>
|
||||
} else {
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="small">Due in @date(dueDate)</span>
|
||||
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown() {
|
||||
@if(condition.milestoneId.isDefined){
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = None).toURL">
|
||||
<i class="icon-remove-circle"></i> Clear milestone filter
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(None)).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
|
||||
</a>
|
||||
</li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
|
||||
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
|
||||
<div style="margin-top: 4px;">
|
||||
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
|
||||
</div>
|
||||
<span class="muted small">@openCount open issues</span>
|
||||
@if(milestone.closedDate.isDefined){
|
||||
@milestone.closedDate.map { closedDate =>
|
||||
<span class="small">Closed in @date(closedDate)</span>
|
||||
}
|
||||
} else {
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="small">Due in @date(dueDate)</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<hr/>
|
||||
<span class="strong">Labels</span>
|
||||
<div>
|
||||
<div id="label-list">
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL"
|
||||
@if(condition.labels.contains(label.labelName)){style="background-color: #@label.color; color: #@label.fontColor;"}>
|
||||
<span class="count-right">@labelCounts.getOrElse(label.labelName, 0)</span>
|
||||
<span style="background-color: #@label.color;" class="label-color"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
<hr/>
|
||||
<input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/>
|
||||
<br/>
|
||||
<span class="strong">New label</span>
|
||||
@_root_.issues.labels.html.edit(None, repository)
|
||||
}
|
||||
<span class="strong">Labels</span>
|
||||
<div>
|
||||
<div id="label-list">
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL"
|
||||
@if(condition.labels.contains(label.labelName)){style="background-color: #@label.color; color: #@label.fontColor;"}>
|
||||
<span class="count-right">@labelCounts.getOrElse(label.labelName, 0)</span>
|
||||
<span style="background-color: #@label.color;" class="label-color"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
<hr/>
|
||||
<input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/>
|
||||
<br/>
|
||||
<span class="strong">New label</span>
|
||||
@_root_.issues.labels.html.edit(None, repository)
|
||||
}
|
||||
</div>
|
||||
@***** show issue list *****@
|
||||
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
||||
</div>
|
||||
@***** show issue list *****@
|
||||
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
<form id="batcheditForm" method="POST">
|
||||
<input type="hidden" name="value"/>
|
||||
<input type="hidden" name="checked"/>
|
||||
</form>
|
||||
@if(hasWritePermission){
|
||||
<form id="batcheditForm" method="POST">
|
||||
<input type="hidden" name="value"/>
|
||||
<input type="hidden" name="checked"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if(hasWritePermission){
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
|
||||
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
|
||||
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
|
||||
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
|
||||
</div>
|
||||
@helper.html.dropdown(
|
||||
value = (condition.sort, condition.direction) match {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user