Compare commits
180 Commits
1.7
...
maintenanc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7deea43c75 | ||
|
|
8842e1fef2 | ||
|
|
5090112c39 | ||
|
|
d4a3e88f1a | ||
|
|
acada42d3f | ||
|
|
b6cfbcd19c | ||
|
|
0961eb5976 | ||
|
|
153244c390 | ||
|
|
e97b5c3c89 | ||
|
|
374893a5ae | ||
|
|
17f581f654 | ||
|
|
590b431ec1 | ||
|
|
98266fe0e1 | ||
|
|
b620307983 | ||
|
|
2c14dfb781 | ||
|
|
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 | ||
|
|
b885a1a0d4 | ||
|
|
1705bd3ae9 | ||
|
|
e87c69f989 | ||
|
|
1c529eea3d | ||
|
|
738b0cfe9a | ||
|
|
913561cb2a | ||
|
|
05a91565dc | ||
|
|
79827efe9b | ||
|
|
8722cd89fc | ||
|
|
52fcc4ad1e | ||
|
|
59a096bfd6 | ||
|
|
5a1f541e13 | ||
|
|
94bd1c6a93 | ||
|
|
5b1aef5e52 | ||
|
|
89bfcdc44e | ||
|
|
fba81138ea | ||
|
|
d50e07265e | ||
|
|
c92891538e | ||
|
|
ccc1e9bc8b | ||
|
|
f33b398428 | ||
|
|
226a8af262 | ||
|
|
ebcc5ab4b1 | ||
|
|
10e16e8379 | ||
|
|
df1f3d8a00 | ||
|
|
5e2dfffe25 | ||
|
|
897f2ea6dd | ||
|
|
3ff39ec578 | ||
|
|
3d852a535d | ||
|
|
6f6a61f31a | ||
|
|
10f54f5790 | ||
|
|
0e7280585a | ||
|
|
1da7173f27 | ||
|
|
1cb1e68a01 | ||
|
|
b59c8a5512 | ||
|
|
fe63ad0976 | ||
|
|
941cb7b851 | ||
|
|
d1cf0d9fd7 | ||
|
|
64c2bb4d6b | ||
|
|
24c9f5c17e | ||
|
|
d368e4e80d | ||
|
|
5c0ff84fc4 | ||
|
|
502a21b6b6 | ||
|
|
0e9bf59c0f | ||
|
|
108f9fccdd | ||
|
|
ac884bd7c3 | ||
|
|
a4cb5c991c | ||
|
|
68f1f55f37 | ||
|
|
1dc779d5e8 | ||
|
|
f781c7a08c | ||
|
|
a8511a9f39 | ||
|
|
47714eec45 | ||
|
|
c46e9b2f4d | ||
|
|
26d579f13f | ||
|
|
6556d26742 | ||
|
|
608dce2205 | ||
|
|
f86e50c723 | ||
|
|
b60fe33886 | ||
|
|
5210a143fd | ||
|
|
6b11c1a180 | ||
|
|
b3669f6d66 | ||
|
|
bbff75e037 | ||
|
|
7e10618ceb | ||
|
|
7f4def6b83 | ||
|
|
5790d246c8 | ||
|
|
19dee09c86 | ||
|
|
dfe2889912 | ||
|
|
223ba791fe | ||
|
|
0d49bbe7ac | ||
|
|
8381e8122a | ||
|
|
f38924c7fe | ||
|
|
43152c9341 | ||
|
|
cf84e8b7cc | ||
|
|
2b42e73530 | ||
|
|
60030959f2 | ||
|
|
7174523ac5 | ||
|
|
f573fef9eb | ||
|
|
b4250d8254 | ||
|
|
ac4d4de3c1 | ||
|
|
05e6d008fa | ||
|
|
dd4abb2073 | ||
|
|
612aba1365 | ||
|
|
94dce09570 | ||
|
|
cc241c5a7b | ||
|
|
13cf9d01f0 | ||
|
|
47453fec3f | ||
|
|
641d506559 | ||
|
|
3dec2b8159 | ||
|
|
a0bd969140 | ||
|
|
b30d42a37b | ||
|
|
a03acc68e7 | ||
|
|
05296473d3 | ||
|
|
2118f8c764 | ||
|
|
e366af98b5 | ||
|
|
81e2ac44c3 | ||
|
|
07bb326c06 | ||
|
|
bcc2c8cc2d | ||
|
|
2e0e17f1aa | ||
|
|
c517b44e82 | ||
|
|
f311339786 | ||
|
|
34853d0322 | ||
|
|
9c60b69c88 | ||
|
|
4f10bccf84 | ||
|
|
c7eaebf597 | ||
|
|
60e1052d33 | ||
|
|
7e77c102b0 | ||
|
|
a452c582ab | ||
|
|
0d3adb074d | ||
|
|
8ec4b52dda | ||
|
|
9265c68383 | ||
|
|
4bd2d78ecb | ||
|
|
e7aa766d0a | ||
|
|
7d8300b3ce | ||
|
|
af8a1234ed | ||
|
|
bd0ecd0a9d | ||
|
|
35c8f02f90 | ||
|
|
f160952817 | ||
|
|
9e5a302ab1 | ||
|
|
a1dc19fa26 | ||
|
|
e79ded934f | ||
|
|
ef3e7d9286 | ||
|
|
68b25ddbb5 | ||
|
|
f96040eade | ||
|
|
599a808054 | ||
|
|
382c5c55ec | ||
|
|
afb2306904 | ||
|
|
2642da3be3 | ||
|
|
dcbf283c9d | ||
|
|
f38fa0132c | ||
|
|
569053f7e0 | ||
|
|
037a97ff3d |
1
.gitignore
vendored
@@ -14,6 +14,7 @@ project/plugins/project/
|
||||
.classpath
|
||||
.project
|
||||
.cache
|
||||
.settings
|
||||
|
||||
# IntelliJ specific
|
||||
.idea/
|
||||
|
||||
54
README.md
@@ -37,21 +37,67 @@ Installation
|
||||
|
||||
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]
|
||||
- --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
|
||||
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
|
||||
|
||||
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
|
||||
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 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
|
||||
|
||||
### 1.8 - 30 Nov 2013
|
||||
- Add user and group deletion
|
||||
- Improve pull request performance
|
||||
- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
|
||||
- LDAP StartTLS support
|
||||
- Enable hard wrapping in Markdown
|
||||
- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
|
||||
- Fix some bugs
|
||||
|
||||
### 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 use https 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 full name as user property
|
||||
- Change link color for absent Wiki pages
|
||||
- Add ZIP download button to the repository viewer tab
|
||||
|
||||
20
contrib/macosx/gitbucket.plist
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>gitbucket</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/java</string>
|
||||
<string>-Dmail.smtp.starttls.enable=true</string>
|
||||
<string>-jar</string>
|
||||
<string>gitbucket.war</string>
|
||||
<string>--host=127.0.0.1</string>
|
||||
<string>--port=8080</string>
|
||||
<string>--https=true</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: gitbucket
|
||||
Summary: Github clone written with Scala.
|
||||
Version: 1.6
|
||||
Summary: GitHub clone written with Scala.
|
||||
Version: 1.7
|
||||
Release: 1%{?dist}
|
||||
License: Apache
|
||||
URL: https://github.com/takezoe/gitbucket
|
||||
@@ -15,7 +15,7 @@ Requires: java >= 1.7
|
||||
|
||||
%description
|
||||
|
||||
GitBucket is the easily installable Github clone written with Scala.
|
||||
GitBucket is the easily installable GitHub clone written with Scala.
|
||||
|
||||
|
||||
%install
|
||||
@@ -40,5 +40,8 @@ touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
||||
|
||||
|
||||
%changelog
|
||||
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
||||
- Version bump to v1.7.
|
||||
|
||||
* Thu Oct 17 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
||||
- First build.
|
||||
|
||||
242
etc/icons.svg
@@ -25,17 +25,17 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="629.30023"
|
||||
inkscape:cy="281.44758"
|
||||
inkscape:cx="450.21999"
|
||||
inkscape:cy="97.51519"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1-9"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-height="706"
|
||||
inkscape:window-x="1912"
|
||||
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 +746,238 @@
|
||||
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" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 47 KiB |
@@ -1,8 +1,6 @@
|
||||
import sbt._
|
||||
import Keys._
|
||||
import org.scalatra.sbt._
|
||||
import org.scalatra.sbt.PluginKeys._
|
||||
import sbt.ScalaVersion
|
||||
import twirl.sbt.TwirlPlugin._
|
||||
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
||||
|
||||
@@ -25,14 +23,14 @@ object MyBuild extends Build {
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||
),
|
||||
scalacOptions := Seq("-deprecation"),
|
||||
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.2.5",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.0.2",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
|
||||
"commons-io" % "commons-io" % "2.4",
|
||||
"org.pegdown" % "pegdown" % "1.4.1",
|
||||
"org.apache.commons" % "commons-compress" % "1.5",
|
||||
@@ -43,7 +41,7 @@ object MyBuild extends Build {
|
||||
"com.h2database" % "h2" % "1.3.173",
|
||||
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")),
|
||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
||||
"junit" % "junit" % "4.11" % "test"
|
||||
),
|
||||
EclipseKeys.withSource := true,
|
||||
|
||||
2
sbt.bat
@@ -1,2 +1,2 @@
|
||||
set SCRIPT_DIR=%~dp0
|
||||
java -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 -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
|
||||
|
||||
2
sbt.sh
@@ -1 +1 @@
|
||||
java -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 -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
|
||||
|
||||
@@ -25,8 +25,8 @@ 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,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);
|
||||
}
|
||||
@@ -51,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
src/main/java/util/PatchUtil.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package util;
|
||||
|
||||
import org.eclipse.jgit.api.errors.PatchApplyException;
|
||||
import org.eclipse.jgit.diff.RawText;
|
||||
import org.eclipse.jgit.internal.JGitText;
|
||||
import org.eclipse.jgit.patch.FileHeader;
|
||||
import org.eclipse.jgit.patch.HunkHeader;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class helps to apply patch. Most of these code came from {@link org.eclipse.jgit.api.ApplyCommand}.
|
||||
*/
|
||||
public class PatchUtil {
|
||||
|
||||
public static String apply(String source, String patch, FileHeader fh)
|
||||
throws IOException, PatchApplyException {
|
||||
RawText rt = new RawText(source.getBytes("UTF-8"));
|
||||
List<String> oldLines = new ArrayList<String>(rt.size());
|
||||
for (int i = 0; i < rt.size(); i++)
|
||||
oldLines.add(rt.getString(i));
|
||||
List<String> newLines = new ArrayList<String>(oldLines);
|
||||
for (HunkHeader hh : fh.getHunks()) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(patch.getBytes("UTF-8"), hh.getStartOffset(), hh.getEndOffset() - hh.getStartOffset());
|
||||
RawText hrt = new RawText(out.toByteArray());
|
||||
List<String> hunkLines = new ArrayList<String>(hrt.size());
|
||||
for (int i = 0; i < hrt.size(); i++)
|
||||
hunkLines.add(hrt.getString(i));
|
||||
int pos = 0;
|
||||
for (int j = 1; j < hunkLines.size(); j++) {
|
||||
String hunkLine = hunkLines.get(j);
|
||||
switch (hunkLine.charAt(0)) {
|
||||
case ' ':
|
||||
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
|
||||
hunkLine.substring(1))) {
|
||||
throw new PatchApplyException(MessageFormat.format(
|
||||
JGitText.get().patchApplyException, hh));
|
||||
}
|
||||
pos++;
|
||||
break;
|
||||
case '-':
|
||||
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
|
||||
hunkLine.substring(1))) {
|
||||
throw new PatchApplyException(MessageFormat.format(
|
||||
JGitText.get().patchApplyException, hh));
|
||||
}
|
||||
newLines.remove(hh.getNewStartLine() - 1 + pos);
|
||||
break;
|
||||
case '+':
|
||||
newLines.add(hh.getNewStartLine() - 1 + pos,
|
||||
hunkLine.substring(1));
|
||||
pos++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isNoNewlineAtEndOfFile(fh))
|
||||
newLines.add(""); //$NON-NLS-1$
|
||||
if (!rt.isMissingNewlineAtEnd())
|
||||
oldLines.add(""); //$NON-NLS-1$
|
||||
if (!isChanged(oldLines, newLines))
|
||||
return null; // don't touch the file
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String l : newLines) {
|
||||
// don't bother handling line endings - if it was windows, the \r is
|
||||
// still there!
|
||||
sb.append(l).append('\n');
|
||||
}
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isChanged(List<String> ol, List<String> nl) {
|
||||
if (ol.size() != nl.size())
|
||||
return true;
|
||||
for (int i = 0; i < ol.size(); i++)
|
||||
if (!ol.get(i).equals(nl.get(i)))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isNoNewlineAtEndOfFile(FileHeader fh) {
|
||||
HunkHeader lastHunk = fh.getHunks().get(fh.getHunks().size() - 1);
|
||||
RawText lhrt = new RawText(lastHunk.getBuffer());
|
||||
return lhrt.getString(lhrt.size() - 1).equals(
|
||||
"\\ No newline at end of file"); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
1
src/main/resources/update/1_8.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE ACCOUNT ADD COLUMN REMOVED BOOLEAN DEFAULT FALSE;
|
||||
@@ -1,13 +1,22 @@
|
||||
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
|
||||
import app._
|
||||
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||
import org.scalatra._
|
||||
import javax.servlet._
|
||||
import java.util.EnumSet
|
||||
|
||||
class ScalatraBootstrap extends LifeCycle {
|
||||
override def init(context: ServletContext) {
|
||||
// Register TransactionFilter and BasicAuthenticationFilter at first
|
||||
context.addFilter("transactionFilter", new TransactionFilter)
|
||||
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
||||
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
|
||||
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
|
||||
|
||||
// Register controllers
|
||||
context.mount(new IndexController, "/")
|
||||
context.mount(new SearchController, "/")
|
||||
context.mount(new FileUploadController, "/upload")
|
||||
context.mount(new SignInController, "/*")
|
||||
context.mount(new DashboardController, "/*")
|
||||
context.mount(new UserManagementController, "/*")
|
||||
context.mount(new SystemSettingsController, "/*")
|
||||
@@ -20,7 +29,9 @@ 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)
|
||||
if(!dir.exists){
|
||||
dir.mkdirs()
|
||||
|
||||
@@ -5,15 +5,13 @@ import util.{FileUtil, OneselfAuthenticator}
|
||||
import util.StringUtil._
|
||||
import util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.FlashMapSupport
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
class AccountController extends AccountControllerBase
|
||||
with SystemSettingsService with AccountService with RepositoryService with ActivityService
|
||||
with OneselfAuthenticator
|
||||
with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
|
||||
|
||||
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 OneselfAuthenticator =>
|
||||
|
||||
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||
url: Option[String], fileId: Option[String])
|
||||
@@ -97,6 +95,27 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:userName/_delete")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
|
||||
getAccountByUserName(userName, true).foreach { account =>
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
deleteRepository(userName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
}
|
||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
removeUserRelatedData(userName)
|
||||
|
||||
updateAccount(account.copy(isRemoved = true))
|
||||
}
|
||||
|
||||
session.invalidate
|
||||
redirect("/")
|
||||
})
|
||||
|
||||
get("/register"){
|
||||
if(loadSystemSettings().allowAccountRegistration){
|
||||
if(context.loginAccount.isDefined){
|
||||
|
||||
@@ -3,31 +3,32 @@ package app
|
||||
import _root_.util.Directory._
|
||||
import _root_.util.Implicits._
|
||||
import _root_.util.ControlUtil._
|
||||
import _root_.util.{FileUtil, Validations, Keys}
|
||||
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
|
||||
import org.scalatra._
|
||||
import org.scalatra.json._
|
||||
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 service.{SystemSettingsService, AccountService}
|
||||
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
|
||||
import java.text.SimpleDateFormat
|
||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||
import org.scalatra.i18n._
|
||||
|
||||
/**
|
||||
* Provides generic features for controller implementations.
|
||||
*/
|
||||
abstract class ControllerBase extends ScalatraFilter
|
||||
with ClientSideValidationFormSupport with JacksonJsonSupport 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
|
||||
@@ -37,12 +38,15 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||
if(account == null){
|
||||
// Redirect to login form
|
||||
httpResponse.sendRedirect(context + "/signin?" + path)
|
||||
// TODO Should use the configured base url.
|
||||
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
|
||||
} else if(account.isAdmin){
|
||||
// H2 Console (administrators only)
|
||||
// TODO Should use the configured base url.
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
// Redirect to dashboard
|
||||
// TODO Should use the configured base url.
|
||||
httpResponse.sendRedirect(context + "/")
|
||||
}
|
||||
} else if(path.startsWith("/git/")){
|
||||
@@ -52,15 +56,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().baseUrl.getOrElse(servletContext.getContextPath), LoginAccount, request)
|
||||
contextCache.set(context)
|
||||
context
|
||||
}
|
||||
case context => context
|
||||
}
|
||||
}
|
||||
|
||||
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
|
||||
@@ -71,7 +84,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
action
|
||||
}
|
||||
|
||||
override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
|
||||
override def ajaxGet[T](path : String, form : ValueType[T])(action : T => Any) : Route =
|
||||
super.ajaxGet(path, form){ form =>
|
||||
request.setAttribute(Keys.Request.Ajax, "true")
|
||||
action(form)
|
||||
@@ -83,7 +96,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
action
|
||||
}
|
||||
|
||||
override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
|
||||
override def ajaxPost[T](path : String, form : ValueType[T])(action : T => Any) : Route =
|
||||
super.ajaxPost(path, form){ form =>
|
||||
request.setAttribute(Keys.Request.Ajax, "true")
|
||||
action(form)
|
||||
@@ -106,27 +119,31 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
if(request.getMethod.toUpperCase == "POST"){
|
||||
org.scalatra.Unauthorized(redirect("/signin"))
|
||||
} else {
|
||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + 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.
|
||||
*
|
||||
* @param path the context path
|
||||
*/
|
||||
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
|
||||
case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
|
||||
|
||||
def redirectUrl = if(request.getParameter("redirect") != null){
|
||||
request.getParameter("redirect")
|
||||
} else {
|
||||
currentUrl
|
||||
}
|
||||
lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length)
|
||||
|
||||
/**
|
||||
* Get object from cache.
|
||||
@@ -169,13 +186,13 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
|
||||
}
|
||||
|
||||
protected def uniqueUserName: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
getAccountByUserName(value).map { _ => "User already exists." }
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getAccountByUserName(value, true).map { _ => "User already exists." }
|
||||
}
|
||||
|
||||
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
getAccountByMailAddress(value)
|
||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||
getAccountByMailAddress(value, true)
|
||||
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
||||
.map { _ => "Mail address is already registered." }
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import util._
|
||||
import service._
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
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
|
||||
@@ -73,28 +73,26 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
JGitUtil.initRepository(gitdir)
|
||||
|
||||
if(form.createReadme){
|
||||
FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir =>
|
||||
// Clone the repository
|
||||
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
|
||||
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"
|
||||
}
|
||||
|
||||
// Create README.md
|
||||
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
|
||||
if(form.description.nonEmpty){
|
||||
form.name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
form.description.get
|
||||
} else {
|
||||
form.name + "\n" +
|
||||
"===============\n"
|
||||
}, "UTF-8")
|
||||
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
|
||||
val git = Git.open(tmpdir)
|
||||
git.add.addFilepattern("README.md").call
|
||||
git.commit
|
||||
.setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
.setMessage("Initial commit").call
|
||||
git.push.call
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,54 +113,62 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
val loginUserName = loginAccount.userName
|
||||
|
||||
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
|
||||
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
|
||||
// Insert to the database at first
|
||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||
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)
|
||||
)
|
||||
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)
|
||||
// Insert default labels
|
||||
insertDefaultLabels(loginUserName, repository.name)
|
||||
|
||||
// clone repository actually
|
||||
JGitUtil.cloneRepository(
|
||||
getRepositoryDir(repository.owner, repository.name),
|
||||
getRepositoryDir(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))
|
||||
// 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)
|
||||
// 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(_) => ???
|
||||
}
|
||||
case Left(_) => ???
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record activity
|
||||
recordForkActivity(repository.owner, repository.name, loginUserName)
|
||||
// Record activity
|
||||
recordForkActivity(repository.owner, repository.name, loginUserName)
|
||||
// redirect to the repository
|
||||
redirect(s"/${loginUserName}/${repository.name}")
|
||||
}
|
||||
}
|
||||
// redirect to the repository
|
||||
redirect("/%s/%s".format(loginUserName, repository.name))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -176,7 +182,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
private def existsAccount: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||
}
|
||||
|
||||
@@ -184,7 +190,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
* Duplicate check for the repository name.
|
||||
*/
|
||||
private def unique: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
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.")
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ import org.apache.commons.io.FileUtils
|
||||
* This servlet saves uploaded file as temporary file and returns the unique id.
|
||||
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
|
||||
*/
|
||||
class FileUploadController extends ScalatraServlet
|
||||
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
|
||||
class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
|
||||
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||
|
||||
|
||||
@@ -5,12 +5,17 @@ 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)
|
||||
|
||||
val form = mapping(
|
||||
"userName" -> trim(label("Username", text(required))),
|
||||
"password" -> trim(label("Password", text(required)))
|
||||
)(SignInForm.apply)
|
||||
|
||||
get("/"){
|
||||
val loginAccount = context.loginAccount
|
||||
@@ -22,6 +27,44 @@ trait IndexControllerBase extends ControllerBase {
|
||||
)
|
||||
}
|
||||
|
||||
get("/signin"){
|
||||
val redirect = params.get("redirect")
|
||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||
flash += Keys.Flash.Redirect -> redirect.get
|
||||
}
|
||||
html.signin(loadSystemSettings())
|
||||
}
|
||||
|
||||
post("/signin", form){ form =>
|
||||
authenticate(loadSystemSettings(), form.userName, form.password) match {
|
||||
case Some(account) => signin(account)
|
||||
case None => redirect("/signin")
|
||||
}
|
||||
}
|
||||
|
||||
get("/signout"){
|
||||
session.invalidate
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
/**
|
||||
* Set account information into HttpSession and redirect.
|
||||
*/
|
||||
private def signin(account: model.Account) = {
|
||||
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
|
||||
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
|
||||
redirect("/")
|
||||
} else {
|
||||
redirect(redirectUrl)
|
||||
}
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON API for collaborator completion.
|
||||
*
|
||||
@@ -30,7 +73,7 @@ trait IndexControllerBase extends ControllerBase {
|
||||
get("/_user/proposals")(usersOnly {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -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,6 +111,11 @@ 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}")
|
||||
@@ -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
|
||||
@@ -274,6 +284,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,6 +332,11 @@ 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 =>
|
||||
|
||||
@@ -3,6 +3,7 @@ package app
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import service._
|
||||
import util.CollaboratorsAuthenticator
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
class LabelsController extends LabelsControllerBase
|
||||
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
|
||||
@@ -51,7 +52,7 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
* Constraint for the identifier such as user name, repository name or page name.
|
||||
*/
|
||||
private def labelName: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(!value.matches("^[^,]+$")){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
|
||||
@@ -4,38 +4,42 @@ import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticat
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import util.FileUtil._
|
||||
import service._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import org.apache.commons.io.FileUtils
|
||||
import scala.collection.JavaConverters._
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
|
||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||
import service.IssuesService._
|
||||
import service.PullRequestService._
|
||||
import util.JGitUtil.DiffInfo
|
||||
import service.RepositoryService.RepositoryTreeNode
|
||||
import util.JGitUtil.CommitInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.eclipse.jgit.merge.MergeStrategy
|
||||
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 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 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])
|
||||
|
||||
val pullRequestForm = mapping(
|
||||
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
||||
"content" -> trim(label("Content", optional(text()))),
|
||||
"targetUserName" -> trim(text(required, maxlength(100))),
|
||||
"targetBranch" -> trim(text(required, maxlength(100))),
|
||||
"requestUserName" -> trim(text(required, maxlength(100))),
|
||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||
"commitIdTo" -> trim(text(required, maxlength(40)))
|
||||
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
||||
"content" -> trim(label("Content", optional(text()))),
|
||||
"targetUserName" -> trim(text(required, maxlength(100))),
|
||||
"targetBranch" -> trim(text(required, maxlength(100))),
|
||||
"requestUserName" -> trim(text(required, maxlength(100))),
|
||||
"requestRepositoryName" -> trim(text(required, maxlength(100))),
|
||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||
"commitIdTo" -> trim(text(required, maxlength(40)))
|
||||
)(PullRequestForm.apply)
|
||||
|
||||
val mergeForm = mapping(
|
||||
@@ -48,6 +52,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
targetUserName: String,
|
||||
targetBranch: String,
|
||||
requestUserName: String,
|
||||
requestRepositoryName: String,
|
||||
requestBranch: String,
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String)
|
||||
@@ -74,8 +79,10 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
pulls.html.pullreq(
|
||||
issue, pullreq,
|
||||
getComments(owner, name, issueId),
|
||||
getIssueLabels(owner, name, issueId.toInt),
|
||||
(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),
|
||||
@@ -88,83 +95,110 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
pulls.html.mergeguide(
|
||||
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch),
|
||||
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
|
||||
pullreq,
|
||||
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository =>
|
||||
params("id").toIntOpt.map { issueId =>
|
||||
val branchName = params("branchName")
|
||||
val userName = context.loginAccount.get.userName
|
||||
if(repository.repository.defaultBranch != branchName){
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.branchDelete().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 =>
|
||||
params("id").toIntOpt.flatMap { issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val name = repository.name
|
||||
LockUtil.lock(s"${owner}/${name}/merge"){
|
||||
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
|
||||
val remote = getRepositoryDir(owner, name)
|
||||
withTmpDir(new java.io.File(getTemporaryDir(owner, name), s"merge-${issueId}")){ tmpdir =>
|
||||
using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call){ git =>
|
||||
using(Git.open(getRepositoryDir(owner, name))) { git =>
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(owner, name, issueId, true)
|
||||
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(owner, name, issueId, true)
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// fetch pull request to temporary working repository
|
||||
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
|
||||
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(owner, name).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
|
||||
|
||||
// merge pull request
|
||||
git.checkout.setName(pullreq.branch).call
|
||||
|
||||
val result = git.merge
|
||||
.include(git.getRepository.resolve(pullRequestBranchName))
|
||||
.setFastForward(FastForwardMode.NO_FF)
|
||||
.setCommit(false)
|
||||
.call
|
||||
|
||||
if(result.getConflicts != null){
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
// merge commit
|
||||
git.getRepository.writeMergeCommitMsg(
|
||||
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
|
||||
+ form.message)
|
||||
|
||||
git.commit
|
||||
.setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
.call
|
||||
|
||||
// push
|
||||
git.push.call
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
// merge
|
||||
val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
|
||||
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
val conflicted = try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
if (conflicted) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
// creates merge commit
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(merger.getResultTreeId)
|
||||
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
|
||||
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
mergeCommit.setAuthor(personIdent)
|
||||
mergeCommit.setCommitter(personIdent)
|
||||
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n\n" +
|
||||
form.message)
|
||||
|
||||
// insertObject and got mergeCommit Object Id
|
||||
val inserter = git.getRepository.newObjectInserter
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
|
||||
// update refs
|
||||
val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(false)
|
||||
refUpdate.setRefLogIdent(personIdent)
|
||||
refUpdate.setRefLogMessage("merged", true)
|
||||
refUpdate.update()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// call web hook
|
||||
getWebHookURLs(owner, name) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(ownerAccount <- getAccountByUserName(owner)){
|
||||
callWebHook(owner, name, webHookURLs,
|
||||
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,89 +216,101 @@ 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
|
||||
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
|
||||
(getRepository(originOwner, repository.name, baseUrl),
|
||||
getRepository(forkedOwner, repository.name, baseUrl)) match {
|
||||
case (Some(originRepository), Some(forkedRepository)) => {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originOwner, repository.name)),
|
||||
Git.open(getRepositoryDir(forkedOwner, repository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
val forkedId = getForkedCommitId(oldGit, newGit,
|
||||
originOwner, repository.name, originBranch,
|
||||
forkedOwner, repository.name, forkedBranch)
|
||||
|
||||
val oldId = oldGit.getRepository.resolve(forkedId)
|
||||
val newId = newGit.getRepository.resolve(forkedBranch)
|
||||
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originOwner, repository.name, oldId.getName,
|
||||
forkedOwner, repository.name, newId.getName)
|
||||
|
||||
pulls.html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
repository.repository.originUserName.map { userName =>
|
||||
userName :: getForkedRepositories(userName, repository.name)
|
||||
} getOrElse List(repository.owner),
|
||||
originBranch,
|
||||
forkedBranch,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
repository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
(for(
|
||||
originRepositoryName <- if(originOwner == forkedOwner){
|
||||
Some(forkedRepository.name)
|
||||
} else {
|
||||
forkedRepository.repository.originRepositoryName.orElse {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
val forkedId = getForkedCommitId(oldGit, newGit,
|
||||
originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||
|
||||
val oldId = oldGit.getRepository.resolve(forkedId)
|
||||
val newId = newGit.getRepository.resolve(forkedBranch)
|
||||
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner, originRepository.name, oldId.getName,
|
||||
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||
|
||||
pulls.html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
},
|
||||
originBranch,
|
||||
forkedBranch,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
|
||||
}
|
||||
case _ => NotFound
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository =>
|
||||
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
|
||||
(getRepository(originOwner, repository.name, baseUrl),
|
||||
getRepository(forkedOwner, repository.name, baseUrl)) match {
|
||||
case (Some(originRepository), Some(forkedRepository)) => {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originOwner, repository.name)),
|
||||
Git.open(getRepositoryDir(forkedOwner, repository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
pulls.html.mergecheck(
|
||||
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
|
||||
(for(
|
||||
originRepositoryName <- if(originOwner == forkedOwner){
|
||||
Some(forkedRepository.name)
|
||||
} else {
|
||||
forkedRepository.repository.originRepositoryName.orElse {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
pulls.html.mergecheck(
|
||||
checkConflict(originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch))
|
||||
}
|
||||
case _ => NotFound()
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||
@@ -286,7 +332,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
issueId = issueId,
|
||||
originBranch = form.targetBranch,
|
||||
requestUserName = form.requestUserName,
|
||||
requestRepositoryName = repository.name,
|
||||
requestRepositoryName = form.requestRepositoryName,
|
||||
requestBranch = form.requestBranch,
|
||||
commitIdFrom = form.commitIdFrom,
|
||||
commitIdTo = form.commitIdTo)
|
||||
@@ -294,7 +340,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
// fetch requested branch
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
|
||||
.setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
|
||||
.call
|
||||
}
|
||||
@@ -315,23 +361,52 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
*/
|
||||
private def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
||||
// TODO Are there more quick way?
|
||||
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
|
||||
val remote = getRepositoryDir(userName, repositoryName)
|
||||
withTmpDir(new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")){ tmpdir =>
|
||||
using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call){ git =>
|
||||
|
||||
git.checkout.setName(branch).call
|
||||
|
||||
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${branch}"
|
||||
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
||||
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||
try {
|
||||
// fetch objects from origin repository branch
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
|
||||
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
||||
.setRefSpecs(refSpec)
|
||||
.call
|
||||
|
||||
val result = git.merge
|
||||
.include(git.getRepository.resolve("FETCH_HEAD"))
|
||||
.setCommit(false).call
|
||||
// merge conflict check
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
|
||||
val mergeTip = git.getRepository.resolve(tmpRefName)
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
} finally {
|
||||
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
||||
refUpdate.setForceUpdate(true)
|
||||
refUpdate.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.getConflicts != null
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
|
||||
*/
|
||||
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String,
|
||||
issueId: Int): Boolean = {
|
||||
LockUtil.lock(s"${userName}/${repositoryName}/merge") {
|
||||
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||
// merge
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,8 +438,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
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)
|
||||
existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
|
||||
}.head.id
|
||||
|
||||
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
|
||||
@@ -379,7 +453,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
|
||||
new CommitInfo(revCommit)
|
||||
}.toList.splitWith{ (commit1, commit2) =>
|
||||
}.toList.splitWith { (commit1, commit2) =>
|
||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
import util.ControlUtil._
|
||||
@@ -15,17 +15,18 @@ 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 =>
|
||||
|
||||
// for repository options
|
||||
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
||||
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
||||
|
||||
val optionsForm = mapping(
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean()))
|
||||
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean()))
|
||||
)(OptionsForm.apply)
|
||||
|
||||
// for collaborator addition
|
||||
@@ -42,6 +43,13 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
"url" -> trim(label("url", text(required, webHook)))
|
||||
)(WebHookForm.apply)
|
||||
|
||||
// for transfer ownership
|
||||
case class TransferOwnerShipForm(newOwner: String)
|
||||
|
||||
val transferForm = mapping(
|
||||
"newOwner" -> trim(label("New owner", text(required, transferUser)))
|
||||
)(TransferOwnerShipForm.apply)
|
||||
|
||||
/**
|
||||
* Redirect to the Options page.
|
||||
*/
|
||||
@@ -69,8 +77,21 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
repository.repository.isPrivate
|
||||
} getOrElse form.isPrivate
|
||||
)
|
||||
// Change repository name
|
||||
if(repository.name != form.repositoryName){
|
||||
// Update database
|
||||
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
|
||||
// Move git repository
|
||||
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||
FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
|
||||
}
|
||||
// Move wiki repository
|
||||
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
|
||||
}
|
||||
}
|
||||
flash += "info" -> "Repository settings has been updated."
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/options")
|
||||
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -137,15 +158,13 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
.setMaxCount(3)
|
||||
.call.iterator.asScala.map(new CommitInfo(_))
|
||||
|
||||
val webHookURLs = getWebHookURLs(repository.owner, repository.name)
|
||||
if(webHookURLs.nonEmpty){
|
||||
callWebHook(repository.owner, repository.name, webHookURLs,
|
||||
WebHookPayload(
|
||||
git,
|
||||
"refs/heads/" + repository.repository.defaultBranch,
|
||||
repository,
|
||||
commits.toList,
|
||||
getAccountByUserName(repository.owner).get))
|
||||
getWebHookURLs(repository.owner, repository.name) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(ownerAccount <- getAccountByUserName(repository.owner)){
|
||||
callWebHook(repository.owner, repository.name, webHookURLs,
|
||||
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
|
||||
flash += "info" -> "Test payload deployed!"
|
||||
@@ -154,10 +173,30 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the delete repository page.
|
||||
* Display the danger zone.
|
||||
*/
|
||||
get("/:owner/:repository/settings/delete")(ownerOnly {
|
||||
settings.html.delete(_)
|
||||
get("/:owner/:repository/settings/danger")(ownerOnly {
|
||||
settings.html.danger(_)
|
||||
})
|
||||
|
||||
/**
|
||||
* Transfer repository ownership.
|
||||
*/
|
||||
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
|
||||
// Change repository owner
|
||||
if(repository.owner != form.newOwner){
|
||||
// Update database
|
||||
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
|
||||
// Move git repository
|
||||
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||
FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
|
||||
}
|
||||
// Move wiki repository
|
||||
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
|
||||
}
|
||||
}
|
||||
redirect(s"/${form.newOwner}/${repository.name}")
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -177,7 +216,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
* Provides duplication check for web hook url.
|
||||
*/
|
||||
private def webHook: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
|
||||
}
|
||||
|
||||
@@ -185,7 +224,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
* Provides Constraint to validate the collaborator name.
|
||||
*/
|
||||
private def collaborator: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getAccountByUserName(value) match {
|
||||
case None => Some("User does not exist.")
|
||||
case Some(x) if(x.isGroupAccount)
|
||||
@@ -196,4 +235,32 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate check for the rename repository name.
|
||||
*/
|
||||
private def renameRepositoryName: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||
params.get("repository").filter(_ != value).flatMap { _ =>
|
||||
params.get("owner").flatMap { userName =>
|
||||
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Constraint to validate the repository transfer user.
|
||||
*/
|
||||
private def transferUser: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getAccountByUserName(value) match {
|
||||
case None => Some("User does not exist.")
|
||||
case Some(x) => if(x.userName == params("owner")){
|
||||
Some("This is current repository owner.")
|
||||
} else {
|
||||
params.get("repository").flatMap { repositoryName =>
|
||||
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package app
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
|
||||
import _root_.util._
|
||||
import service._
|
||||
import org.scalatra._
|
||||
import java.io.File
|
||||
@@ -12,15 +12,16 @@ import org.eclipse.jgit.lib._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.treewalk._
|
||||
import java.util.zip.{ZipEntry, ZipOutputStream}
|
||||
import scala.Some
|
||||
|
||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
with RepositoryService with AccountService with ReferrerAuthenticator
|
||||
with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
|
||||
/**
|
||||
* The repository viewer.
|
||||
*/
|
||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with ReferrerAuthenticator =>
|
||||
self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
|
||||
/**
|
||||
* Returns converted HTML from Markdown for preview.
|
||||
@@ -150,10 +151,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/:branchName")(collaboratorsOnly { repository =>
|
||||
val branchName = params("branchName")
|
||||
val userName = context.loginAccount.get.userName
|
||||
if(repository.repository.defaultBranch != branchName){
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.branchDelete().setBranchNames(branchName).call()
|
||||
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
|
||||
}
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/branches")
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays tags.
|
||||
*/
|
||||
@@ -175,7 +191,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
workDir.mkdirs
|
||||
|
||||
val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
|
||||
val zipFile = new File(workDir, repository.name + "-" +
|
||||
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
|
||||
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||
@@ -204,6 +221,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
contentType = "application/octet-stream"
|
||||
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
|
||||
zipFile
|
||||
} else {
|
||||
BadRequest
|
||||
@@ -232,6 +250,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
(id, path.substring(id.length).replaceFirst("^/", ""))
|
||||
}
|
||||
|
||||
|
||||
private val readmeFiles = Seq("readme.md", "readme.markdown")
|
||||
|
||||
/**
|
||||
* Provides HTML of the file list.
|
||||
*
|
||||
@@ -251,9 +272,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
// process README.md
|
||||
val readme = files.find(_.name == "README.md").map { file =>
|
||||
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
// process README.md or README.markdown
|
||||
val readme = files.find { file =>
|
||||
readmeFiles.contains(file.name.toLowerCase)
|
||||
}.map { file =>
|
||||
file -> StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
}
|
||||
|
||||
repo.html.files(revision, repository,
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import util.Implicits._
|
||||
import util.StringUtil._
|
||||
import util.Keys
|
||||
|
||||
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
|
||||
|
||||
trait SignInControllerBase extends ControllerBase { self: SystemSettingsService with AccountService =>
|
||||
|
||||
case class SignInForm(userName: String, password: String)
|
||||
|
||||
val form = mapping(
|
||||
"userName" -> trim(label("Username", text(required))),
|
||||
"password" -> trim(label("Password", text(required)))
|
||||
)(SignInForm.apply)
|
||||
|
||||
get("/signin"){
|
||||
val redirect = params.get("redirect")
|
||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||
session.setAttribute(Keys.Session.Redirect, redirect.get)
|
||||
}
|
||||
html.signin(loadSystemSettings())
|
||||
}
|
||||
|
||||
post("/signin", form){ form =>
|
||||
authenticate(loadSystemSettings(), form.userName, form.password) match {
|
||||
case Some(account) => signin(account)
|
||||
case None => redirect("/signin")
|
||||
}
|
||||
}
|
||||
|
||||
get("/signout"){
|
||||
session.invalidate
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
/**
|
||||
* Set account information into HttpSession and redirect.
|
||||
*/
|
||||
private def signin(account: model.Account) = {
|
||||
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
|
||||
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
|
||||
redirect("/")
|
||||
} else {
|
||||
redirect(urlEncode(redirectUrl).replaceAll("%2F", "/"))
|
||||
}
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import service.{AccountService, SystemSettingsService}
|
||||
import SystemSettingsService._
|
||||
import util.AdminAuthenticator
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.FlashMapSupport
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with SystemSettingsService with AccountService with AdminAuthenticator
|
||||
|
||||
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
trait SystemSettingsControllerBase extends ControllerBase {
|
||||
self: SystemSettingsService with 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())),
|
||||
@@ -33,7 +33,10 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
"bindPassword" -> trim(label("Bind Password", optional(text()))),
|
||||
"baseDN" -> trim(label("Base DN", text(required))),
|
||||
"userNameAttribute" -> trim(label("User name attribute", text(required))),
|
||||
"mailAttribute" -> trim(label("Mail address attribute", text(required)))
|
||||
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
|
||||
"mailAttribute" -> trim(label("Mail address attribute", text(required))),
|
||||
"tls" -> trim(label("Enable TLS", optional(boolean()))),
|
||||
"keystore" -> trim(label("Keystore", optional(text())))
|
||||
)(Ldap.apply))
|
||||
)(SystemSettings.apply)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import util.AdminAuthenticator
|
||||
import util.StringUtil._
|
||||
import util.ControlUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.Directory._
|
||||
|
||||
class UserManagementController extends UserManagementControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
@@ -17,57 +19,61 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
url: Option[String], fileId: Option[String])
|
||||
|
||||
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||
mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String])
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String], clearImage: Boolean)
|
||||
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
val newUserForm = mapping(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text())))
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||
)(NewUserForm.apply)
|
||||
|
||||
val editUserForm = mapping(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
|
||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
||||
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
||||
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
)(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()))),
|
||||
"memberNames" -> trim(label("Member Names" ,optional(text())))
|
||||
)(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()))
|
||||
"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()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
val users = getAllUsers()
|
||||
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)
|
||||
}.toMap
|
||||
admin.users.html.list(users, members)
|
||||
admin.users.html.list(users, members, includeRemoved)
|
||||
})
|
||||
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
@@ -82,18 +88,32 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||
val userName = params("userName")
|
||||
admin.users.html.user(getAccountByUserName(userName))
|
||||
admin.users.html.user(getAccountByUserName(userName, true))
|
||||
})
|
||||
|
||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { account =>
|
||||
updateAccount(getAccountByUserName(userName).get.copy(
|
||||
getAccountByUserName(userName, true).map { account =>
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
deleteRepository(userName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
}
|
||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
removeUserRelatedData(userName)
|
||||
}
|
||||
|
||||
updateAccount(account.copy(
|
||||
password = form.password.map(sha1).getOrElse(account.password),
|
||||
fullName = form.fullName,
|
||||
mailAddress = form.mailAddress,
|
||||
isAdmin = form.isAdmin,
|
||||
url = form.url))
|
||||
url = form.url,
|
||||
isRemoved = form.isRemoved))
|
||||
|
||||
updateImage(userName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
@@ -114,20 +134,34 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
|
||||
admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
}
|
||||
})
|
||||
|
||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
|
||||
getAccountByUserName(groupName).map { account =>
|
||||
updateGroup(groupName, form.url)
|
||||
updateGroupMembers(form.groupName, memberNames)
|
||||
getAccountByUserName(groupName, true).map { account =>
|
||||
updateGroup(groupName, form.url, form.isRemoved)
|
||||
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
memberNames.foreach { userName =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
if(form.isRemoved){
|
||||
// Remove from GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, Nil)
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
deleteRepository(groupName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
||||
}
|
||||
} else {
|
||||
// Update GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, memberNames)
|
||||
// Update COLLABORATOR for group repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
memberNames.foreach { userName =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +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 service.WikiService.WikiPageInfo
|
||||
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)
|
||||
|
||||
@@ -66,7 +64,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository,
|
||||
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||
}
|
||||
})
|
||||
@@ -96,7 +94,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/}")
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/")
|
||||
} else {
|
||||
flash += "info" -> "This patch was not able to be reversed."
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
|
||||
@@ -170,12 +168,12 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
})
|
||||
|
||||
private def unique: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
|
||||
}
|
||||
|
||||
private def pagename: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(value.exists("\\/:*?\"<>|".contains(_))){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
@@ -186,17 +184,17 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
}
|
||||
|
||||
private def conflictForNew: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] = {
|
||||
optionIf(targetWikiPage.nonEmpty){
|
||||
Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.")
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
targetWikiPage.map { _ =>
|
||||
"Someone has created the wiki since you started. Please reload this page and re-apply your changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def conflictForEdit: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] = {
|
||||
optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(true)){
|
||||
Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.")
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
targetWikiPage.filter(_.id != params("id")).map{ _ =>
|
||||
"Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ object Accounts extends Table[Account]("ACCOUNT") {
|
||||
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
||||
def image = column[String]("IMAGE")
|
||||
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
||||
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
|
||||
def removed = column[Boolean]("REMOVED")
|
||||
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount ~ removed <> (Account, Account.unapply _)
|
||||
}
|
||||
|
||||
case class Account(
|
||||
@@ -28,5 +29,6 @@ case class Account(
|
||||
updatedDate: java.util.Date,
|
||||
lastLoginDate: Option[java.util.Date],
|
||||
image: Option[String],
|
||||
isGroupAccount: Boolean
|
||||
isGroupAccount: Boolean,
|
||||
isRemoved: Boolean
|
||||
)
|
||||
|
||||
@@ -36,11 +36,15 @@ trait AccountService {
|
||||
*/
|
||||
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
|
||||
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
|
||||
case Right(mailAddress) => {
|
||||
case Right(ldapUserInfo) => {
|
||||
// Create or update account by LDAP information
|
||||
getAccountByUserName(userName) match {
|
||||
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
|
||||
case None => createAccount(userName, "", userName, mailAddress, false, None)
|
||||
getAccountByUserName(userName, true) match {
|
||||
case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
|
||||
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)
|
||||
}
|
||||
getAccountByUserName(userName)
|
||||
}
|
||||
@@ -51,13 +55,18 @@ trait AccountService {
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByUserName(userName: String): Option[Account] =
|
||||
Query(Accounts) filter(_.userName is userName.bind) firstOption
|
||||
def getAccountByUserName(userName: String, includeRemoved: Boolean = false): Option[Account] =
|
||||
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
|
||||
|
||||
def getAccountByMailAddress(mailAddress: String): Option[Account] =
|
||||
Query(Accounts) filter(_.mailAddress is mailAddress.bind) 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
|
||||
|
||||
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
|
||||
def getAllUsers(includeRemoved: Boolean = true): List[Account] =
|
||||
if(includeRemoved){
|
||||
Query(Accounts) sortBy(_.userName) list
|
||||
} else {
|
||||
Query(Accounts) filter (_.removed is false.bind) sortBy(_.userName) list
|
||||
}
|
||||
|
||||
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
|
||||
Accounts insert Account(
|
||||
@@ -71,12 +80,13 @@ trait AccountService {
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = false)
|
||||
isGroupAccount = false,
|
||||
isRemoved = false)
|
||||
|
||||
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.? }
|
||||
.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,
|
||||
@@ -85,7 +95,8 @@ trait AccountService {
|
||||
account.url,
|
||||
account.registeredDate,
|
||||
currentDate,
|
||||
account.lastLoginDate)
|
||||
account.lastLoginDate,
|
||||
account.isRemoved)
|
||||
|
||||
def updateAvatarImage(userName: String, image: Option[String]): Unit =
|
||||
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
|
||||
@@ -105,10 +116,11 @@ trait AccountService {
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = true)
|
||||
isGroupAccount = true,
|
||||
isRemoved = false)
|
||||
|
||||
def updateGroup(groupName: String, url: Option[String]): Unit =
|
||||
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
|
||||
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 = {
|
||||
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
|
||||
@@ -131,6 +143,12 @@ trait AccountService {
|
||||
.map(_.groupName)
|
||||
.list
|
||||
|
||||
def removeUserRelatedData(userName: String): Unit = {
|
||||
Query(GroupMembers).filter(_.userName is userName.bind).delete
|
||||
Query(Collaborators).filter(_.collaboratorName is userName.bind).delete
|
||||
Query(Repositories).filter(_.userName is userName.bind).delete
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object AccountService extends AccountService
|
||||
object AccountService extends AccountService
|
||||
@@ -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 &&
|
||||
@@ -331,7 +330,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 +351,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
|
||||
|
||||
@@ -15,6 +15,9 @@ 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 getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
|
||||
Query(PullRequests)
|
||||
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
@@ -43,6 +46,18 @@ trait PullRequestService { self: IssuesService =>
|
||||
commitIdFrom,
|
||||
commitIdTo))
|
||||
|
||||
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean): List[PullRequest] =
|
||||
Query(PullRequests)
|
||||
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
.filter { case (t1, t2) =>
|
||||
(t1.requestUserName is userName.bind) &&
|
||||
(t1.requestRepositoryName is repositoryName.bind) &&
|
||||
(t1.requestBranch is branch.bind) &&
|
||||
(t2.closed is closed.bind)
|
||||
}
|
||||
.map { case (t1, t2) => t1 }
|
||||
.list
|
||||
|
||||
}
|
||||
|
||||
object PullRequestService {
|
||||
|
||||
@@ -39,6 +39,67 @@ trait RepositoryService { self: AccountService =>
|
||||
IssueId insert (userName, repositoryName, 0)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
PullRequests.filter { t =>
|
||||
t.requestRepositoryName is oldRepositoryName.bind
|
||||
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
||||
|
||||
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)) :_*)
|
||||
|
||||
// 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}#")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def deleteRepository(userName: String, repositoryName: String): Unit = {
|
||||
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
|
||||
@@ -46,8 +107,8 @@ trait RepositoryService { self: AccountService =>
|
||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||
@@ -120,7 +181,7 @@ trait RepositoryService { self: AccountService =>
|
||||
case Some(x) if(x.isAdmin) => Query(Repositories)
|
||||
// for Normal Users
|
||||
case Some(x) if(!x.isAdmin) =>
|
||||
Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
|
||||
Query(Repositories) filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) ||
|
||||
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
|
||||
}
|
||||
// for Guests
|
||||
@@ -207,11 +268,11 @@ trait RepositoryService { self: AccountService =>
|
||||
}.length).first
|
||||
|
||||
|
||||
def getForkedRepositories(userName: String, repositoryName: String): List[String] =
|
||||
def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] =
|
||||
Query(Repositories).filter { t =>
|
||||
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
|
||||
}
|
||||
.sortBy(_.userName asc).map(_.userName).list
|
||||
.sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,19 @@ 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.getOrElse {
|
||||
defining(request.getRequestURL.toString){ url =>
|
||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
||||
}
|
||||
}.replaceFirst("/$", "")
|
||||
|
||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||
defining(new java.util.Properties()){ props =>
|
||||
settings.baseUrl.foreach(props.setProperty(BaseURL, _))
|
||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
@@ -31,7 +39,10 @@ trait SystemSettingsService {
|
||||
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
|
||||
props.setProperty(LdapBaseDN, ldap.baseDN)
|
||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
||||
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
|
||||
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
||||
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
||||
}
|
||||
}
|
||||
props.store(new java.io.FileOutputStream(GitBucketConf), null)
|
||||
@@ -45,6 +56,7 @@ trait SystemSettingsService {
|
||||
props.load(new java.io.FileInputStream(GitBucketConf))
|
||||
}
|
||||
SystemSettings(
|
||||
getOptionValue(props, BaseURL, None),
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, Gravatar, true),
|
||||
getValue(props, Notification, false),
|
||||
@@ -69,7 +81,10 @@ trait SystemSettingsService {
|
||||
getOptionValue(props, LdapBindPassword, None),
|
||||
getValue(props, LdapBaseDN, ""),
|
||||
getValue(props, LdapUserNameAttribute, ""),
|
||||
getValue(props, LdapMailAddressAttribute, "")))
|
||||
getOptionValue(props, LdapFullNameAttribute, None),
|
||||
getValue(props, LdapMailAddressAttribute, ""),
|
||||
getOptionValue[Boolean](props, LdapTls, None),
|
||||
getOptionValue(props, LdapKeystore, None)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -83,6 +98,7 @@ object SystemSettingsService {
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
case class SystemSettings(
|
||||
baseUrl: Option[String],
|
||||
allowAccountRegistration: Boolean,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
@@ -97,7 +113,10 @@ object SystemSettingsService {
|
||||
bindPassword: Option[String],
|
||||
baseDN: String,
|
||||
userNameAttribute: String,
|
||||
mailAttribute: String)
|
||||
fullNameAttribute: Option[String],
|
||||
mailAttribute: String,
|
||||
tls: Option[Boolean],
|
||||
keystore: Option[String])
|
||||
|
||||
case class Smtp(
|
||||
host: String,
|
||||
@@ -111,6 +130,7 @@ object SystemSettingsService {
|
||||
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"
|
||||
@@ -128,7 +148,10 @@ object SystemSettingsService {
|
||||
private val LdapBindPassword = "ldap.bind_password"
|
||||
private val LdapBaseDN = "ldap.baseDN"
|
||||
private val LdapUserNameAttribute = "ldap.username_attribute"
|
||||
private val LdapFullNameAttribute = "ldap.fullname_attribute"
|
||||
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
||||
private val LdapTls = "ldap.tls"
|
||||
private val LdapKeystore = "ldap.keystore"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
|
||||
defining(props.getProperty(key)){ value =>
|
||||
|
||||
@@ -74,14 +74,16 @@ trait WebHookService {
|
||||
object WebHookService {
|
||||
|
||||
case class WebHookPayload(
|
||||
pusher: WebHookUser,
|
||||
ref: String,
|
||||
commits: List[WebHookCommit],
|
||||
repository: WebHookRepository)
|
||||
|
||||
object WebHookPayload {
|
||||
def apply(git: Git, refName: String, repositoryInfo: RepositoryInfo,
|
||||
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
|
||||
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
|
||||
WebHookPayload(
|
||||
WebHookUser(pusher.fullName, pusher.mailAddress),
|
||||
refName,
|
||||
commits.map { commit =>
|
||||
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
package service
|
||||
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.{StringUtil, Directory, JGitUtil, LockUtil}
|
||||
import util.ControlUtil._
|
||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
||||
import org.eclipse.jgit.diff.DiffFormatter
|
||||
import org.eclipse.jgit.api.errors.PatchApplyException
|
||||
import util._
|
||||
import _root_.util.ControlUtil._
|
||||
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry}
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
|
||||
import java.io.ByteArrayInputStream
|
||||
import org.eclipse.jgit.patch._
|
||||
import org.eclipse.jgit.api.errors.PatchFormatException
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.Some
|
||||
|
||||
|
||||
object WikiService {
|
||||
|
||||
@@ -42,13 +49,8 @@ trait WikiService {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
|
||||
if(!dir.exists){
|
||||
try {
|
||||
JGitUtil.initRepository(dir)
|
||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
|
||||
} finally {
|
||||
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
|
||||
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
|
||||
}
|
||||
JGitUtil.initRepository(dir)
|
||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,11 +60,12 @@ trait WikiService {
|
||||
*/
|
||||
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
optionIf(!JGitUtil.isEmpty(git)){
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
||||
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId)
|
||||
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
|
||||
file.committer, file.time, file.commitId)
|
||||
}
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +74,7 @@ trait WikiService {
|
||||
*/
|
||||
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
optionIf(!JGitUtil.isEmpty(git)){
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
val index = path.lastIndexOf('/')
|
||||
val parentPath = if(index < 0) "." else path.substring(0, index)
|
||||
val fileName = if(index < 0) path else path.substring(index + 1)
|
||||
@@ -79,7 +82,7 @@ trait WikiService {
|
||||
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
|
||||
git.getRepository.open(file.id).getBytes
|
||||
}
|
||||
}
|
||||
} else None
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,12 +102,13 @@ trait WikiService {
|
||||
*/
|
||||
def revertWikiPage(owner: String, repository: String, from: String, to: String,
|
||||
committer: model.Account, pageName: Option[String]): Boolean = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
|
||||
// clone working copy
|
||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
||||
|
||||
using(Git.open(workDir)){ git =>
|
||||
case class RevertInfo(operation: String, filePath: String, source: String)
|
||||
|
||||
try {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
|
||||
val reader = git.getRepository.newObjectReader
|
||||
val oldTreeIter = new CanonicalTreeParser
|
||||
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
|
||||
@@ -112,7 +116,6 @@ trait WikiService {
|
||||
val newTreeIter = new CanonicalTreeParser
|
||||
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
|
||||
pageName match {
|
||||
case Some(x) => diff.getNewPath == x + ".md"
|
||||
@@ -127,72 +130,135 @@ trait WikiService {
|
||||
new String(out.toByteArray, "UTF-8")
|
||||
}
|
||||
|
||||
try {
|
||||
git.apply.setPatch(new java.io.ByteArrayInputStream(patch.getBytes("UTF-8"))).call
|
||||
git.add.addFilepattern(".").call
|
||||
git.commit.setCommitter(committer.fullName, committer.mailAddress).setMessage(pageName match {
|
||||
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
|
||||
case None => s"Revert ${from} ... ${to}"
|
||||
}).call
|
||||
git.push.call
|
||||
true
|
||||
} catch {
|
||||
case ex: PatchApplyException => false
|
||||
val p = new Patch()
|
||||
p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
|
||||
if(!p.getErrors.isEmpty){
|
||||
throw new PatchFormatException(p.getErrors())
|
||||
}
|
||||
val revertInfo = (p.getFiles.asScala.map { fh =>
|
||||
fh.getChangeType match {
|
||||
case DiffEntry.ChangeType.MODIFY => {
|
||||
val source = getWikiPage(owner, repository, fh.getNewPath.replaceFirst("\\.md$", "")).map(_.content).getOrElse("")
|
||||
val applied = PatchUtil.apply(source, patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else Nil
|
||||
}
|
||||
case DiffEntry.ChangeType.ADD => {
|
||||
val applied = PatchUtil.apply("", patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else Nil
|
||||
}
|
||||
case DiffEntry.ChangeType.DELETE => {
|
||||
Seq(RevertInfo("DELETE", fh.getNewPath, ""))
|
||||
}
|
||||
case DiffEntry.ChangeType.RENAME => {
|
||||
val applied = PatchUtil.apply("", patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else {
|
||||
Seq(RevertInfo("DELETE", fh.getOldPath, ""))
|
||||
}
|
||||
}
|
||||
case _ => Nil
|
||||
}
|
||||
}).flatten
|
||||
|
||||
if(revertInfo.nonEmpty){
|
||||
val builder = DirCache.newInCore.builder()
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revertInfo.filter(_.operation == "ADD").foreach { x =>
|
||||
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
|
||||
}
|
||||
builder.finish()
|
||||
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||
pageName match {
|
||||
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
|
||||
case None => s"Revert ${from} ... ${to}"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save the wiki page.
|
||||
*/
|
||||
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
||||
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
|
||||
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
|
||||
// clone working copy
|
||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
||||
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 created = true
|
||||
var updated = false
|
||||
var removed = false
|
||||
|
||||
// write as file
|
||||
using(Git.open(workDir)){ git =>
|
||||
defining(new File(workDir, newPageName + ".md")){ file =>
|
||||
// new page
|
||||
val created = !file.exists
|
||||
|
||||
// created or updated
|
||||
val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
|
||||
FileUtils.writeStringToFile(file, content, "UTF-8")
|
||||
git.add.addFilepattern(file.getName).call
|
||||
}
|
||||
|
||||
// delete file
|
||||
val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){
|
||||
git.rm.addFilepattern(currentPageName + ".md").call
|
||||
}
|
||||
|
||||
// commit and push
|
||||
optionIf(added || deleted){
|
||||
defining(git.commit.setCommitter(committer.fullName, committer.mailAddress)
|
||||
.setMessage(if(message.trim.length == 0){
|
||||
if(deleted){
|
||||
s"Rename ${currentPageName} to ${newPageName}"
|
||||
} else if(created){
|
||||
s"Created ${newPageName}"
|
||||
} else {
|
||||
s"Updated ${newPageName}"
|
||||
}
|
||||
} else {
|
||||
message
|
||||
}).call){ commit =>
|
||||
git.push.call
|
||||
Some(commit.getName)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(created || updated || removed){
|
||||
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||
if(message.trim.length == 0) {
|
||||
if(removed){
|
||||
s"Rename ${currentPageName} to ${newPageName}"
|
||||
} else if(created){
|
||||
s"Created ${newPageName}"
|
||||
} else {
|
||||
s"Updated ${newPageName}"
|
||||
}
|
||||
} else {
|
||||
message
|
||||
})
|
||||
|
||||
Some(newHeadId)
|
||||
} else None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,36 +268,35 @@ trait WikiService {
|
||||
*/
|
||||
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
||||
committer: String, mailAddress: String, message: String): Unit = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
|
||||
// clone working copy
|
||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
||||
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
|
||||
|
||||
// delete file
|
||||
new File(workDir, pageName + ".md").delete
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using(Git.open(workDir)){ git =>
|
||||
git.rm.addFilepattern(pageName + ".md").call
|
||||
|
||||
// commit and push
|
||||
git.commit.setCommitter(committer, mailAddress).setMessage(message).call
|
||||
git.push.call
|
||||
if(removed){
|
||||
builder.finish()
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
|
||||
if(!workDir.exists){
|
||||
Git.cloneRepository
|
||||
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
|
||||
.setDirectory(workDir)
|
||||
.call
|
||||
.getRepository
|
||||
.close
|
||||
} else using(Git.open(workDir)){ git =>
|
||||
git.pull.call
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ object AutoUpdate {
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
Version(1, 11),
|
||||
Version(1, 10),
|
||||
Version(1, 9),
|
||||
Version(1, 8),
|
||||
Version(1, 7),
|
||||
Version(1, 6),
|
||||
Version(1, 5),
|
||||
@@ -86,7 +90,7 @@ object AutoUpdate {
|
||||
/**
|
||||
* The version file (GITBUCKET_HOME/version).
|
||||
*/
|
||||
val versionFile = new File(GitBucketHome, "version")
|
||||
lazy val versionFile = new File(GitBucketHome, "version")
|
||||
|
||||
/**
|
||||
* Returns the current version from the version file.
|
||||
@@ -101,11 +105,8 @@ object AutoUpdate {
|
||||
}
|
||||
case _ => Version(0, 0)
|
||||
}
|
||||
} else {
|
||||
Version(0, 0)
|
||||
}
|
||||
|
||||
}
|
||||
} else Version(0, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -117,8 +118,12 @@ class AutoUpdateListener extends ServletContextListener {
|
||||
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
||||
|
||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||
val datadir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||
if(datadir != null){
|
||||
System.setProperty("gitbucket.home", datadir)
|
||||
}
|
||||
org.h2.Driver.load()
|
||||
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 =>
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.slf4j.LoggerFactory
|
||||
import javax.servlet.ServletConfig
|
||||
import javax.servlet.ServletContext
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import util.{Keys, JGitUtil, Directory}
|
||||
import util.{StringUtil, Keys, JGitUtil, Directory}
|
||||
import util.ControlUtil._
|
||||
import util.Implicits._
|
||||
import service._
|
||||
@@ -43,33 +43,33 @@ class GitRepositoryServlet extends GitServlet {
|
||||
|
||||
def getServletContext(): ServletContext = config.getServletContext
|
||||
def getServletName(): String = config.getServletName
|
||||
});
|
||||
})
|
||||
|
||||
super.init(config)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
||||
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
||||
|
||||
logger.debug("requestURI: " + request.getRequestURI)
|
||||
logger.debug("userName:" + userName)
|
||||
logger.debug("pusher:" + pusher)
|
||||
|
||||
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)
|
||||
|
||||
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL))
|
||||
if(!repository.endsWith(".wiki")){
|
||||
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
|
||||
}
|
||||
receivePack
|
||||
}
|
||||
}
|
||||
@@ -77,84 +77,118 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
class CommitLogHook(owner: String, repository: String, userName: String, baseURL: String) extends PostReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with WebHookService {
|
||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||
|
||||
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||
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("/")
|
||||
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 =>
|
||||
optionIf(!existIds.contains(commit.id)){
|
||||
createIssueComment(commit)
|
||||
Some(commit)
|
||||
// Extract new commit and apply issue comment
|
||||
val newCommits = if(commits.size > 1000){
|
||||
val existIds = getAllCommitIds(owner, repository)
|
||||
commits.flatMap { commit =>
|
||||
if(!existIds.contains(commit.id)){
|
||||
createIssueComment(commit)
|
||||
Some(commit)
|
||||
} else None
|
||||
}
|
||||
} else {
|
||||
commits.flatMap { commit =>
|
||||
if(!existsCommitId(owner, repository, commit.id)){
|
||||
createIssueComment(commit)
|
||||
Some(commit)
|
||||
} else None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
commits.flatMap { commit =>
|
||||
optionIf(!existsCommitId(owner, repository, commit.id)){
|
||||
createIssueComment(commit)
|
||||
Some(commit)
|
||||
|
||||
// batch insert all new commit id
|
||||
insertAllCommitIds(owner, repository, newCommits.map(_.id))
|
||||
|
||||
// record activity
|
||||
if(refName(1) == "heads"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
|
||||
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
|
||||
case _ =>
|
||||
}
|
||||
} else if(refName(1) == "tags"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// batch insert all new commit id
|
||||
insertAllCommitIds(owner, repository, newCommits.map(_.id))
|
||||
if(refName(1) == "heads"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE |
|
||||
ReceiveCommand.Type.UPDATE |
|
||||
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
|
||||
updatePullRequests(branchName)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
// record activity
|
||||
if(refName(1) == "heads"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, userName, branchName)
|
||||
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, userName, branchName)
|
||||
// call web hook
|
||||
getWebHookURLs(owner, repository) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(pusherAccount <- getAccountByUserName(pusher);
|
||||
ownerAccount <- getAccountByUserName(owner);
|
||||
repositoryInfo <- getRepository(owner, repository, baseUrl)){
|
||||
callWebHook(owner, repository, webHookURLs,
|
||||
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
} else if(refName(1) == "tags"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, userName, branchName, newCommits)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
// call web hook
|
||||
val webHookURLs = getWebHookURLs(owner, repository)
|
||||
if(webHookURLs.nonEmpty){
|
||||
val payload = WebHookPayload(
|
||||
git,
|
||||
command.getRefName,
|
||||
getRepository(owner, repository, baseURL).get,
|
||||
newCommits,
|
||||
getAccountByUserName(owner).get)
|
||||
|
||||
callWebHook(owner, repository, webHookURLs, payload)
|
||||
}
|
||||
}
|
||||
// update repository last modified time.
|
||||
updateLastActivityDate(owner, repository)
|
||||
} catch {
|
||||
case ex: Exception => {
|
||||
logger.error(ex.toString, ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
// update repository last modified time.
|
||||
updateLastActivityDate(owner, repository)
|
||||
}
|
||||
|
||||
private def createIssueComment(commit: CommitInfo) = {
|
||||
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
|
||||
val issueId = matchData.group(2)
|
||||
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
|
||||
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
|
||||
*/
|
||||
private def updatePullRequests(branch: String) =
|
||||
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
|
||||
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
|
||||
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
|
||||
git.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package util
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.treewalk.TreeWalk
|
||||
import scala.util.control.Exception._
|
||||
import scala.language.reflectiveCalls
|
||||
|
||||
/**
|
||||
* Provides control facilities.
|
||||
@@ -14,21 +16,19 @@ object ControlUtil {
|
||||
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
|
||||
try f(resource) finally {
|
||||
if(resource != null){
|
||||
try {
|
||||
allCatch {
|
||||
resource.close()
|
||||
} catch {
|
||||
case e: Throwable => // ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def using[T](git: Git)(f: Git => T): T =
|
||||
try f(git) finally git.getRepository.close
|
||||
try f(git) finally git.getRepository.close()
|
||||
|
||||
def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
|
||||
try f(git1, git2) finally {
|
||||
git1.getRepository.close
|
||||
git2.getRepository.close
|
||||
git1.getRepository.close()
|
||||
git2.getRepository.close()
|
||||
}
|
||||
|
||||
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
|
||||
@@ -37,12 +37,15 @@ object ControlUtil {
|
||||
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
|
||||
try f(treeWalk) finally treeWalk.release()
|
||||
|
||||
def executeIf(condition: => Boolean)(action: => Unit): Boolean =
|
||||
if(condition){
|
||||
action
|
||||
true
|
||||
} else false
|
||||
|
||||
def optionIf[T](condition: => Boolean)(action: => Option[T]): Option[T] =
|
||||
if(condition) action else None
|
||||
// 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()
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -2,15 +2,30 @@ package util
|
||||
|
||||
import java.io.File
|
||||
import util.ControlUtil._
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
/**
|
||||
* Provides directories used by GitBucket.
|
||||
*/
|
||||
object Directory {
|
||||
|
||||
val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
|
||||
case Some(env) => new File(env)
|
||||
case None => new File(System.getProperty("user.home"), "gitbucket")
|
||||
val GitBucketHome = (System.getProperty("gitbucket.home") match {
|
||||
// -Dgitbucket.home=<path>
|
||||
case path if(path != null) => new File(path)
|
||||
case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
|
||||
// environment variable GITBUCKET_HOME
|
||||
case Some(env) => new File(env)
|
||||
// default is HOME/.gitbucket
|
||||
case None => {
|
||||
val oldHome = new File(System.getProperty("user.home"), "gitbucket")
|
||||
if(oldHome.exists && oldHome.isDirectory && new File(oldHome, "version").exists){
|
||||
//FileUtils.moveDirectory(oldHome, newHome)
|
||||
oldHome
|
||||
} else {
|
||||
new File(System.getProperty("user.home"), ".gitbucket")
|
||||
}
|
||||
}
|
||||
}
|
||||
}).getAbsolutePath
|
||||
|
||||
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
|
||||
@@ -56,25 +71,10 @@ object Directory {
|
||||
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
|
||||
new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
|
||||
|
||||
/**
|
||||
* Temporary directory which is used in the repository creation.
|
||||
*
|
||||
* GitBucket generates initial repository contents in this directory and push them.
|
||||
* This directory is removed after the repository creation.
|
||||
*/
|
||||
def getInitRepositoryDir(owner: String, repository: String): File =
|
||||
new File(getTemporaryDir(owner, repository), "init")
|
||||
|
||||
/**
|
||||
* Substance directory of the wiki repository.
|
||||
*/
|
||||
def getWikiRepositoryDir(owner: String, repository: String): File =
|
||||
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
|
||||
|
||||
/**
|
||||
* Wiki working directory which is cloned from the wiki repository.
|
||||
*/
|
||||
def getWikiWorkDir(owner: String, repository: String): File =
|
||||
new File(getTemporaryDir(owner, repository), "wiki")
|
||||
|
||||
}
|
||||
@@ -63,9 +63,9 @@ object FileUtil {
|
||||
if(dir.exists()){
|
||||
FileUtils.deleteDirectory(dir)
|
||||
}
|
||||
try{
|
||||
try {
|
||||
action(dir)
|
||||
}finally{
|
||||
} finally {
|
||||
FileUtils.deleteDirectory(dir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.eclipse.jgit.errors.MissingObjectException
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.errors.NoHeadException
|
||||
import service.RepositoryService
|
||||
import org.eclipse.jgit.dircache.DirCacheEntry
|
||||
|
||||
/**
|
||||
* Provides complex JGit operations.
|
||||
@@ -78,9 +79,9 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
|
||||
optionIf(i >= 0){
|
||||
if(i >= 0){
|
||||
Some(fullMessage.trim.substring(i).trim)
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,4 +465,33 @@ object JGitUtil {
|
||||
}.find(_._1 != null)
|
||||
}
|
||||
|
||||
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
|
||||
val entry = new DirCacheEntry(path)
|
||||
entry.setFileMode(mode)
|
||||
entry.setObjectId(objectId)
|
||||
entry
|
||||
}
|
||||
|
||||
def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
|
||||
fullName: String, mailAddress: String, message: String): String = {
|
||||
val newCommit = new CommitBuilder()
|
||||
newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
|
||||
newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
|
||||
newCommit.setMessage(message)
|
||||
if(headId != null){
|
||||
newCommit.setParentIds(List(headId).asJava)
|
||||
}
|
||||
newCommit.setTreeId(treeId)
|
||||
|
||||
val newHeadId = inserter.insert(newCommit)
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
|
||||
val refUpdate = git.getRepository.updateRef(Constants.HEAD)
|
||||
refUpdate.setNewObjectId(newHeadId)
|
||||
refUpdate.update()
|
||||
|
||||
newHeadId.getName
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,8 @@ package util
|
||||
import util.ControlUtil._
|
||||
import service.SystemSettingsService
|
||||
import com.novell.ldap._
|
||||
import java.security.Security
|
||||
import org.slf4j.LoggerFactory
|
||||
import service.SystemSettingsService.Ldap
|
||||
import scala.annotation.tailrec
|
||||
|
||||
@@ -11,72 +13,98 @@ import scala.annotation.tailrec
|
||||
*/
|
||||
object LDAPUtil {
|
||||
|
||||
private val LDAP_VERSION: Int = 3
|
||||
private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
|
||||
private val logger = LoggerFactory.getLogger(getClass().getName())
|
||||
|
||||
/**
|
||||
* Try authentication by LDAP using given configuration.
|
||||
* Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage).
|
||||
* Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
|
||||
*/
|
||||
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = {
|
||||
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = {
|
||||
bind(
|
||||
ldapSettings.host,
|
||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
ldapSettings.bindDN.getOrElse(""),
|
||||
ldapSettings.bindPassword.getOrElse("")
|
||||
) match {
|
||||
case Some(conn) => {
|
||||
withConnection(conn) { conn =>
|
||||
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
|
||||
case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
|
||||
case None => Left("User does not exist.")
|
||||
}
|
||||
}
|
||||
host = ldapSettings.host,
|
||||
port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
dn = ldapSettings.bindDN.getOrElse(""),
|
||||
password = ldapSettings.bindPassword.getOrElse(""),
|
||||
tls = ldapSettings.tls.getOrElse(false),
|
||||
keystore = ldapSettings.keystore.getOrElse(""),
|
||||
error = "System LDAP authentication failed."
|
||||
){ conn =>
|
||||
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
|
||||
case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
|
||||
case None => Left("User does not exist.")
|
||||
}
|
||||
case None => Left("System LDAP authentication failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = {
|
||||
private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = {
|
||||
bind(
|
||||
ldapSettings.host,
|
||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
userDN,
|
||||
password
|
||||
) match {
|
||||
case Some(conn) => {
|
||||
withConnection(conn) { conn =>
|
||||
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
|
||||
case Some(mailAddress) => Right(mailAddress)
|
||||
case None => Left("Can't find mail address.")
|
||||
}
|
||||
}
|
||||
host = ldapSettings.host,
|
||||
port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
dn = userDN,
|
||||
password = password,
|
||||
tls = ldapSettings.tls.getOrElse(false),
|
||||
keystore = ldapSettings.keystore.getOrElse(""),
|
||||
error = "User LDAP Authentication Failed."
|
||||
){ conn =>
|
||||
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
|
||||
case Some(mailAddress) => Right(LDAPUserInfo(
|
||||
userName = userName,
|
||||
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
|
||||
findFullName(conn, userDN, fullNameAttribute)
|
||||
}.getOrElse(userName),
|
||||
mailAddress = mailAddress))
|
||||
case None => Left("Can't find mail address.")
|
||||
}
|
||||
case None => Left("User LDAP Authentication Failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = {
|
||||
val conn: LDAPConnection = new LDAPConnection
|
||||
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) {
|
||||
// Dynamically set Sun as the security provider
|
||||
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider())
|
||||
|
||||
if (keystore.compareTo("") != 0) {
|
||||
// Dynamically set the property that JSSE uses to identify
|
||||
// the keystore that holds trusted root certificates
|
||||
System.setProperty("javax.net.ssl.trustStore", keystore)
|
||||
}
|
||||
}
|
||||
|
||||
val conn: LDAPConnection = new LDAPConnection(new LDAPJSSEStartTLSFactory())
|
||||
try {
|
||||
// Connect to the server
|
||||
conn.connect(host, port)
|
||||
|
||||
if (tls) {
|
||||
// Secure the connection
|
||||
conn.startTLS()
|
||||
}
|
||||
|
||||
// Bind to the server
|
||||
conn.bind(LDAP_VERSION, dn, password.getBytes)
|
||||
Some(conn)
|
||||
|
||||
// Execute a given function and returns a its result
|
||||
f(conn)
|
||||
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
if (conn.isConnected) conn.disconnect()
|
||||
None
|
||||
// Provide more information if something goes wrong
|
||||
logger.info("" + e)
|
||||
|
||||
if (conn.isConnected) {
|
||||
conn.disconnect()
|
||||
}
|
||||
// Returns an error message
|
||||
Left(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = {
|
||||
try {
|
||||
f(conn)
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a specified user and returns userDN if exists.
|
||||
*/
|
||||
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
|
||||
@tailrec
|
||||
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
|
||||
@@ -97,8 +125,18 @@ object LDAPUtil {
|
||||
|
||||
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
|
||||
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
|
||||
optionIf (results.hasMore) {
|
||||
if(results.hasMore) {
|
||||
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
|
||||
}
|
||||
} else None
|
||||
}
|
||||
|
||||
private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] =
|
||||
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results =>
|
||||
if(results.hasMore) {
|
||||
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
|
||||
} else None
|
||||
}
|
||||
|
||||
case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String)
|
||||
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
.foreach { case (address, name) =>
|
||||
email.setFrom(address, name)
|
||||
}
|
||||
email.setCharset("UTF-8")
|
||||
email.setSubject(subject)
|
||||
email.setHtmlMsg(msg)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package util
|
||||
import java.net.{URLDecoder, URLEncoder}
|
||||
import org.mozilla.universalchardet.UniversalDetector
|
||||
import util.ControlUtil._
|
||||
import org.apache.commons.io.input.BOMInputStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
|
||||
object StringUtil {
|
||||
|
||||
@@ -27,7 +29,12 @@ object StringUtil {
|
||||
def escapeHtml(value: String): String =
|
||||
value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
||||
|
||||
def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content))
|
||||
/**
|
||||
* 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..
|
||||
*/
|
||||
def convertFromByteArray(content: Array[Byte]): String =
|
||||
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
|
||||
|
||||
def detectEncoding(content: Array[Byte]): String =
|
||||
defining(new UniversalDetector(null)){ detector =>
|
||||
@@ -38,4 +45,14 @@ 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 { matchData => matchData.group(2) }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
trait Validations {
|
||||
|
||||
@@ -8,7 +9,7 @@ trait Validations {
|
||||
* Constraint for the identifier such as user name, repository name or page name.
|
||||
*/
|
||||
def identifier: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(!value.matches("^[a-zA-Z0-9\\-_.]+$")){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
@@ -25,7 +26,7 @@ trait Validations {
|
||||
*/
|
||||
def date(constraints: Constraint*): SingleValueType[java.util.Date] =
|
||||
new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
|
||||
def convert(value: String): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
|
||||
def convert(value: String, messages: Messages): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
// by user name
|
||||
getAccountByUserName(userName).map { account =>
|
||||
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/${account.userName}/_avatar"""
|
||||
}
|
||||
@@ -28,13 +28,13 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
// by mail address
|
||||
getAccountByMailAddress(mailAddress).map { account =>
|
||||
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/${account.userName}/_avatar"""
|
||||
}
|
||||
} getOrElse {
|
||||
if(getSystemSettings().gravatar){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/_unknown/_avatar"""
|
||||
}
|
||||
|
||||
@@ -14,22 +14,21 @@ trait LinkConverter { self: RequestCache =>
|
||||
// escape HTML tags
|
||||
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
|
||||
// convert issue id to link
|
||||
.replaceBy(("(^|\\W)" + issueIdPrefix + "(\\d+)(\\W|$)").r){ m =>
|
||||
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
||||
getIssue(repository.owner, repository.name, m.group(2)) match {
|
||||
case Some(issue) if(issue.isPullRequest)
|
||||
=> Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""")
|
||||
case Some(_) => Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""")
|
||||
case None => Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""")
|
||||
|
||||
=> Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>""")
|
||||
case Some(_) => Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>""")
|
||||
case None => Some(s"""#${m.group(2)}""")
|
||||
}
|
||||
}
|
||||
// convert @username to link
|
||||
.replaceBy("(^|\\W)@([a-zA-Z0-9\\-_]+)(\\W|$)".r){ m =>
|
||||
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m =>
|
||||
getAccountByUserName(m.group(2)).map { _ =>
|
||||
s"""${m.group(1)}<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>${m.group(3)}"""
|
||||
s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
|
||||
}
|
||||
}
|
||||
// convert commit id to link
|
||||
.replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", s"""$$1<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>$$3""")
|
||||
.replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -19,11 +21,11 @@ object Markdown {
|
||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
|
||||
// escape issue id
|
||||
val source = if(enableRefsLink){
|
||||
markdown.replaceAll("(^|\\W)#([0-9]+)(\\W|$)", "$1issue:$2$3")
|
||||
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
||||
} else markdown
|
||||
|
||||
val rootNode = new PegDownProcessor(
|
||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES
|
||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
|
||||
).parseMarkdown(source.toCharArray)
|
||||
|
||||
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
|
||||
@@ -110,6 +112,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 +137,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,16 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
|
||||
|
||||
/**
|
||||
* Returns <img> which displays the avatar icon.
|
||||
* Looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
* 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.
|
||||
*/
|
||||
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
|
||||
getAvatarImageHtml(userName, size, "", tooltip)
|
||||
|
||||
/**
|
||||
* Returns <img> which displays the avatar icon for the given mail address.
|
||||
* This method looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
*/
|
||||
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
|
||||
getAvatarImageHtml(commit.committer, size, commit.mailAddress)
|
||||
|
||||
@@ -57,7 +61,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
value
|
||||
}
|
||||
|
||||
import scala.util.matching.Regex
|
||||
import scala.util.matching.Regex._
|
||||
implicit class RegexReplaceString(s: String) {
|
||||
def replaceAll(pattern: String, replacer: (Match) => String): String = {
|
||||
@@ -65,6 +68,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert link notations in the activity message.
|
||||
*/
|
||||
def activityMessage(message: String)(implicit context: app.Context): Html =
|
||||
Html(message
|
||||
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
|
||||
@@ -72,7 +78,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
|
||||
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||
.replaceAll("\\[user:([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1">$$1</a>""")
|
||||
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -101,14 +107,32 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
def assets(implicit context: app.Context): String = s"${context.path}/assets"
|
||||
|
||||
/**
|
||||
* Generates the link to the account page.
|
||||
* Generates the text link to the account page.
|
||||
* If user does not exist or disabled, this method returns user name as text without link.
|
||||
*/
|
||||
def user(userName: String, mailAddress: String, styleClass: String = "")(implicit context: app.Context): Html = {
|
||||
getAccountByMailAddress(mailAddress).map { account =>
|
||||
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${userName}</a>""")
|
||||
} getOrElse Html(userName)
|
||||
}
|
||||
def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: app.Context): Html =
|
||||
userWithContent(userName, mailAddress, styleClass)(Html(userName))
|
||||
|
||||
/**
|
||||
* Generates the avatar link to the account page.
|
||||
* If user does not exist or disabled, this method returns avatar image without link.
|
||||
*/
|
||||
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html =
|
||||
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip))
|
||||
|
||||
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: app.Context): Html =
|
||||
(if(mailAddress.isEmpty){
|
||||
getAccountByUserName(userName)
|
||||
} else {
|
||||
getAccountByMailAddress(mailAddress)
|
||||
}).map { account =>
|
||||
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${content}</a>""")
|
||||
} getOrElse content
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given Date is past date.
|
||||
*/
|
||||
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
</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>
|
||||
<input type="submit" class="btn btn-success" value="Save"/>
|
||||
<a href="@url(account.get.userName)" class="btn">Cancel</a>
|
||||
} else {
|
||||
@@ -64,3 +67,10 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#delete').click(function(){
|
||||
return confirm('Once you delete your account, there is no going back.\nAre you sure?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,5 +1,6 @@
|
||||
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import util.Directory._
|
||||
@import view.helpers._
|
||||
@html.main("System Settings"){
|
||||
@menu("system"){
|
||||
@@ -8,9 +9,30 @@
|
||||
<div class="box">
|
||||
<div class="box-header">System Settings</div>
|
||||
<div class="box-content">
|
||||
<!--====================================================================-->
|
||||
<!-- GITBUCKET_HOME -->
|
||||
<!--====================================================================-->
|
||||
<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"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p>
|
||||
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>
|
||||
<label class="strong">Account registration</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
@@ -87,6 +109,13 @@
|
||||
<span id="error-ldap_userNameAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapFullNameAttribute">Full name attribute</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" value="@settings.ldap.map(_.fullNameAttribute)"/>
|
||||
<span id="error-ldap_fullNameAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
|
||||
<div class="controls">
|
||||
@@ -94,6 +123,20 @@
|
||||
<span id="error-ldap_mailAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="ldap.tls"@if(settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/> Enable TLS
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapBindDN">Keystore</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapKeystore" name="ldap.keystore" value="@settings.ldap.map(_.keystore)"/>
|
||||
<span id="error-ldap_keystore" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Notification email -->
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
<span id="error-groupName" class="error"></span>
|
||||
</div>
|
||||
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
@if(account.isDefined){
|
||||
<label for="removed">
|
||||
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
|
||||
Disable
|
||||
</label>
|
||||
}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label class="strong">URL (Optional)</label>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
@(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context)
|
||||
@(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Manage Users"){
|
||||
@admin.html.menu("users"){
|
||||
<div style="text-align: right; margin-bottom: 4px;">
|
||||
<div class="pull-right" style="margin-bottom: 4px;">
|
||||
<a href="@path/admin/users/_newuser" class="btn">New User</a>
|
||||
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
|
||||
</div>
|
||||
<label for="includeRemoved">
|
||||
<input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/>
|
||||
Include removed users
|
||||
</label>
|
||||
<table class="table table-bordered table-hover">
|
||||
@users.map { account =>
|
||||
<tr>
|
||||
<td>
|
||||
<td @if(account.isRemoved){style="background-color: #dddddd;"}>
|
||||
<div class="pull-right">
|
||||
@if(account.isGroupAccount){
|
||||
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
|
||||
@@ -57,4 +61,11 @@
|
||||
}
|
||||
</table>
|
||||
}
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#includeRemoved').click(function(){
|
||||
location.href = '@path/admin/users?includeRemoved=' + this.checked;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -11,6 +11,12 @@
|
||||
<span id="error-userName" class="error"></span>
|
||||
</div>
|
||||
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
@if(account.isDefined){
|
||||
<label for="removed">
|
||||
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
|
||||
Disable
|
||||
</label>
|
||||
}
|
||||
</fieldset>
|
||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||
<fieldset>
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
}
|
||||
<div class="head">
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
@if(!repository.repository.isPrivate){
|
||||
<i class="icon-eye-open"></i>
|
||||
<img src="@assets/common/images/repo_private_lg.png"/>
|
||||
} else {
|
||||
@if(repository.repository.originUserName.isDefined){
|
||||
<img src="@assets/common/images/repo_fork_lg.png"/>
|
||||
} else {
|
||||
<img src="@assets/common/images/repo_public_lg.png"/>
|
||||
}
|
||||
}
|
||||
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@(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">
|
||||
@if(value.isEmpty){
|
||||
@@ -11,7 +11,7 @@
|
||||
}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<ul class="dropdown-menu@if(right){ pull-right}">
|
||||
@body
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -32,10 +32,13 @@
|
||||
<tr>
|
||||
<td>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
@if(!repository.repository.isPrivate){
|
||||
<i class="icon-eye-open"></i>
|
||||
<img src="@assets/common/images/repo_private.png"/>
|
||||
} else {
|
||||
@if(repository.repository.originUserName.isDefined){
|
||||
<img src="@assets/common/images/repo_fork.png"/>
|
||||
} else {
|
||||
<img src="@assets/common/images/repo_public.png"/>
|
||||
}
|
||||
}
|
||||
@if(repository.owner == loginAccount.get.userName){
|
||||
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
||||
@@ -64,10 +67,13 @@
|
||||
<tr>
|
||||
<td>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
@if(!repository.repository.isPrivate){
|
||||
<i class="icon-eye-open"></i>
|
||||
<img src="@assets/common/images/repo_private.png"/>
|
||||
} else {
|
||||
@if(repository.repository.originUserName.isDefined){
|
||||
<img src="@assets/common/images/repo_fork.png"/>
|
||||
} else {
|
||||
<img src="@assets/common/images/repo_public.png"/>
|
||||
}
|
||||
}
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
</td>
|
||||
|
||||
@@ -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>
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> 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>
|
||||
@@ -36,11 +48,11 @@
|
||||
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<span class="label label-info">Merged</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code>
|
||||
@user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
|
||||
@if(pullreq.get.requestUserName == repository.owner){
|
||||
<span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.branch)</span>
|
||||
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
|
||||
} else {
|
||||
<span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span>
|
||||
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
|
||||
}
|
||||
@datetime(comment.registeredDate)
|
||||
</div>
|
||||
@@ -50,9 +62,9 @@
|
||||
<span class="label label-important">Closed</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
@if(issue.isPullRequest){
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the pull request @datetime(comment.registeredDate)
|
||||
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate)
|
||||
} else {
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
|
||||
@user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -60,7 +72,14 @@
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label label-success">Reopened</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
|
||||
@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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
@import context._
|
||||
<span id="error-edit-content-@commentId" class="error"></span>
|
||||
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||
<input type="button" class="btn btn-small" value="Update Comment"/>
|
||||
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
|
||||
<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"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
var callback = function(data){
|
||||
$('#commentContent-@commentId').empty().html(data.content);
|
||||
};
|
||||
|
||||
$('#commentContent-@commentId input.btn').click(function(){
|
||||
$('#update-comment-@commentId').click(function(){
|
||||
$.ajax({
|
||||
url: '@path/@owner/@repository/issue_comments/edit/@commentId',
|
||||
type: 'POST',
|
||||
@@ -25,7 +27,7 @@ $(function(){
|
||||
});
|
||||
});
|
||||
|
||||
$('#commentContent-@commentId a.btn').click(function(){
|
||||
$('#cancel-comment-@commentId').click(function(){
|
||||
$.get('@path/@owner/@repository/issue_comments/_data/@commentId', callback);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<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="button" class="btn btn-small" value="Update Issue"/>
|
||||
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
|
||||
<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"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#edit-content').elastic();
|
||||
@@ -14,7 +16,7 @@ $(function(){
|
||||
$('#issueContent').empty().html(data.content);
|
||||
};
|
||||
|
||||
$('#issueContent input.btn').click(function(){
|
||||
$('#update').click(function(){
|
||||
$.ajax({
|
||||
url: '@path/@owner/@repository/issues/edit/@issueId',
|
||||
type: 'POST',
|
||||
@@ -29,7 +31,7 @@ $(function(){
|
||||
});
|
||||
});
|
||||
|
||||
$('#issueContent a.btn').click(function(){
|
||||
$('#cancel').click(function(){
|
||||
$.get('@path/@owner/@repository/issues/_data/@issueId', callback);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -33,51 +33,7 @@
|
||||
}
|
||||
</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>
|
||||
}
|
||||
</div>
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labellist(issueLabels)
|
||||
</ul>
|
||||
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
|
||||
</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>
|
||||
@@ -14,14 +14,14 @@
|
||||
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
|
||||
}
|
||||
<div class="small muted">
|
||||
<a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> opened this issue @datetime(issue.registeredDate)
|
||||
@user(issue.openedUserName, styleClass="username strong") opened this issue @datetime(issue.registeredDate)
|
||||
</div>
|
||||
<h4 id="issueTitle">@issue.title</h4>
|
||||
</div>
|
||||
<div class="issue-info">
|
||||
<span id="label-assigned">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@avatar(userName, 20) <a href="@url(userName)" class="username strong">@userName</a> is assigned
|
||||
@avatar(userName, 20) @user(userName, styleClass="username strong") is assigned
|
||||
}.getOrElse("No one is assigned")
|
||||
</span>
|
||||
@if(hasWritePermission){
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="issue-participants">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
<span class="strong">@participants.size</span> @plural(participants.size, "participant")
|
||||
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
|
||||
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
|
||||
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>
|
||||
@@ -165,7 +165,7 @@
|
||||
#@issue.issueId
|
||||
</span>
|
||||
<div class="small muted" style="margin-left: 20px;">
|
||||
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)
|
||||
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||
@if(commentCount > 0){
|
||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
|
||||
} else {
|
||||
<a href="@path/signin?redirect=@redirectUrl" class="btn btn-last">Sign in</a>
|
||||
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
|
||||
}
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
|
||||
diffs: Seq[util.JGitUtil.DiffInfo],
|
||||
members: List[String],
|
||||
members: List[(String, String)],
|
||||
originId: String,
|
||||
forkedId: String,
|
||||
sourceId: String,
|
||||
@@ -20,25 +20,25 @@
|
||||
</div>
|
||||
<div id="compare-edit" style="display: none;">
|
||||
<a href="#" id="cancel-condition-editing" class="pull-right"><i class="icon-remove-circle"></i></a>
|
||||
@helper.html.dropdown(originRepository.owner + "/" + repository.name, "base fork") {
|
||||
@members.map { member =>
|
||||
<li><a href="#" class="origin-owner" data-name="@member">@helper.html.checkicon(member == originRepository.owner) @member/@repository.name</a></li>
|
||||
@helper.html.dropdown(originRepository.owner + "/" + originRepository.name, "base fork") {
|
||||
@members.map { case (owner, name) =>
|
||||
<li><a href="#" class="origin-owner" data-owner="@owner" data-name="@name">@helper.html.checkicon(owner == originRepository.owner) @owner/@name</a></li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown(originId, "base") {
|
||||
@originRepository.branchList.map { branch =>
|
||||
<li><a href="#" class="origin-branch" data-name="@encodeRefName(branch)">@helper.html.checkicon(branch == originId) @branch</a></li>
|
||||
<li><a href="#" class="origin-branch" data-branch="@encodeRefName(branch)">@helper.html.checkicon(branch == originId) @branch</a></li>
|
||||
}
|
||||
}
|
||||
...
|
||||
@helper.html.dropdown(forkedRepository.owner + "/" + repository.name, "head fork") {
|
||||
@members.map { member =>
|
||||
<li><a href="#" class="forked-owner" data-name="@member">@helper.html.checkicon(member == forkedRepository.owner) @member/@repository.name</a></li>
|
||||
@helper.html.dropdown(forkedRepository.owner + "/" + forkedRepository.name, "head fork") {
|
||||
@members.map { case (owner, name) =>
|
||||
<li><a href="#" class="forked-owner" data-owner="@owner" data-name="@name">@helper.html.checkicon(owner == forkedRepository.owner) @owner/@name</a></li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown(forkedId, "compare") {
|
||||
@forkedRepository.branchList.map { branch =>
|
||||
<li><a href="#" class="forked-branch" data-name="@encodeRefName(branch)">@helper.html.checkicon(branch == forkedId) @branch</a></li>
|
||||
<li><a href="#" class="forked-branch" data-branch="@encodeRefName(branch)">@helper.html.checkicon(branch == forkedId) @branch</a></li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
<div id="pull-request-form" class="box" style="display: none;">
|
||||
<div class="box-content">
|
||||
<form method="POST" action="@path/@originRepository.owner/@repository.name/pulls/new" validate="true">
|
||||
<form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
|
||||
<div style="width: 260px; position: absolute; margin-left: 635px;">
|
||||
<div class="check-conflict" style="display: none;">
|
||||
<img src="@assets/common/images/indicator.gif"/> Checking...
|
||||
@@ -62,6 +62,7 @@
|
||||
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
|
||||
<input type="hidden" name="targetBranch" value="@originId"/>
|
||||
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
|
||||
<input type="hidden" name="requestRepositoryName" value="@forkedRepository.name"/>
|
||||
<input type="hidden" name="requestBranch" value="@forkedId"/>
|
||||
<input type="hidden" name="commitIdFrom" value="@sourceId"/>
|
||||
<input type="hidden" name="commitIdTo" value="@commitId"/>
|
||||
@@ -104,14 +105,16 @@ $(function(){
|
||||
|
||||
@if(members.isEmpty){
|
||||
location.href = '@url(repository)/compare/' +
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')) + '...' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'));
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'));
|
||||
} else {
|
||||
location.href = '@url(repository)/compare/' +
|
||||
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('name')) + ':' +
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')) + '...' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) + ':' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'));
|
||||
location.href = '@path/' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + '/' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) +'/compare/' +
|
||||
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -129,15 +132,15 @@ $(function(){
|
||||
|
||||
@if(members.isEmpty){
|
||||
checkConflict(
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')),
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'))
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')),
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'))
|
||||
);
|
||||
} else {
|
||||
checkConflict(
|
||||
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('name')) + ":" +
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')),
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) + ":" +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'))
|
||||
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')),
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
@(issue: model.Issue,
|
||||
pullreq: model.PullRequest,
|
||||
comments: List[model.IssueComment],
|
||||
issueLabels: List[model.Label],
|
||||
collaborators: List[String],
|
||||
milestones: List[(model.Milestone, Int, Int)],
|
||||
labels: List[model.Label],
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@@ -35,6 +37,18 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName &&
|
||||
pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
|
||||
<div class="box issue-comment-box" style="background-color: #d0eeff;">
|
||||
<div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
|
||||
<a href="@url(repository)/pull/@issue.issueId/delete/@pullreq.requestBranch" class="btn btn-info pull-right delete-branch" data-name="@pullreq.requestBranch">Delete branch</a>
|
||||
<div>
|
||||
<span class="strong">Pull request successfully merged and closed</span>
|
||||
</div>
|
||||
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@issues.html.commentform(issue, hasWritePermission, repository)
|
||||
</div>
|
||||
<div class="span2">
|
||||
@@ -51,6 +65,7 @@
|
||||
<span class="strong">@comments.size</span> @plural(comments.size, "comment")
|
||||
</div>
|
||||
<hr/>
|
||||
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
@@ -62,8 +77,12 @@ $(function(){
|
||||
|
||||
@if(hasWritePermission){
|
||||
$('.check-conflict').show();
|
||||
$.get('@url(repository)/pull/@issue.issueId/mergeguide',
|
||||
function(data){ $('.check-conflict').html(data); });
|
||||
$.get('@url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
|
||||
|
||||
$('.delete-branch').click(function(e){
|
||||
var branchName = $(e.target).data('name');
|
||||
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -85,7 +85,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="small muted" style="margin-left: 20px;">
|
||||
@avatar(issue.openedUserName, 20) by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)
|
||||
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||
@if(commentCount > 0){
|
||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
@(issue: model.Issue,
|
||||
pullreq: model.PullRequest,
|
||||
comments: List[model.IssueComment],
|
||||
issueLabels: List[model.Label],
|
||||
collaborators: List[String],
|
||||
milestones: List[(model.Milestone, Int, Int)],
|
||||
labels: List[model.Label],
|
||||
dayByDayCommits: Seq[Seq[util.JGitUtil.CommitInfo]],
|
||||
diffs: Seq[util.JGitUtil.DiffInfo],
|
||||
hasWritePermission: Boolean,
|
||||
@@ -16,18 +18,18 @@
|
||||
@if(issue.closed) {
|
||||
@comments.find(_.action == "merge").map{ comment =>
|
||||
<span class="label label-info">Merged</span>
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> merged @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code>
|
||||
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
at @datetime(comment.registeredDate)
|
||||
}.getOrElse {
|
||||
<span class="label label-important">Closed</span>
|
||||
<a href="@url(issue.openedUserName)" class="username strong">@issue.openedUserName</a> wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code>
|
||||
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
}
|
||||
} else {
|
||||
<span class="label label-success">Open</span>
|
||||
<a href="@url(issue.openedUserName)" class="username strong">@issue.openedUserName</a> wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code>
|
||||
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
}
|
||||
</div>
|
||||
<ul class="nav nav-tabs" id="pullreq-tab">
|
||||
@@ -37,7 +39,7 @@
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="discussion">
|
||||
@pulls.html.discussion(issue, pullreq, comments, collaborators, milestones, hasWritePermission, repository)
|
||||
@pulls.html.discussion(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
<div class="tab-pane" id="commits">
|
||||
@pulls.html.commits(dayByDayCommits, repository)
|
||||
|
||||
@@ -52,3 +52,33 @@
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
<script src="@assets/common/js/jquery.ba-hashchange.js"></script>
|
||||
<script>
|
||||
$(window).load(function(){
|
||||
$(window).hashchange(function(){
|
||||
updateHighlighting();
|
||||
}).hashchange();
|
||||
});
|
||||
|
||||
/**
|
||||
* Hightlight lines which are specified by URL hash.
|
||||
*/
|
||||
function updateHighlighting(){
|
||||
var hash = location.hash;
|
||||
if(hash.match(/#L\d+(-L\d+)?/)){
|
||||
$('li.highlight').removeClass('highlight');
|
||||
var lines = hash.substr(1).split('-');
|
||||
if(lines.length == 1){
|
||||
$('#' + lines[0]).addClass('highlight');
|
||||
$(window).scrollTop($('#' + lines[0]).offset().top - 40);
|
||||
} else if(lines.length > 1){
|
||||
var start = parseInt(lines[0].substr(1));
|
||||
var end = parseInt(lines[1].substr(1));
|
||||
for(var i = start; i <= end; i++){
|
||||
$('#L' + i).addClass('highlight');
|
||||
}
|
||||
$(window).scrollTop($('#L' + start).offset().top - 40);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,4 +1,5 @@
|
||||
@(branchInfo: List[(String, java.util.Date)],
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@@ -17,9 +18,9 @@
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@url(repository)/tree/@encodeRefName(branchName)">@branchName</a>
|
||||
@*
|
||||
<a href="#" class="btn btn-danger btn-mini">Delete branch</a>
|
||||
*@
|
||||
@if(hasWritePermission && repository.repository.defaultBranch != branchName){
|
||||
<a href="@url(repository)/delete/@encodeRefName(branchName)" class="btn btn-danger btn-mini pull-right delete-branch" data-name="@branchName">Delete branch</a>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@datetime(latestUpdateDate)
|
||||
@@ -35,4 +36,12 @@
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('.delete-branch').click(function(e){
|
||||
var branchName = $(e.target).data('name');
|
||||
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
pathList: List[String],
|
||||
latestCommit: util.JGitUtil.CommitInfo,
|
||||
files: List[util.JGitUtil.FileInfo],
|
||||
readme: Option[String])(implicit context: app.Context)
|
||||
readme: Option[(util.JGitUtil.FileInfo, String)])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@@ -77,10 +77,10 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@readme.map { content =>
|
||||
@readme.map { case(file, content) =>
|
||||
<div id="readme" class="box">
|
||||
<div class="box-header">README.md</div>
|
||||
<div class="box-content">@markdown(content, repository, false, false)</div>
|
||||
<div class="box-header">@file.name</div>
|
||||
<div class="box-content markdown-body">@markdown(content, repository, false, false)</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@(originRepository: Option[service.RepositoryService.RepositoryInfo],
|
||||
members: List[String],
|
||||
members: List[(String, String)],
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@@ -23,11 +23,11 @@
|
||||
}
|
||||
(origin)
|
||||
</div>
|
||||
@members.map { owner =>
|
||||
@members.map { case (owner, name) =>
|
||||
<div class="block">
|
||||
@avatar(owner, 20)
|
||||
<span@if(repository.owner == owner){ class="highlight"}>
|
||||
<a href="@url(owner)">@owner</a> / <a href="@path/@owner/@repository.name">@repository.name</a>
|
||||
<a href="@url(owner)">@owner</a> / <a href="@path/@owner/@name">@name</a>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
</ul>
|
||||
@if(!isGroupRepository){
|
||||
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true">
|
||||
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true" autocomplete="off">
|
||||
<div>
|
||||
<span class="error" id="error-userName"></span>
|
||||
</div>
|
||||
|
||||
44
src/main/twirl/settings/danger.scala.html
Normal file
@@ -0,0 +1,44 @@
|
||||
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Danger Zone", Some(repository)){
|
||||
@html.header("settings", repository)
|
||||
@menu("danger", repository){
|
||||
<div class="box">
|
||||
<div class="box-header">Danger Zone</div>
|
||||
<div class="box-content">
|
||||
<form id="transfer-form" method="post" action="@url(repository)/settings/transfer" validate="true" autocomplete="off">
|
||||
<fieldset>
|
||||
<label class="strong">Transfer Ownership</label>
|
||||
<div>
|
||||
Transfer this repo to another user or to group.
|
||||
<div class="pull-right">
|
||||
@helper.html.account("newOwner", 150)
|
||||
<input type="submit" class="btn btn-danger" value="Transfer"/>
|
||||
<div>
|
||||
<span id="error-newOwner" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<form id="delete-form" method="post" action="@url(repository)/settings/delete">
|
||||
<fieldset class="margin">
|
||||
<label class="strong">Delete repository</label>
|
||||
<div>
|
||||
Once you delete a repository, there is no going back.
|
||||
<input type="submit" class="btn btn-danger pull-right" value="Delete this repository"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#delete-form').submit(function(){
|
||||
return confirm('Once you delete a repository, there is no going back.\nAre you sure?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Delete Repository", Some(repository)){
|
||||
@html.header("settings", repository)
|
||||
@menu("delete", repository){
|
||||
<form id="form" method="post" action="@url(repository)/settings/delete">
|
||||
<h3>Delete repository</h3>
|
||||
<p>
|
||||
Once you delete a repository, there is no going back.
|
||||
</p>
|
||||
<input type="submit" class="btn btn-danger" value="Delete this repository"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#form').submit(function(){
|
||||
return confirm('Once you delete a repository, there is no going back.\nAre you sure?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -14,8 +14,8 @@
|
||||
<li@if(active=="hooks"){ class="active"}>
|
||||
<a href="@url(repository)/settings/hooks">Service Hooks</a>
|
||||
</li>
|
||||
<li@if(active=="delete"){ class="active"}>
|
||||
<a href="@url(repository)/settings/delete">Delete Repository</a>
|
||||
<li@if(active=="danger"){ class="active"}>
|
||||
<a href="@url(repository)/settings/danger">Danger Zone</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
<div class="box-header">Settings</div>
|
||||
<div class="box-content">
|
||||
<fieldset>
|
||||
<label for="repositoryName" class="strong">Repository Name:</label>
|
||||
<input type="text" name="repositoryName" id="repositoryName" value="@repository.name"/>
|
||||
<span id="error-repositoryName" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset class="margin">
|
||||
<label for="description" class="strong">Description:</label>
|
||||
<input type="text" name="description" id="description" style="width: 600px;" value="@repository.repository.description"/>
|
||||
</fieldset>
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"${if(pageName == "") "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.main(s"${if(pageName.isEmpty) "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.header("wiki", repository)
|
||||
@tab("", repository)
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName == ""){New Page} else {@pageName}</h1>
|
||||
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<div class="btn-group">
|
||||
@if(pageName != ""){
|
||||
@if(page.isDefined){
|
||||
<a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
|
||||
<a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a>
|
||||
<a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<form action="@url(repository)/wiki/@if(pageName == ""){_new} else {_edit}" method="POST" validate="true">
|
||||
<form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true">
|
||||
<span id="error-pageName" class="error"></span>
|
||||
<input type="text" name="pageName" value="@pageName" style="width: 900px; font-weight: bold;" placeholder="Input a page name."/>
|
||||
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 900px; height: 400px;", "")
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
@commits.map { commit =>
|
||||
<tr>
|
||||
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
|
||||
<td>@avatar(commit.committer, 20) <a href="@url(commit.committer)">@commit.committer</a></td>
|
||||
<td>@avatar(commit, 20) @user(commit.committer, commit.mailAddress)</td>
|
||||
<td width="80%">
|
||||
<span class="muted">@datetime(commit.time):</span> @commit.shortMessage
|
||||
</td>
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
<listener-class>servlet.SessionCleanupListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Automatic migration -->
|
||||
<!-- ===================================================================== -->
|
||||
<listener>
|
||||
<listener-class>servlet.AutoUpdateListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Scalatra configuration -->
|
||||
<!-- ===================================================================== -->
|
||||
@@ -18,6 +25,9 @@
|
||||
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- HTTP interface for Git repositories -->
|
||||
<!-- ===================================================================== -->
|
||||
<servlet>
|
||||
<servlet-name>GitRepositoryServlet</servlet-name>
|
||||
<servlet-class>servlet.GitRepositoryServlet</servlet-class>
|
||||
@@ -27,34 +37,10 @@
|
||||
<servlet-name>GitRepositoryServlet</servlet-name>
|
||||
<url-pattern>/git/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<filter>
|
||||
<filter-name>TransactionFilter</filter-name>
|
||||
<filter-class>servlet.TransactionFilter</filter-class>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>TransactionFilter</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<filter>
|
||||
<filter-name>BasicAuthenticationFilter</filter-name>
|
||||
<filter-class>servlet.BasicAuthenticationFilter</filter-class>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>BasicAuthenticationFilter</filter-name>
|
||||
<url-pattern>/git/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- H2 database configuration -->
|
||||
<!-- ===================================================================== -->
|
||||
<listener>
|
||||
<listener-class>servlet.AutoUpdateListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<context-param>
|
||||
<param-name>db.user</param-name>
|
||||
<param-value>sa</param-value>
|
||||
@@ -101,4 +87,14 @@
|
||||
<session-timeout>1440</session-timeout>
|
||||
</session-config>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Optional configurations -->
|
||||
<!-- ===================================================================== -->
|
||||
<!--
|
||||
<context-param>
|
||||
<param-name>gitbucket.home</param-name>
|
||||
<param-value>PATH_TO_DATADIR</param-value>
|
||||
</context-param>
|
||||
-->
|
||||
|
||||
</web-app>
|
||||
@@ -480,6 +480,10 @@ pre.blob {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
li.highlight {
|
||||
background-color: #ffb;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* Issues */
|
||||
/****************************************************************************/
|
||||
@@ -706,6 +710,14 @@ ul.collaborator a.remove {
|
||||
/****************************************************************************/
|
||||
/* Markdown */
|
||||
/****************************************************************************/
|
||||
div.markdown-body h1 {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
div.markdown-body h2 {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
div.markdown-body table {
|
||||
/*width: 100%;*/
|
||||
margin-bottom: 20px;
|
||||
@@ -838,3 +850,21 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
-moz-border-radius-topright: 4px;
|
||||
}
|
||||
|
||||
.markdown-head {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a.markdown-anchor-link {
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
width: 32px;
|
||||
height: 16px;
|
||||
background-image: url(../images/link.png);
|
||||
background-repeat: no-repeat;
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1 a.markdown-anchor-link, h2 a.markdown-anchor-link, h3 a.markdown-anchor-link {
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
BIN
src/main/webapp/assets/common/images/link.png
Normal file
|
After Width: | Height: | Size: 343 B |
BIN
src/main/webapp/assets/common/images/repo_fork.png
Normal file
|
After Width: | Height: | Size: 285 B |
BIN
src/main/webapp/assets/common/images/repo_fork_lg.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
src/main/webapp/assets/common/images/repo_private.png
Normal file
|
After Width: | Height: | Size: 280 B |
BIN
src/main/webapp/assets/common/images/repo_private_lg.png
Normal file
|
After Width: | Height: | Size: 420 B |
BIN
src/main/webapp/assets/common/images/repo_public.png
Normal file
|
After Width: | Height: | Size: 247 B |
BIN
src/main/webapp/assets/common/images/repo_public_lg.png
Normal file
|
After Width: | Height: | Size: 352 B |
@@ -11,6 +11,26 @@ $(function(){
|
||||
$('img[data-toggle=tooltip]').tooltip();
|
||||
$('a[data-toggle=tooltip]').tooltip();
|
||||
|
||||
// anchor icon for markdown
|
||||
$('.markdown-head').mouseenter(function(e){
|
||||
$(e.target).children('a.markdown-anchor-link').show();
|
||||
});
|
||||
$('.markdown-head').mouseleave(function(e){
|
||||
var anchorLink = $(e.target).children('a.markdown-anchor-link');
|
||||
if(anchorLink.data('active') != true){
|
||||
anchorLink.hide();
|
||||
}
|
||||
});
|
||||
|
||||
$('a.markdown-anchor-link').mouseenter(function(e){
|
||||
$(e.target).data('active', true);
|
||||
});
|
||||
|
||||
$('a.markdown-anchor-link').mouseleave(function(e){
|
||||
$(e.target).data('active', false);
|
||||
$(e.target).hide();
|
||||
});
|
||||
|
||||
// syntax highlighting by google-code-prettify
|
||||
prettyPrint();
|
||||
});
|
||||
|
||||
390
src/main/webapp/assets/common/js/jquery.ba-hashchange.js
Normal file
@@ -0,0 +1,390 @@
|
||||
/*!
|
||||
* jQuery hashchange event - v1.3 - 7/21/2010
|
||||
* http://benalman.com/projects/jquery-hashchange-plugin/
|
||||
*
|
||||
* Copyright (c) 2010 "Cowboy" Ben Alman
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
* http://benalman.com/about/license/
|
||||
*/
|
||||
|
||||
// Script: jQuery hashchange event
|
||||
//
|
||||
// *Version: 1.3, Last updated: 7/21/2010*
|
||||
//
|
||||
// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
|
||||
// GitHub - http://github.com/cowboy/jquery-hashchange/
|
||||
// Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
|
||||
// (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped)
|
||||
//
|
||||
// About: License
|
||||
//
|
||||
// Copyright (c) 2010 "Cowboy" Ben Alman,
|
||||
// Dual licensed under the MIT and GPL licenses.
|
||||
// http://benalman.com/about/license/
|
||||
//
|
||||
// About: Examples
|
||||
//
|
||||
// These working examples, complete with fully commented code, illustrate a few
|
||||
// ways in which this plugin can be used.
|
||||
//
|
||||
// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
|
||||
// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/
|
||||
//
|
||||
// About: Support and Testing
|
||||
//
|
||||
// Information about what version or versions of jQuery this plugin has been
|
||||
// tested with, what browsers it has been tested in, and where the unit tests
|
||||
// reside (so you can test it yourself).
|
||||
//
|
||||
// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
|
||||
// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
|
||||
// Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
|
||||
// Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/
|
||||
//
|
||||
// About: Known issues
|
||||
//
|
||||
// While this jQuery hashchange event implementation is quite stable and
|
||||
// robust, there are a few unfortunate browser bugs surrounding expected
|
||||
// hashchange event-based behaviors, independent of any JavaScript
|
||||
// window.onhashchange abstraction. See the following examples for more
|
||||
// information:
|
||||
//
|
||||
// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
|
||||
// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
|
||||
// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
|
||||
// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
|
||||
//
|
||||
// Also note that should a browser natively support the window.onhashchange
|
||||
// event, but not report that it does, the fallback polling loop will be used.
|
||||
//
|
||||
// About: Release History
|
||||
//
|
||||
// 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
|
||||
// "removable" for mobile-only development. Added IE6/7 document.title
|
||||
// support. Attempted to make Iframe as hidden as possible by using
|
||||
// techniques from http://www.paciellogroup.com/blog/?p=604. Added
|
||||
// support for the "shortcut" format $(window).hashchange( fn ) and
|
||||
// $(window).hashchange() like jQuery provides for built-in events.
|
||||
// Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
|
||||
// lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
|
||||
// and <jQuery.fn.hashchange.src> properties plus document-domain.html
|
||||
// file to address access denied issues when setting document.domain in
|
||||
// IE6/7.
|
||||
// 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin
|
||||
// from a page on another domain would cause an error in Safari 4. Also,
|
||||
// IE6/7 Iframe is now inserted after the body (this actually works),
|
||||
// which prevents the page from scrolling when the event is first bound.
|
||||
// Event can also now be bound before DOM ready, but it won't be usable
|
||||
// before then in IE6/7.
|
||||
// 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
|
||||
// where browser version is incorrectly reported as 8.0, despite
|
||||
// inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
|
||||
// 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
|
||||
// window.onhashchange functionality into a separate plugin for users
|
||||
// who want just the basic event & back button support, without all the
|
||||
// extra awesomeness that BBQ provides. This plugin will be included as
|
||||
// part of jQuery BBQ, but also be available separately.
|
||||
|
||||
(function($,window,undefined){
|
||||
'$:nomunge'; // Used by YUI compressor.
|
||||
|
||||
// Reused string.
|
||||
var str_hashchange = 'hashchange',
|
||||
|
||||
// Method / object references.
|
||||
doc = document,
|
||||
fake_onhashchange,
|
||||
special = $.event.special,
|
||||
|
||||
// Does the browser support window.onhashchange? Note that IE8 running in
|
||||
// IE7 compatibility mode reports true for 'onhashchange' in window, even
|
||||
// though the event isn't supported, so also test document.documentMode.
|
||||
doc_mode = doc.documentMode,
|
||||
supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 );
|
||||
|
||||
// Get location.hash (or what you'd expect location.hash to be) sans any
|
||||
// leading #. Thanks for making this necessary, Firefox!
|
||||
function get_fragment( url ) {
|
||||
url = url || location.href;
|
||||
return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' );
|
||||
};
|
||||
|
||||
// Method: jQuery.fn.hashchange
|
||||
//
|
||||
// Bind a handler to the window.onhashchange event or trigger all bound
|
||||
// window.onhashchange event handlers. This behavior is consistent with
|
||||
// jQuery's built-in event handlers.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// > jQuery(window).hashchange( [ handler ] );
|
||||
//
|
||||
// Arguments:
|
||||
//
|
||||
// handler - (Function) Optional handler to be bound to the hashchange
|
||||
// event. This is a "shortcut" for the more verbose form:
|
||||
// jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
|
||||
// all bound window.onhashchange event handlers will be triggered. This
|
||||
// is a shortcut for the more verbose
|
||||
// jQuery(window).trigger( 'hashchange' ). These forms are described in
|
||||
// the <hashchange event> section.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// (jQuery) The initial jQuery collection of elements.
|
||||
|
||||
// Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
|
||||
// $(elem).hashchange() for triggering, like jQuery does for built-in events.
|
||||
$.fn[ str_hashchange ] = function( fn ) {
|
||||
return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange );
|
||||
};
|
||||
|
||||
// Property: jQuery.fn.hashchange.delay
|
||||
//
|
||||
// The numeric interval (in milliseconds) at which the <hashchange event>
|
||||
// polling loop executes. Defaults to 50.
|
||||
|
||||
// Property: jQuery.fn.hashchange.domain
|
||||
//
|
||||
// If you're setting document.domain in your JavaScript, and you want hash
|
||||
// history to work in IE6/7, not only must this property be set, but you must
|
||||
// also set document.domain BEFORE jQuery is loaded into the page. This
|
||||
// property is only applicable if you are supporting IE6/7 (or IE8 operating
|
||||
// in "IE7 compatibility" mode).
|
||||
//
|
||||
// In addition, the <jQuery.fn.hashchange.src> property must be set to the
|
||||
// path of the included "document-domain.html" file, which can be renamed or
|
||||
// modified if necessary (note that the document.domain specified must be the
|
||||
// same in both your main JavaScript as well as in this file).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// jQuery.fn.hashchange.domain = document.domain;
|
||||
|
||||
// Property: jQuery.fn.hashchange.src
|
||||
//
|
||||
// If, for some reason, you need to specify an Iframe src file (for example,
|
||||
// when setting document.domain as in <jQuery.fn.hashchange.domain>), you can
|
||||
// do so using this property. Note that when using this property, history
|
||||
// won't be recorded in IE6/7 until the Iframe src file loads. This property
|
||||
// is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7
|
||||
// compatibility" mode).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// jQuery.fn.hashchange.src = 'path/to/file.html';
|
||||
|
||||
$.fn[ str_hashchange ].delay = 50;
|
||||
/*
|
||||
$.fn[ str_hashchange ].domain = null;
|
||||
$.fn[ str_hashchange ].src = null;
|
||||
*/
|
||||
|
||||
// Event: hashchange event
|
||||
//
|
||||
// Fired when location.hash changes. In browsers that support it, the native
|
||||
// HTML5 window.onhashchange event is used, otherwise a polling loop is
|
||||
// initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
|
||||
// see if the hash has changed. In IE6/7 (and IE8 operating in "IE7
|
||||
// compatibility" mode), a hidden Iframe is created to allow the back button
|
||||
// and hash-based history to work.
|
||||
//
|
||||
// Usage as described in <jQuery.fn.hashchange>:
|
||||
//
|
||||
// > // Bind an event handler.
|
||||
// > jQuery(window).hashchange( function(e) {
|
||||
// > var hash = location.hash;
|
||||
// > ...
|
||||
// > });
|
||||
// >
|
||||
// > // Manually trigger the event handler.
|
||||
// > jQuery(window).hashchange();
|
||||
//
|
||||
// A more verbose usage that allows for event namespacing:
|
||||
//
|
||||
// > // Bind an event handler.
|
||||
// > jQuery(window).bind( 'hashchange', function(e) {
|
||||
// > var hash = location.hash;
|
||||
// > ...
|
||||
// > });
|
||||
// >
|
||||
// > // Manually trigger the event handler.
|
||||
// > jQuery(window).trigger( 'hashchange' );
|
||||
//
|
||||
// Additional Notes:
|
||||
//
|
||||
// * The polling loop and Iframe are not created until at least one handler
|
||||
// is actually bound to the 'hashchange' event.
|
||||
// * If you need the bound handler(s) to execute immediately, in cases where
|
||||
// a location.hash exists on page load, via bookmark or page refresh for
|
||||
// example, use jQuery(window).hashchange() or the more verbose
|
||||
// jQuery(window).trigger( 'hashchange' ).
|
||||
// * The event can be bound before DOM ready, but since it won't be usable
|
||||
// before then in IE6/7 (due to the necessary Iframe), recommended usage is
|
||||
// to bind it inside a DOM ready handler.
|
||||
|
||||
// Override existing $.event.special.hashchange methods (allowing this plugin
|
||||
// to be defined after jQuery BBQ in BBQ's source code).
|
||||
special[ str_hashchange ] = $.extend( special[ str_hashchange ], {
|
||||
|
||||
// Called only when the first 'hashchange' event is bound to window.
|
||||
setup: function() {
|
||||
// If window.onhashchange is supported natively, there's nothing to do..
|
||||
if ( supports_onhashchange ) { return false; }
|
||||
|
||||
// Otherwise, we need to create our own. And we don't want to call this
|
||||
// until the user binds to the event, just in case they never do, since it
|
||||
// will create a polling loop and possibly even a hidden Iframe.
|
||||
$( fake_onhashchange.start );
|
||||
},
|
||||
|
||||
// Called only when the last 'hashchange' event is unbound from window.
|
||||
teardown: function() {
|
||||
// If window.onhashchange is supported natively, there's nothing to do..
|
||||
if ( supports_onhashchange ) { return false; }
|
||||
|
||||
// Otherwise, we need to stop ours (if possible).
|
||||
$( fake_onhashchange.stop );
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// fake_onhashchange does all the work of triggering the window.onhashchange
|
||||
// event for browsers that don't natively support it, including creating a
|
||||
// polling loop to watch for hash changes and in IE 6/7 creating a hidden
|
||||
// Iframe to enable back and forward.
|
||||
fake_onhashchange = (function(){
|
||||
var self = {},
|
||||
timeout_id,
|
||||
|
||||
// Remember the initial hash so it doesn't get triggered immediately.
|
||||
last_hash = get_fragment(),
|
||||
|
||||
fn_retval = function(val){ return val; },
|
||||
history_set = fn_retval,
|
||||
history_get = fn_retval;
|
||||
|
||||
// Start the polling loop.
|
||||
self.start = function() {
|
||||
timeout_id || poll();
|
||||
};
|
||||
|
||||
// Stop the polling loop.
|
||||
self.stop = function() {
|
||||
timeout_id && clearTimeout( timeout_id );
|
||||
timeout_id = undefined;
|
||||
};
|
||||
|
||||
// This polling loop checks every $.fn.hashchange.delay milliseconds to see
|
||||
// if location.hash has changed, and triggers the 'hashchange' event on
|
||||
// window when necessary.
|
||||
function poll() {
|
||||
var hash = get_fragment(),
|
||||
history_hash = history_get( last_hash );
|
||||
|
||||
if ( hash !== last_hash ) {
|
||||
history_set( last_hash = hash, history_hash );
|
||||
|
||||
$(window).trigger( str_hashchange );
|
||||
|
||||
} else if ( history_hash !== last_hash ) {
|
||||
location.href = location.href.replace( /#.*/, '' ) + history_hash;
|
||||
}
|
||||
|
||||
timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
|
||||
};
|
||||
|
||||
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
|
||||
// vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv
|
||||
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
|
||||
$.browser.msie && !supports_onhashchange && (function(){
|
||||
// Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8
|
||||
// when running in "IE7 compatibility" mode.
|
||||
|
||||
var iframe,
|
||||
iframe_src;
|
||||
|
||||
// When the event is bound and polling starts in IE 6/7, create a hidden
|
||||
// Iframe for history handling.
|
||||
self.start = function(){
|
||||
if ( !iframe ) {
|
||||
iframe_src = $.fn[ str_hashchange ].src;
|
||||
iframe_src = iframe_src && iframe_src + get_fragment();
|
||||
|
||||
// Create hidden Iframe. Attempt to make Iframe as hidden as possible
|
||||
// by using techniques from http://www.paciellogroup.com/blog/?p=604.
|
||||
iframe = $('<iframe tabindex="-1" title="empty"/>').hide()
|
||||
|
||||
// When Iframe has completely loaded, initialize the history and
|
||||
// start polling.
|
||||
.one( 'load', function(){
|
||||
iframe_src || history_set( get_fragment() );
|
||||
poll();
|
||||
})
|
||||
|
||||
// Load Iframe src if specified, otherwise nothing.
|
||||
.attr( 'src', iframe_src || 'javascript:0' )
|
||||
|
||||
// Append Iframe after the end of the body to prevent unnecessary
|
||||
// initial page scrolling (yes, this works).
|
||||
.insertAfter( 'body' )[0].contentWindow;
|
||||
|
||||
// Whenever `document.title` changes, update the Iframe's title to
|
||||
// prettify the back/next history menu entries. Since IE sometimes
|
||||
// errors with "Unspecified error" the very first time this is set
|
||||
// (yes, very useful) wrap this with a try/catch block.
|
||||
doc.onpropertychange = function(){
|
||||
try {
|
||||
if ( event.propertyName === 'title' ) {
|
||||
iframe.document.title = doc.title;
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Override the "stop" method since an IE6/7 Iframe was created. Even
|
||||
// if there are no longer any bound event handlers, the polling loop
|
||||
// is still necessary for back/next to work at all!
|
||||
self.stop = fn_retval;
|
||||
|
||||
// Get history by looking at the hidden Iframe's location.hash.
|
||||
history_get = function() {
|
||||
return get_fragment( iframe.location.href );
|
||||
};
|
||||
|
||||
// Set a new history item by opening and then closing the Iframe
|
||||
// document, *then* setting its location.hash. If document.domain has
|
||||
// been set, update that as well.
|
||||
history_set = function( hash, history_hash ) {
|
||||
var iframe_doc = iframe.document,
|
||||
domain = $.fn[ str_hashchange ].domain;
|
||||
|
||||
if ( hash !== history_hash ) {
|
||||
// Update Iframe with any initial `document.title` that might be set.
|
||||
iframe_doc.title = doc.title;
|
||||
|
||||
// Opening the Iframe's document after it has been closed is what
|
||||
// actually adds a history entry.
|
||||
iframe_doc.open();
|
||||
|
||||
// Set document.domain for the Iframe document as well, if necessary.
|
||||
domain && iframe_doc.write( '<script>document.domain="' + domain + '"</script>' );
|
||||
|
||||
iframe_doc.close();
|
||||
|
||||
// Update the Iframe's hash, for great justice.
|
||||
iframe.location.hash = hash;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
return self;
|
||||
})();
|
||||
|
||||
})(jQuery,this);
|
||||
@@ -1,39 +0,0 @@
|
||||
$(function(){
|
||||
$.each($('form[validate=true]'), function(i, form){
|
||||
$(form).submit(validate);
|
||||
});
|
||||
$.each($('input[formaction]'), function(i, input){
|
||||
$(input).click(function(){
|
||||
var form = $(input).parents('form')
|
||||
$(form).attr('action', $(input).attr('formaction'))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function validate(e){
|
||||
var form = $(e.target);
|
||||
|
||||
if(form.data('validated') == true){
|
||||
return true;
|
||||
}
|
||||
|
||||
$.post(form.attr('action') + '/validate', $(e.target).serialize(), function(data){
|
||||
// clear all error messages
|
||||
$('.error').text('');
|
||||
|
||||
if($.isEmptyObject(data)){
|
||||
form.data('validated', true);
|
||||
form.submit();
|
||||
} else {
|
||||
form.data('validated', false);
|
||||
displayErrors(data);
|
||||
}
|
||||
}, 'json');
|
||||
return false;
|
||||
}
|
||||
|
||||
function displayErrors(data){
|
||||
$.each(data, function(key, value){
|
||||
$('#error-' + key.split('.').join('_')).text(value);
|
||||
});
|
||||
}
|
||||
@@ -12,7 +12,7 @@ q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?
|
||||
s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,
|
||||
q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d=
|
||||
c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i<c.length;++i)b(c[i]);d===(d|0)&&c[0].setAttribute("value",d);var r=j.createElement("ol");
|
||||
r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.className="L"+(i+d)%10,k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a;
|
||||
r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.id="L"+(i+1),k.className="L"+(i+d)%10,k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a;
|
||||
a.a=b;a.d=g.d;a.e=0;I(d,b)(a);var s=/\bMSIE\s(\d+)/.exec(navigator.userAgent),s=s&&+s[1]<=8,d=/\n/g,x=a.a,m=x.length,g=0,j=a.d,k=j.length,b=0,c=a.g,i=c.length,r=0;c[i]=m;var n,e;for(e=n=0;e<i;)c[e]!==c[e+2]?(c[n++]=c[e++],c[n++]=c[e++]):e+=2;i=n;for(e=n=0;e<i;){for(var p=c[e],w=c[e+1],t=e+2;t+2<=i&&c[t+1]===w;)t+=2;c[n++]=p;c[n++]=w;e=t}c.length=n;var f=a.c,h;if(f)h=f.style.display,f.style.display="none";try{for(;b<k;){var l=j[b+2]||m,B=c[r+2]||m,t=Math.min(l,B),A=j[b+1],G;if(A.nodeType!==1&&(G=x.substring(g,
|
||||
t))){s&&(G=G.replace(d,"\r"));A.nodeValue=G;var L=A.ownerDocument,o=L.createElement("span");o.className=c[r+1];var v=A.parentNode;v.replaceChild(o,A);o.appendChild(A);g<l&&(j[b+1]=A=L.createTextNode(x.substring(t,l)),v.insertBefore(A,o.nextSibling))}g=t;g>=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
|
||||
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],
|
||||
|
||||
@@ -66,7 +66,7 @@ table.diff .replace {
|
||||
background-color:#FD8
|
||||
}
|
||||
table.diff .delete {
|
||||
background-color:#E99;
|
||||
background-color:#FFDDDD;
|
||||
}
|
||||
table.diff .skip {
|
||||
background-color:#EFEFEF;
|
||||
@@ -74,10 +74,10 @@ table.diff .skip {
|
||||
border-right:1px solid #BBC;
|
||||
}
|
||||
table.diff .insert {
|
||||
background-color:#9E9
|
||||
background-color:#DDFFDD
|
||||
}
|
||||
table.diff th.author {
|
||||
text-align:right;
|
||||
border-top:1px solid #BBC;
|
||||
background:#EFEFEF
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
|
||||
val RootMailAddress = "root@localhost"
|
||||
|
||||
"getAllUsers" in { withTestDB{
|
||||
AccountService.getAllUsers must be like{
|
||||
case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false)) => ok
|
||||
AccountService.getAllUsers() must be like{
|
||||
case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -75,3 +75,4 @@ class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ import java.io.File
|
||||
trait ServiceSpecBase {
|
||||
|
||||
def withTestDB[A](action: => A): A = {
|
||||
util.FileUtil.withTmpDir(new File(FileUtils.getTempDirectory(), Random.alphanumeric.take(10).mkString)){
|
||||
dir =>
|
||||
util.FileUtil.withTmpDir(new File(FileUtils.getTempDirectory(), Random.alphanumeric.take(10).mkString)){ dir =>
|
||||
val (url, user, pass) = (s"jdbc:h2:${dir}", "sa", "sa")
|
||||
org.h2.Driver.load()
|
||||
using(DriverManager.getConnection(url, user, pass)){ conn =>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
package util
|
||||
|
||||
import org.specs2.mutable._
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
class ValidationsSpec extends Specification with Validations {
|
||||
|
||||
"identifier" should {
|
||||
"validate id string " in {
|
||||
identifier.validate("id", "aa_ZZ-00.01") mustEqual None
|
||||
identifier.validate("id", "_aaaa") mustEqual Some("id starts with invalid character.")
|
||||
identifier.validate("id", "-aaaa") mustEqual Some("id starts with invalid character.")
|
||||
identifier.validate("id", "aa_ZZ#01") mustEqual Some("id contains invalid character.")
|
||||
identifier.validate("id", "aa_ZZ-00.01", null) mustEqual None
|
||||
identifier.validate("id", "_aaaa", null) mustEqual Some("id starts with invalid character.")
|
||||
identifier.validate("id", "-aaaa", null) mustEqual Some("id starts with invalid character.")
|
||||
identifier.validate("id", "aa_ZZ#01", null) mustEqual Some("id contains invalid character.")
|
||||
}
|
||||
}
|
||||
|
||||
"color" should {
|
||||
"validate color string " in {
|
||||
color.validate("color", "#88aaff") mustEqual None
|
||||
color.validate("color", "#gghhii") mustEqual Some("color must be '#[0-9a-fA-F]{6}'.")
|
||||
val messages = Messages()
|
||||
color.validate("color", "#88aaff", messages) mustEqual None
|
||||
color.validate("color", "#gghhii", messages) mustEqual Some("color must be '#[0-9a-fA-F]{6}'.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +28,7 @@ class ValidationsSpec extends Specification with Validations {
|
||||
// date().validate("date", "2013-10-5" , Map[String, String]()) mustEqual List(("date", "date must be '\\d{4}-\\d{2}-\\d{2}'."))
|
||||
// }
|
||||
"convert date string " in {
|
||||
val result = date().convert("2013-10-05")
|
||||
val result = date().convert("2013-10-05", null)
|
||||
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(result) mustEqual "2013-10-05 00:00:00"
|
||||
}
|
||||
}
|
||||
|
||||