mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 23:27:43 +02:00
Compare commits
364 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6e169ab3c2 | ||
|
|
6ac27e89b3 | ||
|
|
2235dab550 | ||
|
|
7604c2172f | ||
|
|
1e750f4b9d | ||
|
|
d1f0d01ae8 | ||
|
|
167a0f28b2 | ||
|
|
06be5266fd | ||
|
|
60e7165983 | ||
|
|
6dbfc12896 | ||
|
|
6d4b3e54d0 | ||
|
|
2968b92677 | ||
|
|
0d0bf4ad3f | ||
|
|
53fa60b0f8 | ||
|
|
99517fa508 | ||
|
|
2e239d16d4 | ||
|
|
6de5babd5b | ||
|
|
f3ad1a019d | ||
|
|
90ab882e8e | ||
|
|
53269096a6 | ||
|
|
254509f243 | ||
|
|
a697f186af | ||
|
|
2316a80be9 | ||
|
|
bbcb04b263 | ||
|
|
7afe7fbb5f | ||
|
|
7c7da7379d | ||
|
|
37358e9c8c | ||
|
|
41941df87a | ||
|
|
bf2ed81eb1 | ||
|
|
2d85d41e9c | ||
|
|
e5e7b2484c | ||
|
|
6058552654 | ||
|
|
f40c7ff4fa | ||
|
|
da62c6181e | ||
|
|
4d066738eb | ||
|
|
cb12d03262 | ||
|
|
9a6a2d9b78 | ||
|
|
ff0af477cb | ||
|
|
05adf9345f | ||
|
|
ba70fdda48 | ||
|
|
3885fcb2ec | ||
|
|
99800a27f5 | ||
|
|
107622942b | ||
|
|
9794f14a65 | ||
|
|
af759a815f | ||
|
|
0e7078c479 | ||
|
|
83107c7974 | ||
|
|
ff9b2dbe93 | ||
|
|
ebf4e5f2e9 | ||
|
|
21c30583e5 | ||
|
|
d6c9ace306 | ||
|
|
faf1252597 | ||
|
|
7b2ee25ea2 | ||
|
|
5a3207ae42 | ||
|
|
3eab4955b9 | ||
|
|
d772fc3ba2 | ||
|
|
7de0a3fd70 | ||
|
|
eb8710a336 | ||
|
|
25c55ecbd0 | ||
|
|
280df2cedd | ||
|
|
5ba9c86bee | ||
|
|
faa6591d27 | ||
|
|
841d442f0d | ||
|
|
3351eabc4f | ||
|
|
006e1bc61e | ||
|
|
35d1b4ea37 | ||
|
|
b0c5069695 | ||
|
|
dae0d0ad4b | ||
|
|
79e560b7bf | ||
|
|
cf79ac1069 | ||
|
|
8aab7a16c4 | ||
|
|
c16b89b0be | ||
|
|
25bbc00ff3 | ||
|
|
e667b6c139 | ||
|
|
195364223f | ||
|
|
84ce2cac8d | ||
|
|
f3507cf465 | ||
|
|
f74f2c47d3 | ||
|
|
72b25591a5 | ||
|
|
fe23a5c6da | ||
|
|
49fbc5cb62 | ||
|
|
5a19a307a9 | ||
|
|
c3ec52b391 | ||
|
|
f2d68be0a3 | ||
|
|
c1f98ac481 | ||
|
|
8287c84dc7 | ||
|
|
13bff2963e | ||
|
|
035f3f9e02 | ||
|
|
65e6de5ba4 | ||
|
|
82ced9233a | ||
|
|
e94411ebeb | ||
|
|
b92b429ffa | ||
|
|
e457cfb212 | ||
|
|
f1476c52e6 | ||
|
|
332246aed6 | ||
|
|
1c5201dcf1 | ||
|
|
36880ace27 | ||
|
|
0d55d6ef6b | ||
|
|
688bf645b4 | ||
|
|
d5a14482a6 | ||
|
|
cc1e0030df | ||
|
|
fcadcb34a2 | ||
|
|
dd8f440be0 | ||
|
|
17bc422e7a | ||
|
|
380cdbcf75 | ||
|
|
f4f2bf34fc | ||
|
|
ed713d80a9 | ||
|
|
c39703c61c | ||
|
|
537773f975 | ||
|
|
f37eca7c61 | ||
|
|
40a52d5ad5 | ||
|
|
d95bd20cbe | ||
|
|
70ca98d6a2 | ||
|
|
cf7caf55da | ||
|
|
b74bff3b2e | ||
|
|
b2e4853976 | ||
|
|
aef3c5c121 | ||
|
|
4afbfcb016 | ||
|
|
09f8cff4c9 | ||
|
|
9ecd162040 | ||
|
|
8617f02b01 | ||
|
|
9d71d39917 | ||
|
|
5430564065 | ||
|
|
54bc8c16d8 | ||
|
|
9c14ddda18 | ||
|
|
0affdb6ad0 | ||
|
|
532978522a | ||
|
|
05a9a0b45c | ||
|
|
24f8ad11ad | ||
|
|
ce943a0e6c | ||
|
|
204c0cd0f8 | ||
|
|
c213008f1c | ||
|
|
e6ad069509 | ||
|
|
38c7e3cdf8 | ||
|
|
2be79f6590 | ||
|
|
2f7125b6c0 | ||
|
|
bb03a6fc9b | ||
|
|
7b774aee1a | ||
|
|
d53619c247 | ||
|
|
d34118bdfd | ||
|
|
c57bc487a3 | ||
|
|
296fc9a3df | ||
|
|
fd8b5780f3 | ||
|
|
602b6c635a | ||
|
|
a79180699e | ||
|
|
e9901a8abf | ||
|
|
4e63d64c13 | ||
|
|
4261b7adbe | ||
|
|
f30c9f6171 | ||
|
|
c00b704843 | ||
|
|
e89b2020a3 | ||
|
|
18ca3cbd80 | ||
|
|
062d6cd066 | ||
|
|
b4dd067d61 | ||
|
|
fd22e2911a | ||
|
|
73d9e69e43 | ||
|
|
7e4c29f4cf | ||
|
|
32672262ef | ||
|
|
3c865ea20b | ||
|
|
d8698d02b7 | ||
|
|
d5b47e5adb | ||
|
|
accb1cf2ab | ||
|
|
aa8da1b046 | ||
|
|
c52ed32949 | ||
|
|
ec6f4ff734 | ||
|
|
06b0dbf2e5 | ||
|
|
98d24248c2 | ||
|
|
cec1dc98a9 | ||
|
|
36115734bb | ||
|
|
c1eccd391d | ||
|
|
7fe86fcdb2 | ||
|
|
7f81ec52c1 | ||
|
|
7c269de39b | ||
|
|
aa9e34e992 | ||
|
|
4d0ab514fb | ||
|
|
9d526b32e0 | ||
|
|
90a83c5c64 | ||
|
|
e6e5cc67d5 | ||
|
|
4a6eb95474 | ||
|
|
7bce8cf3b6 | ||
|
|
4d1605ded2 | ||
|
|
2bec2cfa93 | ||
|
|
ff07872a3d | ||
|
|
35733cd82e | ||
|
|
38df990033 | ||
|
|
940e2f4759 | ||
|
|
6fe65c76b1 | ||
|
|
c0713eaeda | ||
|
|
000afa1ed6 | ||
|
|
828688ddd0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ project/plugins/project/
|
|||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
.cache
|
.cache
|
||||||
|
.settings
|
||||||
|
|
||||||
# IntelliJ specific
|
# IntelliJ specific
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
147
README.md
147
README.md
@@ -6,7 +6,7 @@ GitBucket is the easily installable Github clone written with Scala.
|
|||||||
The current version of GitBucket provides a basic features below:
|
The current version of GitBucket provides a basic features below:
|
||||||
|
|
||||||
- Public / Private Git repository (http access only)
|
- Public / Private Git repository (http access only)
|
||||||
- Repository viewer (some advanced features are not implemented)
|
- Repository viewer (some advanced features such as online file editing are not implemented)
|
||||||
- Repository search (Code and Issues)
|
- Repository search (Code and Issues)
|
||||||
- Wiki
|
- Wiki
|
||||||
- Issues
|
- Issues
|
||||||
@@ -15,9 +15,13 @@ The current version of GitBucket provides a basic features below:
|
|||||||
- Activity timeline
|
- Activity timeline
|
||||||
- User management (for Administrators)
|
- User management (for Administrators)
|
||||||
- Group (like Organization in Github)
|
- Group (like Organization in Github)
|
||||||
|
- LDAP integration
|
||||||
|
- Gravatar support
|
||||||
|
|
||||||
Following features are not implemented, but we will make them in the future release!
|
Following features are not implemented, but we will make them in the future release!
|
||||||
|
|
||||||
|
- File editing in repository viewer
|
||||||
|
- Comment for the changeset
|
||||||
- Network graph
|
- Network graph
|
||||||
- Statics
|
- Statics
|
||||||
- Watch / Star
|
- Watch / Star
|
||||||
@@ -28,55 +32,128 @@ Installation
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
|
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
|
||||||
2. Deploy it to the servlet container such as Tomcat or Jetty.
|
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
|
||||||
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
|
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
|
||||||
|
|
||||||
The default administrator account is **root** and password is **root**.
|
The default administrator account is **root** and password is **root**.
|
||||||
|
|
||||||
To upgrade GitBucket, only replace gitbucket.war.
|
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.
|
||||||
|
|
||||||
|
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
|
Release Notes
|
||||||
--------
|
--------
|
||||||
|
### 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 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
|
||||||
|
- Improve ZIP exporting performance
|
||||||
|
- Expand issue and comment textarea for long text automatically
|
||||||
|
- Add conflict detection in Wiki
|
||||||
|
- Add reverting wiki page from history
|
||||||
|
- Match committer to user name by email address
|
||||||
|
- Mail notification sender is customizable
|
||||||
|
- Add link to changeset in refs comment for issues
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.6 - 1 Oct 2013
|
||||||
|
- Web hook
|
||||||
|
- Performance improvement for pull request
|
||||||
|
- Executable war file
|
||||||
|
- Specify suitable Content-Type for downloaded files in the repository viewer
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.5 - 4 Sep 2013
|
### 1.5 - 4 Sep 2013
|
||||||
- Fork and pull request.
|
- Fork and pull request
|
||||||
- LDAP authentication.
|
- LDAP authentication
|
||||||
- Mail notification.
|
- Mail notification
|
||||||
- Add an option to turn off the gravatar support.
|
- Add an option to turn off the gravatar support
|
||||||
- Add the branch tab in the repository viewer.
|
- Add the branch tab in the repository viewer
|
||||||
- Encoding auto detection for the file content in the repository viewer.
|
- Encoding auto detection for the file content in the repository viewer
|
||||||
- Add favicon, header logo and icons for the timeline.
|
- Add favicon, header logo and icons for the timeline
|
||||||
- Specify data directory via environment variable GITBUCKET_HOME.
|
- Specify data directory via environment variable GITBUCKET_HOME
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.4 - 31 Jul 2013
|
### 1.4 - 31 Jul 2013
|
||||||
- Group management.
|
- Group management
|
||||||
- Repository search for code and issues.
|
- Repository search for code and issues
|
||||||
- Display user related issues on the dashboard.
|
- Display user related issues on the dashboard
|
||||||
- Display participants avatar of issues on the issue page.
|
- Display participants avatar of issues on the issue page
|
||||||
- Performance improvement for repository viewer.
|
- Performance improvement for repository viewer
|
||||||
- Alert by milestone due date.
|
- Alert by milestone due date
|
||||||
- H2 database administration console.
|
- H2 database administration console
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.3 - 18 Jul 2013
|
### 1.3 - 18 Jul 2013
|
||||||
- Batch updating for issues.
|
- Batch updating for issues
|
||||||
- Display assigned user on issue list.
|
- Display assigned user on issue list
|
||||||
- User icon and Gravatar support.
|
- User icon and Gravatar support
|
||||||
- Convert @xxxx to link to the account page.
|
- Convert @xxxx to link to the account page
|
||||||
- Add copy to clipboard button for git clone URL.
|
- Add copy to clipboard button for git clone URL
|
||||||
- Allows multi-byte characters as wiki page name.
|
- Allow multi-byte characters as wiki page name
|
||||||
- Allows to create the empty repository.
|
- Allow to create the empty repository
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.2 - 09 Jul 2013
|
### 1.2 - 09 Jul 2013
|
||||||
- Added activity timeline.
|
- Add activity timeline
|
||||||
- Bugfix for Git 1.8.1.5 or later.
|
- Bugfix for Git 1.8.1.5 or later
|
||||||
- Allows multi-byte characters as label.
|
- Allow multi-byte characters as label
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.1 - 05 Jul 2013
|
### 1.1 - 05 Jul 2013
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
- Upgrade to JGit 3.0.
|
- Upgrade to JGit 3.0
|
||||||
|
|
||||||
### 1.0 - 04 Jul 2013
|
### 1.0 - 04 Jul 2013
|
||||||
- This is a first public release.
|
- This is a first public release
|
||||||
|
|||||||
61
build.xml
Normal file
61
build.xml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<project name="gitbucket" default="all" basedir=".">
|
||||||
|
|
||||||
|
<property name="target.dir" value="target"/>
|
||||||
|
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
|
||||||
|
<property name="jetty.dir" value="embed-jetty"/>
|
||||||
|
<property name="scala.version" value="2.10"/>
|
||||||
|
<property name="gitbucket.version" value="0.0.1"/>
|
||||||
|
<property name="jetty.version" value="8.1.8.v20121106"/>
|
||||||
|
<property name="servlet.version" value="3.0.0.v201112011016"/>
|
||||||
|
|
||||||
|
<condition property="sbt.exec" value="sbt.bat" else="sbt.sh">
|
||||||
|
<os family="windows" />
|
||||||
|
</condition>
|
||||||
|
|
||||||
|
<target name="clean">
|
||||||
|
<delete dir="${embed.classes.dir}"/>
|
||||||
|
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="war" depends="clean">
|
||||||
|
<exec executable="${sbt.exec}" resolveexecutable="true" failonerror="true">
|
||||||
|
<arg line="clean compile test package" />
|
||||||
|
</exec>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="embed" depends="war">
|
||||||
|
<mkdir dir="${embed.classes.dir}"/>
|
||||||
|
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/javax.servlet-${servlet.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-continuation-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-http-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-io-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-security-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-server-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-servlet-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-util-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-webapp-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-xml-${jetty.version}.jar" />
|
||||||
|
|
||||||
|
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||||
|
basedir="${embed.classes.dir}"
|
||||||
|
update = "true"
|
||||||
|
includes="javax/**,org/**"/>
|
||||||
|
|
||||||
|
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||||
|
basedir="${target.dir}/scala-${scala.version}/classes"
|
||||||
|
update = "true"
|
||||||
|
includes="JettyLauncher.class,HttpsSupportConnector.class"/>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="rename" depends="embed">
|
||||||
|
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||||
|
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="all" depends="rename">
|
||||||
|
</target>
|
||||||
|
|
||||||
|
|
||||||
|
</project>
|
||||||
20
contrib/macosx/gitbucket.plist
Normal file
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>
|
||||||
20
contrib/redhat/gitbucket.conf
Normal file
20
contrib/redhat/gitbucket.conf
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Bind host
|
||||||
|
#GITBUCKET_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Server port
|
||||||
|
#GITBUCKET_PORT=8080
|
||||||
|
|
||||||
|
# Force HTTPS scheme
|
||||||
|
#GITBUCKET_HTTPS=false
|
||||||
|
|
||||||
|
# Data directory (GITBUCKET_HOME/gitbucket)
|
||||||
|
#GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
|
||||||
|
# Path to the WAR file
|
||||||
|
#GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||||
|
|
||||||
|
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
|
||||||
|
#GITBUCKET_PREFIX=
|
||||||
|
|
||||||
|
# Other Java option
|
||||||
|
#GITBUCKET_JVM_OPTS=
|
||||||
109
contrib/redhat/gitbucket.init
Normal file
109
contrib/redhat/gitbucket.init
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# /etc/rc.d/init.d/gitbucket
|
||||||
|
#
|
||||||
|
# Starts the GitBucket server
|
||||||
|
#
|
||||||
|
# chkconfig: 345 60 40
|
||||||
|
# description: Run GitBucket server
|
||||||
|
# processname: java
|
||||||
|
|
||||||
|
# Source function library
|
||||||
|
. /etc/rc.d/init.d/functions
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||||
|
|
||||||
|
# Pull in cq settings
|
||||||
|
[ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket
|
||||||
|
|
||||||
|
# Location of the log and PID file
|
||||||
|
LOG_FILE=/var/log/gitbucket/run.log
|
||||||
|
PID_FILE=/var/run/gitbucket.pid
|
||||||
|
|
||||||
|
# Default return value
|
||||||
|
RETVAL=0
|
||||||
|
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo -n $"Starting GitBucket server: "
|
||||||
|
|
||||||
|
# Compile statup parameters
|
||||||
|
if [ $GITBUCKET_PORT ]; then
|
||||||
|
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_PREFIX ]; then
|
||||||
|
START_OPTS="${START_OPTS} --prefix=${GITBUCKET_PREFIX}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_HOST ]; then
|
||||||
|
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_HTTPS ]; then
|
||||||
|
START_OPTS="${START_OPTS} --https=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the Java process
|
||||||
|
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
# Store PID of the Java process into a file
|
||||||
|
echo $! > $PID_FILE
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "GitBucket startup"
|
||||||
|
else
|
||||||
|
failure "GitBucket startup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
echo -n $"Stopping GitBucket server: "
|
||||||
|
|
||||||
|
# Run the Java process
|
||||||
|
kill $(cat $PID_FILE 2>/dev/null) >>$LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
rm -f $PID_FILE
|
||||||
|
success "GitBucket stopping"
|
||||||
|
else
|
||||||
|
failure "GitBucket stopping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status -p $PID_FILE java
|
||||||
|
RETVAL=$?
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo $"Usage: $0 [start|stop|restart|status]"
|
||||||
|
RETVAL=2
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
exit $RETVAL
|
||||||
47
contrib/redhat/gitbucket.spec
Normal file
47
contrib/redhat/gitbucket.spec
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
Name: gitbucket
|
||||||
|
Summary: GitHub clone written with Scala.
|
||||||
|
Version: 1.7
|
||||||
|
Release: 1%{?dist}
|
||||||
|
License: Apache
|
||||||
|
URL: https://github.com/takezoe/gitbucket
|
||||||
|
Group: System/Servers
|
||||||
|
Source0: %{name}.war
|
||||||
|
Source1: %{name}.init
|
||||||
|
Source2: %{name}.conf
|
||||||
|
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
||||||
|
BuildArch: noarch
|
||||||
|
Requires: java >= 1.7
|
||||||
|
|
||||||
|
|
||||||
|
%description
|
||||||
|
|
||||||
|
GitBucket is the easily installable GitHub clone written with Scala.
|
||||||
|
|
||||||
|
|
||||||
|
%install
|
||||||
|
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
||||||
|
%{__mkdir_p} %{buildroot}{%{_sysconfdir}/{init.d,sysconfig},%{_datarootdir}/%{name}/lib,%{_sharedstatedir}/%{name},%{_localstatedir}/log/%{name}}
|
||||||
|
%{__install} -m 0644 %{SOURCE0} %{buildroot}%{_datarootdir}/%{name}/lib
|
||||||
|
%{__install} -m 0755 %{SOURCE1} %{buildroot}%{_sysconfdir}/init.d/%{name}
|
||||||
|
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
|
||||||
|
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
||||||
|
|
||||||
|
|
||||||
|
%clean
|
||||||
|
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
||||||
|
|
||||||
|
|
||||||
|
%files
|
||||||
|
%defattr(-,root,root,-)
|
||||||
|
%{_datarootdir}/%{name}/lib/%{name}.war
|
||||||
|
%{_sysconfdir}/init.d/%{name}
|
||||||
|
%config %{_sysconfdir}/sysconfig/%{name}
|
||||||
|
%{_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.
|
||||||
BIN
embed-jetty/javax.servlet-3.0.0.v201112011016.jar
Normal file
BIN
embed-jetty/javax.servlet-3.0.0.v201112011016.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-continuation-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-continuation-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-http-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-http-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-io-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-io-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-security-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-security-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-server-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-server-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-servlet-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-servlet-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-util-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-util-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-webapp-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-webapp-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-xml-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-xml-8.1.8.v20121106.jar
Normal file
Binary file not shown.
242
etc/icons.svg
242
etc/icons.svg
@@ -25,17 +25,17 @@
|
|||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pageshadow="2"
|
inkscape:pageshadow="2"
|
||||||
inkscape:zoom="1.4"
|
inkscape:zoom="1.4"
|
||||||
inkscape:cx="629.30023"
|
inkscape:cx="450.21999"
|
||||||
inkscape:cy="281.44758"
|
inkscape:cy="97.51519"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:current-layer="layer1-9"
|
inkscape:current-layer="layer1-9"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
inkscape:window-width="1366"
|
inkscape:window-width="1366"
|
||||||
inkscape:window-height="705"
|
inkscape:window-height="706"
|
||||||
inkscape:window-x="-8"
|
inkscape:window-x="1912"
|
||||||
inkscape:window-y="-8"
|
inkscape:window-y="-8"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:snap-global="true"
|
inkscape:snap-global="false"
|
||||||
inkscape:snap-grids="false"
|
inkscape:snap-grids="false"
|
||||||
inkscape:snap-page="false"
|
inkscape:snap-page="false"
|
||||||
inkscape:snap-bbox="true"
|
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"
|
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"
|
id="rect2995-0-2-7-7"
|
||||||
inkscape:connector-curvature="0" />
|
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>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 47 KiB |
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sbt.version=0.12.3
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import sbt._
|
import sbt._
|
||||||
import Keys._
|
import Keys._
|
||||||
import org.scalatra.sbt._
|
import org.scalatra.sbt._
|
||||||
import org.scalatra.sbt.PluginKeys._
|
|
||||||
import sbt.ScalaVersion
|
|
||||||
import twirl.sbt.TwirlPlugin._
|
import twirl.sbt.TwirlPlugin._
|
||||||
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
||||||
|
|
||||||
@@ -10,7 +8,7 @@ object MyBuild extends Build {
|
|||||||
val Organization = "jp.sf.amateras"
|
val Organization = "jp.sf.amateras"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val Version = "0.0.1"
|
val Version = "0.0.1"
|
||||||
val ScalaVersion = "2.10.1"
|
val ScalaVersion = "2.10.3"
|
||||||
val ScalatraVersion = "2.2.1"
|
val ScalatraVersion = "2.2.1"
|
||||||
|
|
||||||
lazy val project = Project (
|
lazy val project = Project (
|
||||||
@@ -25,25 +23,31 @@ object MyBuild extends Build {
|
|||||||
Classpaths.typesafeReleases,
|
Classpaths.typesafeReleases,
|
||||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||||
),
|
),
|
||||||
|
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
|
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
|
||||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||||
"org.json4s" %% "json4s-jackson" % "3.2.4",
|
"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",
|
"commons-io" % "commons-io" % "2.4",
|
||||||
"org.pegdown" % "pegdown" % "1.3.0",
|
"org.pegdown" % "pegdown" % "1.4.1",
|
||||||
"org.apache.commons" % "commons-compress" % "1.5",
|
"org.apache.commons" % "commons-compress" % "1.5",
|
||||||
"org.apache.commons" % "commons-email" % "1.3.1",
|
"org.apache.commons" % "commons-email" % "1.3.1",
|
||||||
|
"org.apache.httpcomponents" % "httpclient" % "4.3",
|
||||||
"com.typesafe.slick" %% "slick" % "1.0.1",
|
"com.typesafe.slick" %% "slick" % "1.0.1",
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
"com.h2database" % "h2" % "1.3.171",
|
"com.h2database" % "h2" % "1.3.173",
|
||||||
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
|
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container",
|
"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
|
EclipseKeys.withSource := true,
|
||||||
|
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
|
||||||
|
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
|
||||||
|
packageOptions += Package.MainClass("JettyLauncher")
|
||||||
) ++ seq(Twirl.settings: _*)
|
) ++ seq(Twirl.settings: _*)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
|
|||||||
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
|
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
|
||||||
|
|
||||||
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
|
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
|
||||||
|
|
||||||
|
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")
|
||||||
|
|||||||
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
|||||||
set SCRIPT_DIR=%~dp0
|
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
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 "$@"
|
||||||
|
|||||||
62
src/main/java/JettyLauncher.java
Normal file
62
src/main/java/JettyLauncher.java
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import org.eclipse.jetty.io.EndPoint;
|
||||||
|
import org.eclipse.jetty.server.Request;
|
||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||||
|
import org.eclipse.jetty.webapp.WebAppContext;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.ProtectionDomain;
|
||||||
|
|
||||||
|
public class JettyLauncher {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
String host = null;
|
||||||
|
int port = 8080;
|
||||||
|
String contextPath = "/";
|
||||||
|
boolean forceHttps = false;
|
||||||
|
|
||||||
|
for(String arg: args) {
|
||||||
|
if(arg.startsWith("--") && arg.contains("=")) {
|
||||||
|
String[] dim = arg.split("=");
|
||||||
|
if(dim.length >= 2) {
|
||||||
|
if(dim[0].equals("--host")) {
|
||||||
|
host = dim[1];
|
||||||
|
} else if(dim[0].equals("--port")) {
|
||||||
|
port = Integer.parseInt(dim[1]);
|
||||||
|
} else if(dim[0].equals("--prefix")) {
|
||||||
|
contextPath = dim[1];
|
||||||
|
} else if(dim[0].equals("--gitbucket.home")){
|
||||||
|
System.setProperty("gitbucket.home", dim[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Server server = new Server();
|
||||||
|
|
||||||
|
SelectChannelConnector connector = new SelectChannelConnector();
|
||||||
|
if(host != null) {
|
||||||
|
connector.setHost(host);
|
||||||
|
}
|
||||||
|
connector.setMaxIdleTime(1000 * 60 * 60);
|
||||||
|
connector.setSoLingerTime(-1);
|
||||||
|
connector.setPort(port);
|
||||||
|
server.addConnector(connector);
|
||||||
|
|
||||||
|
WebAppContext context = new WebAppContext();
|
||||||
|
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
|
||||||
|
URL location = domain.getCodeSource().getLocation();
|
||||||
|
|
||||||
|
context.setContextPath(contextPath);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/main/java/util/PatchUtil.java
Normal file
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,4 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<logger name="scala.slick" level="INFO" />
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<logger name="service.WebHookService" level="DEBUG" />
|
||||||
|
<logger name="servlet" level="DEBUG" />
|
||||||
|
-->
|
||||||
</configuration>
|
</configuration>
|
||||||
8
src/main/resources/update/1_6.sql
Normal file
8
src/main/resources/update/1_6.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE WEB_HOOK (
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
URL VARCHAR(200) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL);
|
||||||
|
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
5
src/main/resources/update/1_7.sql
Normal file
5
src/main/resources/update/1_7.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE ACCOUNT ADD COLUMN FULL_NAME VARCHAR(100);
|
||||||
|
|
||||||
|
UPDATE ACCOUNT SET FULL_NAME = USER_NAME WHERE FULL_NAME IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ACCOUNT ALTER COLUMN FULL_NAME SET NOT NULL;
|
||||||
1
src/main/resources/update/1_8.sql
Normal file
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 app._
|
||||||
|
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import javax.servlet._
|
import javax.servlet._
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
class ScalatraBootstrap extends LifeCycle {
|
class ScalatraBootstrap extends LifeCycle {
|
||||||
override def init(context: ServletContext) {
|
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 IndexController, "/")
|
||||||
context.mount(new SearchController, "/")
|
context.mount(new SearchController, "/")
|
||||||
context.mount(new FileUploadController, "/upload")
|
context.mount(new FileUploadController, "/upload")
|
||||||
context.mount(new SignInController, "/*")
|
|
||||||
context.mount(new DashboardController, "/*")
|
context.mount(new DashboardController, "/*")
|
||||||
context.mount(new UserManagementController, "/*")
|
context.mount(new UserManagementController, "/*")
|
||||||
context.mount(new SystemSettingsController, "/*")
|
context.mount(new SystemSettingsController, "/*")
|
||||||
@@ -20,7 +29,9 @@ class ScalatraBootstrap extends LifeCycle {
|
|||||||
context.mount(new IssuesController, "/*")
|
context.mount(new IssuesController, "/*")
|
||||||
context.mount(new PullRequestsController, "/*")
|
context.mount(new PullRequestsController, "/*")
|
||||||
context.mount(new RepositorySettingsController, "/*")
|
context.mount(new RepositorySettingsController, "/*")
|
||||||
|
context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*")
|
||||||
|
|
||||||
|
// Create GITBUCKET_HOME directory if it does not exist
|
||||||
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
||||||
if(!dir.exists){
|
if(!dir.exists){
|
||||||
dir.mkdirs()
|
dir.mkdirs()
|
||||||
|
|||||||
@@ -5,25 +5,24 @@ import util.{FileUtil, OneselfAuthenticator}
|
|||||||
import util.StringUtil._
|
import util.StringUtil._
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import org.scalatra.FlashMapSupport
|
import org.apache.commons.io.FileUtils
|
||||||
|
|
||||||
class AccountController extends AccountControllerBase
|
class AccountController extends AccountControllerBase
|
||||||
with SystemSettingsService with AccountService with RepositoryService with ActivityService
|
with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
|
||||||
with OneselfAuthenticator
|
|
||||||
|
|
||||||
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
|
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||||
self: SystemSettingsService with AccountService with RepositoryService with ActivityService
|
self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
|
||||||
with OneselfAuthenticator =>
|
|
||||||
|
|
||||||
case class AccountNewForm(userName: String, password: String,mailAddress: String,
|
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||||
url: Option[String], fileId: Option[String])
|
url: Option[String], fileId: Option[String])
|
||||||
|
|
||||||
case class AccountEditForm(password: Option[String], mailAddress: String,
|
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
|
||||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||||
|
|
||||||
val newForm = mapping(
|
val newForm = mapping(
|
||||||
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||||
|
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
||||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||||
"fileId" -> trim(label("File ID" , optional(text())))
|
"fileId" -> trim(label("File ID" , optional(text())))
|
||||||
@@ -31,6 +30,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
|||||||
|
|
||||||
val editForm = mapping(
|
val editForm = mapping(
|
||||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
"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")))),
|
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||||
@@ -84,6 +84,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
|||||||
getAccountByUserName(userName).map { account =>
|
getAccountByUserName(userName).map { account =>
|
||||||
updateAccount(account.copy(
|
updateAccount(account.copy(
|
||||||
password = form.password.map(sha1).getOrElse(account.password),
|
password = form.password.map(sha1).getOrElse(account.password),
|
||||||
|
fullName = form.fullName,
|
||||||
mailAddress = form.mailAddress,
|
mailAddress = form.mailAddress,
|
||||||
url = form.url))
|
url = form.url))
|
||||||
|
|
||||||
@@ -94,6 +95,27 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
|||||||
} getOrElse NotFound
|
} 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"){
|
get("/register"){
|
||||||
if(loadSystemSettings().allowAccountRegistration){
|
if(loadSystemSettings().allowAccountRegistration){
|
||||||
if(context.loginAccount.isDefined){
|
if(context.loginAccount.isDefined){
|
||||||
@@ -106,7 +128,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
|||||||
|
|
||||||
post("/register", newForm){ form =>
|
post("/register", newForm){ form =>
|
||||||
if(loadSystemSettings().allowAccountRegistration){
|
if(loadSystemSettings().allowAccountRegistration){
|
||||||
createAccount(form.userName, sha1(form.password), form.mailAddress, false, form.url)
|
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
|
||||||
updateImage(form.userName, form.fileId, false)
|
updateImage(form.userName, form.fileId, false)
|
||||||
redirect("/signin")
|
redirect("/signin")
|
||||||
} else NotFound
|
} else NotFound
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import _root_.util.Directory._
|
import _root_.util.Directory._
|
||||||
import _root_.util.{FileUtil, Validations}
|
import _root_.util.Implicits._
|
||||||
|
import _root_.util.ControlUtil._
|
||||||
|
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import org.scalatra.json._
|
import org.scalatra.json._
|
||||||
import org.json4s._
|
import org.json4s._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import model.Account
|
import model.Account
|
||||||
import scala.Some
|
import service.{SystemSettingsService, AccountService}
|
||||||
import service.AccountService
|
|
||||||
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
|
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||||
|
import org.scalatra.i18n._
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides generic features for controller implementations.
|
* Provides generic features for controller implementations.
|
||||||
*/
|
*/
|
||||||
abstract class ControllerBase extends ScalatraFilter
|
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
|
implicit val jsonFormats = DefaultFormats
|
||||||
|
|
||||||
@@ -32,10 +35,10 @@ abstract class ControllerBase extends ScalatraFilter
|
|||||||
val path = httpRequest.getRequestURI.substring(context.length)
|
val path = httpRequest.getRequestURI.substring(context.length)
|
||||||
|
|
||||||
if(path.startsWith("/console/")){
|
if(path.startsWith("/console/")){
|
||||||
val account = httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account]
|
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||||
if(account == null){
|
if(account == null){
|
||||||
// Redirect to login form
|
// Redirect to login form
|
||||||
httpResponse.sendRedirect(context + "/signin?" + path)
|
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
|
||||||
} else if(account.isAdmin){
|
} else if(account.isAdmin){
|
||||||
// H2 Console (administrators only)
|
// H2 Console (administrators only)
|
||||||
chain.doFilter(request, response)
|
chain.doFilter(request, response)
|
||||||
@@ -55,91 +58,72 @@ abstract class ControllerBase extends ScalatraFilter
|
|||||||
/**
|
/**
|
||||||
* Returns the context object for the request.
|
* Returns the context object for the request.
|
||||||
*/
|
*/
|
||||||
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
|
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request)
|
||||||
|
|
||||||
private def currentURL: String = {
|
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
|
||||||
val queryString = request.getQueryString
|
|
||||||
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
|
|
||||||
}
|
|
||||||
|
|
||||||
private def LoginAccount: Option[Account] = {
|
def ajaxGet(path : String)(action : => Any) : Route =
|
||||||
session.get("LOGIN_ACCOUNT") match {
|
|
||||||
case Some(x: Account) => Some(x)
|
|
||||||
case _ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def ajaxGet(path : String)(action : => Any) : Route = {
|
|
||||||
super.get(path){
|
super.get(path){
|
||||||
request.setAttribute("AJAX", "true")
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
action
|
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 =>
|
super.ajaxGet(path, form){ form =>
|
||||||
request.setAttribute("AJAX", "true")
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
action(form)
|
action(form)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def ajaxPost(path : String)(action : => Any) : Route = {
|
def ajaxPost(path : String)(action : => Any) : Route =
|
||||||
super.post(path){
|
super.post(path){
|
||||||
request.setAttribute("AJAX", "true")
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
action
|
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 =>
|
super.ajaxPost(path, form){ form =>
|
||||||
request.setAttribute("AJAX", "true")
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
action(form)
|
action(form)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected def NotFound() = {
|
protected def NotFound() =
|
||||||
if(request.getAttribute("AJAX") == null){
|
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||||
org.scalatra.NotFound(html.error("Not Found"))
|
|
||||||
} else {
|
|
||||||
org.scalatra.NotFound()
|
org.scalatra.NotFound()
|
||||||
|
} else {
|
||||||
|
org.scalatra.NotFound(html.error("Not Found"))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected def Unauthorized()(implicit context: app.Context) = {
|
protected def Unauthorized()(implicit context: app.Context) =
|
||||||
if(request.getAttribute("AJAX") == null){
|
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||||
|
org.scalatra.Unauthorized()
|
||||||
|
} else {
|
||||||
if(context.loginAccount.isDefined){
|
if(context.loginAccount.isDefined){
|
||||||
org.scalatra.Unauthorized(redirect("/"))
|
org.scalatra.Unauthorized(redirect("/"))
|
||||||
} else {
|
} else {
|
||||||
if(request.getMethod.toUpperCase == "POST"){
|
if(request.getMethod.toUpperCase == "POST"){
|
||||||
org.scalatra.Unauthorized(redirect("/signin"))
|
org.scalatra.Unauthorized(redirect("/signin"))
|
||||||
} else {
|
} 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 "")
|
||||||
|
}
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
org.scalatra.Unauthorized()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected def baseUrl = {
|
override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
|
||||||
val url = request.getRequestURL.toString
|
includeContextPath: Boolean = true, includeServletPath: Boolean = true)
|
||||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
(implicit request: HttpServletRequest, response: HttpServletResponse) =
|
||||||
}
|
if (path.startsWith("http")) path
|
||||||
|
else baseUrl + url(path, params, false, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context object for the current request.
|
* Context object for the current request.
|
||||||
*/
|
*/
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get object from cache.
|
* Get object from cache.
|
||||||
@@ -147,13 +131,14 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
|
|||||||
* If object has not been cached with the specified key then retrieves by given action.
|
* If object has not been cached with the specified key then retrieves by given action.
|
||||||
* Cached object are available during a request.
|
* Cached object are available during a request.
|
||||||
*/
|
*/
|
||||||
def cache[A](key: String)(action: => A): A = {
|
def cache[A](key: String)(action: => A): A =
|
||||||
Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse {
|
defining(Keys.Request.Cache(key)){ cacheKey =>
|
||||||
val newObject = action
|
Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
|
||||||
request.setAttribute("cache." + key, newObject)
|
val newObject = action
|
||||||
newObject
|
request.setAttribute(cacheKey, newObject)
|
||||||
|
newObject
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +148,7 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
|
|||||||
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
|
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
|
||||||
self: AccountService =>
|
self: AccountService =>
|
||||||
|
|
||||||
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = {
|
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
|
||||||
if(clearImage){
|
if(clearImage){
|
||||||
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
||||||
new java.io.File(getUserUploadDir(userName), image).delete()
|
new java.io.File(getUserUploadDir(userName), image).delete()
|
||||||
@@ -179,16 +164,15 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
|
|||||||
updateAvatarImage(userName, Some(filename))
|
updateAvatarImage(userName, Some(filename))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected def uniqueUserName: Constraint = new Constraint(){
|
protected def uniqueUserName: 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).map { _ => "User already exists." }
|
getAccountByUserName(value, true).map { _ => "User already exists." }
|
||||||
}
|
}
|
||||||
|
|
||||||
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
|
protected def uniqueMailAddress(paramName: String = ""): 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] =
|
||||||
getAccountByMailAddress(value)
|
getAccountByMailAddress(value, true)
|
||||||
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
||||||
.map { _ => "Mail address is already registered." }
|
.map { _ => "Mail address is already registered." }
|
||||||
}
|
}
|
||||||
@@ -215,12 +199,7 @@ trait FileUploadControllerBase {
|
|||||||
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
|
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
|
||||||
FileUtils.deleteDirectory(TemporaryDir)
|
FileUtils.deleteDirectory(TemporaryDir)
|
||||||
|
|
||||||
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
|
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
|
||||||
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
|
session.getAndRemove[String](Keys.Session.Upload(fileId))
|
||||||
if(filename.isDefined){
|
|
||||||
session.removeAttribute("upload_" + fileId)
|
|
||||||
}
|
|
||||||
filename
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
import util._
|
import util._
|
||||||
import service._
|
import service._
|
||||||
import java.io.File
|
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.apache.commons.io._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import org.eclipse.jgit.lib.PersonIdent
|
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||||
import scala.Some
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
class CreateRepositoryController extends CreateRepositoryControllerBase
|
class CreateRepositoryController extends CreateRepositoryControllerBase
|
||||||
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
||||||
@@ -73,32 +73,26 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
|||||||
JGitUtil.initRepository(gitdir)
|
JGitUtil.initRepository(gitdir)
|
||||||
|
|
||||||
if(form.createReadme){
|
if(form.createReadme){
|
||||||
val tmpdir = getInitRepositoryDir(form.owner, form.name)
|
using(Git.open(gitdir)){ git =>
|
||||||
try {
|
val builder = DirCache.newInCore.builder()
|
||||||
// Clone the repository
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
|
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
|
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||||
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
|
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||||
if(form.description.nonEmpty){
|
builder.finish()
|
||||||
form.name + "\n" +
|
|
||||||
"===============\n" +
|
|
||||||
"\n" +
|
|
||||||
form.description.get
|
|
||||||
} else {
|
|
||||||
form.name + "\n" +
|
|
||||||
"===============\n"
|
|
||||||
}, "UTF-8")
|
|
||||||
|
|
||||||
val git = Git.open(tmpdir)
|
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||||
git.add.addFilepattern("README.md").call
|
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||||
git.commit
|
|
||||||
.setCommitter(new PersonIdent(loginUserName, loginAccount.mailAddress))
|
|
||||||
.setMessage("Initial commit").call
|
|
||||||
git.push.call
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,54 +113,62 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
|||||||
val loginUserName = loginAccount.userName
|
val loginUserName = loginAccount.userName
|
||||||
|
|
||||||
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
|
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
|
||||||
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
|
if(repository.owner == loginUserName){
|
||||||
// Insert to the database at first
|
// redirect to the repository
|
||||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
redirect(s"/${repository.owner}/${repository.name}")
|
||||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(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(
|
createRepository(
|
||||||
repositoryName = repository.name,
|
repositoryName = repository.name,
|
||||||
userName = loginUserName,
|
userName = loginUserName,
|
||||||
description = repository.repository.description,
|
description = repository.repository.description,
|
||||||
isPrivate = repository.repository.isPrivate,
|
isPrivate = repository.repository.isPrivate,
|
||||||
originRepositoryName = Some(originRepositoryName),
|
originRepositoryName = Some(originRepositoryName),
|
||||||
originUserName = Some(originUserName),
|
originUserName = Some(originUserName),
|
||||||
parentRepositoryName = Some(repository.name),
|
parentRepositoryName = Some(repository.name),
|
||||||
parentUserName = Some(repository.owner)
|
parentUserName = Some(repository.owner)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Insert default labels
|
// Insert default labels
|
||||||
insertDefaultLabels(loginUserName, repository.name)
|
insertDefaultLabels(loginUserName, repository.name)
|
||||||
|
|
||||||
// clone repository actually
|
// clone repository actually
|
||||||
JGitUtil.cloneRepository(
|
JGitUtil.cloneRepository(
|
||||||
getRepositoryDir(repository.owner, repository.name),
|
getRepositoryDir(repository.owner, repository.name),
|
||||||
getRepositoryDir(loginUserName, repository.name))
|
getRepositoryDir(loginUserName, repository.name))
|
||||||
|
|
||||||
// Create Wiki repository
|
// Create Wiki repository
|
||||||
JGitUtil.cloneRepository(
|
JGitUtil.cloneRepository(
|
||||||
getWikiRepositoryDir(repository.owner, repository.name),
|
getWikiRepositoryDir(repository.owner, repository.name),
|
||||||
getWikiRepositoryDir(loginUserName, repository.name))
|
getWikiRepositoryDir(loginUserName, repository.name))
|
||||||
|
|
||||||
// insert commit id
|
// insert commit id
|
||||||
JGitUtil.withGit(getRepositoryDir(loginUserName, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
|
||||||
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
|
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
|
||||||
JGitUtil.getCommitLog(git, branch) match {
|
JGitUtil.getCommitLog(git, branch) match {
|
||||||
case Right((commits, _)) => commits.foreach { commit =>
|
case Right((commits, _)) => commits.foreach { commit =>
|
||||||
if(!existsCommitId(loginUserName, repository.name, commit.id)){
|
if(!existsCommitId(loginUserName, repository.name, commit.id)){
|
||||||
insertCommitId(loginUserName, repository.name, commit.id)
|
insertCommitId(loginUserName, repository.name, commit.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
case Left(_) => ???
|
||||||
}
|
}
|
||||||
case Left(_) => ???
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Record activity
|
// Record activity
|
||||||
recordForkActivity(repository.owner, repository.name, loginUserName)
|
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))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -180,7 +182,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def existsAccount: Constraint = new Constraint(){
|
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
|
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +190,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
|||||||
* Duplicate check for the repository name.
|
* Duplicate check for the repository name.
|
||||||
*/
|
*/
|
||||||
private def unique: Constraint = new Constraint(){
|
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 =>
|
params.get("owner").flatMap { userName =>
|
||||||
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.UsersAuthenticator
|
import util.{UsersAuthenticator, Keys}
|
||||||
|
import util.Implicits._
|
||||||
|
|
||||||
class DashboardController extends DashboardControllerBase
|
class DashboardController extends DashboardControllerBase
|
||||||
with IssuesService with PullRequestService with RepositoryService with AccountService
|
with IssuesService with PullRequestService with RepositoryService with AccountService
|
||||||
@@ -42,12 +43,10 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
import IssuesService._
|
import IssuesService._
|
||||||
|
|
||||||
// condition
|
// condition
|
||||||
val sessionKey = "dashboard/issues"
|
val condition = session.putAndGet(Keys.Session.DashboardIssues,
|
||||||
val condition = if(request.getQueryString == null)
|
if(request.hasQueryString) IssueSearchCondition(request)
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
|
||||||
else IssueSearchCondition(request)
|
)
|
||||||
|
|
||||||
session.put(sessionKey, condition)
|
|
||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
val userName = context.loginAccount.get.userName
|
||||||
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
|
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
|
||||||
@@ -75,15 +74,10 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
import PullRequestService._
|
import PullRequestService._
|
||||||
|
|
||||||
// condition
|
// condition
|
||||||
val sessionKey = "dashboard/pulls"
|
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
|
||||||
val condition = {
|
if(request.hasQueryString) IssueSearchCondition(request)
|
||||||
if(request.getQueryString == null)
|
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
}.copy(repo = repository))
|
||||||
else
|
|
||||||
IssueSearchCondition(request)
|
|
||||||
}.copy(repo = repository)
|
|
||||||
|
|
||||||
session.put(sessionKey, condition)
|
|
||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
val userName = context.loginAccount.get.userName
|
||||||
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
|
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
|
||||||
@@ -93,10 +87,6 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
val counts = countIssueGroupByRepository(
|
val counts = countIssueGroupByRepository(
|
||||||
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
|
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
|
||||||
|
|
||||||
getRepositoryNamesOfUser(userName).map { repoName =>
|
|
||||||
(userName, repoName, counts.collectFirst { case (_, repoName, count) => count })
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboard.html.pulls(
|
dashboard.html.pulls(
|
||||||
pulls.html.listparts(
|
pulls.html.listparts(
|
||||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
|
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import util.{FileUtil}
|
import _root_.util.{Keys, FileUtil}
|
||||||
|
import util.ControlUtil._
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
|
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
@@ -11,17 +12,15 @@ import org.apache.commons.io.FileUtils
|
|||||||
* This servlet saves uploaded file as temporary file and returns the unique id.
|
* 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.
|
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
|
||||||
*/
|
*/
|
||||||
class FileUploadController extends ScalatraServlet
|
class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
|
||||||
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
|
|
||||||
|
|
||||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||||
|
|
||||||
post("/image"){
|
post("/image"){
|
||||||
fileParams.get("file") match {
|
fileParams.get("file") match {
|
||||||
case Some(file) if(FileUtil.isImage(file.name)) => {
|
case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
|
||||||
val fileId = generateFileId
|
|
||||||
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
|
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
|
||||||
session += "upload_" + fileId -> file.name
|
session += Keys.Session.Upload(fileId) -> file.name
|
||||||
Ok(fileId)
|
Ok(fileId)
|
||||||
}
|
}
|
||||||
case None => BadRequest
|
case None => BadRequest
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import service._
|
|||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
class IndexController extends IndexControllerBase
|
class IndexController extends IndexControllerBase
|
||||||
with RepositoryService with SystemSettingsService with ActivityService with AccountService
|
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
|
||||||
with UsersAuthenticator
|
|
||||||
|
|
||||||
trait IndexControllerBase extends ControllerBase {
|
trait IndexControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with SystemSettingsService with ActivityService with AccountService
|
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
|
||||||
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("/"){
|
get("/"){
|
||||||
val loginAccount = context.loginAccount
|
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.
|
* JSON API for collaborator completion.
|
||||||
*
|
*
|
||||||
@@ -30,7 +73,7 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
get("/_user/proposals")(usersOnly {
|
get("/_user/proposals")(usersOnly {
|
||||||
contentType = formats("json")
|
contentType = formats("json")
|
||||||
org.json4s.jackson.Serialization.write(
|
org.json4s.jackson.Serialization.write(
|
||||||
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
|
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ import jp.sf.amateras.scalatra.forms._
|
|||||||
|
|
||||||
import service._
|
import service._
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier}
|
import util._
|
||||||
|
import util.Implicits._
|
||||||
|
import util.ControlUtil._
|
||||||
import org.scalatra.Ok
|
import org.scalatra.Ok
|
||||||
|
import model.Issue
|
||||||
|
|
||||||
class IssuesController extends IssuesControllerBase
|
class IssuesController extends IssuesControllerBase
|
||||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
trait IssuesControllerBase extends ControllerBase {
|
trait IssuesControllerBase extends ControllerBase {
|
||||||
self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
|
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
case class IssueCreateForm(title: String, content: Option[String],
|
case class IssueCreateForm(title: String, content: Option[String],
|
||||||
@@ -57,79 +60,84 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
||||||
val owner = repository.owner
|
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
|
||||||
val name = repository.name
|
getIssue(owner, name, issueId) map {
|
||||||
val issueId = params("id")
|
issues.html.issue(
|
||||||
|
|
||||||
getIssue(owner, name, issueId) map {
|
|
||||||
issues.html.issue(
|
|
||||||
_,
|
_,
|
||||||
getComments(owner, name, issueId.toInt),
|
getComments(owner, name, issueId.toInt),
|
||||||
getIssueLabels(owner, name, issueId.toInt),
|
getIssueLabels(owner, name, issueId.toInt),
|
||||||
(getCollaborators(owner, name) :+ owner).sorted,
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
getMilestonesWithIssueCount(owner, name),
|
getMilestonesWithIssueCount(owner, name),
|
||||||
getLabels(owner, name),
|
getLabels(owner, name),
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
repository)
|
repository)
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
||||||
val owner = repository.owner
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
val name = repository.name
|
issues.html.create(
|
||||||
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
issues.html.create(
|
getMilestones(owner, name),
|
||||||
(getCollaborators(owner, name) :+ owner).sorted,
|
getLabels(owner, name),
|
||||||
getMilestones(owner, name),
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
getLabels(owner, name),
|
repository)
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
}
|
||||||
repository)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
|
||||||
val owner = repository.owner
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
val name = repository.name
|
val writable = hasWritePermission(owner, name, context.loginAccount)
|
||||||
val writable = hasWritePermission(owner, name, context.loginAccount)
|
val userName = context.loginAccount.get.userName
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
|
|
||||||
// insert issue
|
// insert issue
|
||||||
val issueId = createIssue(owner, name, userName, form.title, form.content,
|
val issueId = createIssue(owner, name, userName, form.title, form.content,
|
||||||
if(writable) form.assignedUserName else None,
|
if(writable) form.assignedUserName else None,
|
||||||
if(writable) form.milestoneId else None)
|
if(writable) form.milestoneId else None)
|
||||||
|
|
||||||
// insert labels
|
// insert labels
|
||||||
if(writable){
|
if(writable){
|
||||||
form.labelNames.map { value =>
|
form.labelNames.map { value =>
|
||||||
val labels = getLabels(owner, name)
|
val labels = getLabels(owner, name)
|
||||||
value.split(",").foreach { labelName =>
|
value.split(",").foreach { labelName =>
|
||||||
labels.find(_.labelName == labelName).map { label =>
|
labels.find(_.labelName == labelName).map { label =>
|
||||||
registerIssueLabel(owner, name, issueId, label.labelId)
|
registerIssueLabel(owner, name, issueId, label.labelId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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}")
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// record activity
|
|
||||||
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
|
||||||
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
||||||
val owner = repository.owner
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
val name = repository.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(""))
|
||||||
|
|
||||||
getIssue(owner, name, params("id")).map { issue =>
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||||
if(isEditable(owner, name, issue.openedUserName)){
|
} else Unauthorized
|
||||||
updateIssue(owner, name, issue.issueId, form.title, form.content)
|
} getOrElse NotFound
|
||||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
}
|
||||||
} else Unauthorized
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
@@ -147,15 +155,24 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
val owner = repository.owner
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
val name = repository.name
|
getComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
updateComment(comment.commentId, form.content)
|
||||||
|
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
getComment(owner, name, params("id")).map { comment =>
|
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
|
||||||
if(isEditable(owner, name, comment.commentedUserName)){
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
updateComment(comment.commentId, form.content)
|
getComment(owner, name, params("id")).map { comment =>
|
||||||
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
} else Unauthorized
|
Ok(deleteComment(comment.commentId))
|
||||||
} getOrElse NotFound
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
|
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
|
||||||
@@ -194,17 +211,17 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
||||||
val issueId = params("id").toInt
|
defining(params("id").toInt){ issueId =>
|
||||||
|
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||||
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||||
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
|
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
|
||||||
val issueId = params("id").toInt
|
defining(params("id").toInt){ issueId =>
|
||||||
|
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||||
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||||
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
||||||
@@ -223,41 +240,41 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||||
val action = params.get("value")
|
defining(params.get("value")){ action =>
|
||||||
|
executeBatch(repository) {
|
||||||
executeBatch(repository) {
|
handleComment(_, None, repository)( _ => action)
|
||||||
handleComment(_, None, repository)( _ => action)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
|
|
||||||
val labelId = params("value").toInt
|
|
||||||
|
|
||||||
executeBatch(repository) { issueId =>
|
|
||||||
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
|
||||||
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
|
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
|
||||||
val value = assignedUserName("value")
|
params("value").toIntOpt.map{ labelId =>
|
||||||
|
executeBatch(repository) { issueId =>
|
||||||
|
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
||||||
|
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
executeBatch(repository) {
|
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
|
||||||
updateAssignedUserName(repository.owner, repository.name, _, value)
|
defining(assignedUserName("value")){ value =>
|
||||||
|
executeBatch(repository) {
|
||||||
|
updateAssignedUserName(repository.owner, repository.name, _, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
|
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
|
||||||
val value = milestoneId("value")
|
defining(milestoneId("value")){ value =>
|
||||||
|
executeBatch(repository) {
|
||||||
executeBatch(repository) {
|
updateMilestoneId(repository.owner, repository.name, _, value)
|
||||||
updateMilestoneId(repository.owner, repository.name, _, value)
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
||||||
val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
|
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
|
||||||
|
|
||||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||||
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||||
@@ -267,95 +284,109 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
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]]
|
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||||
*/
|
*/
|
||||||
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
||||||
(getAction: model.Issue => Option[String] =
|
(getAction: model.Issue => Option[String] =
|
||||||
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
|
|
||||||
getIssue(owner, name, issueId.toString) map { issue =>
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
val (action, recordActivity) =
|
val userName = context.loginAccount.get.userName
|
||||||
getAction(issue)
|
|
||||||
.collect {
|
getIssue(owner, name, issueId.toString) map { issue =>
|
||||||
|
val (action, recordActivity) =
|
||||||
|
getAction(issue)
|
||||||
|
.collect {
|
||||||
case "close" => true -> (Some("close") ->
|
case "close" => true -> (Some("close") ->
|
||||||
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||||
case "reopen" => false -> (Some("reopen") ->
|
case "reopen" => false -> (Some("reopen") ->
|
||||||
Some(recordReopenIssueActivity _))
|
Some(recordReopenIssueActivity _))
|
||||||
}
|
}
|
||||||
.map { case (closed, t) =>
|
.map { case (closed, t) =>
|
||||||
updateClosed(owner, name, issueId, closed)
|
updateClosed(owner, name, issueId, closed)
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
.getOrElse(None -> None)
|
.getOrElse(None -> None)
|
||||||
|
|
||||||
val commentId = content
|
val commentId = content
|
||||||
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
|
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
|
||||||
.getOrElse ( action.get.capitalize -> action.get )
|
.getOrElse ( action.get.capitalize -> action.get )
|
||||||
match {
|
match {
|
||||||
case (content, action) => createComment(owner, name, userName, issueId, content, action)
|
case (content, action) => createComment(owner, name, userName, issueId, content, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// record activity
|
// record activity
|
||||||
content foreach {
|
content foreach {
|
||||||
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||||
(owner, name, userName, issueId, _)
|
(owner, name, userName, issueId, _)
|
||||||
}
|
}
|
||||||
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
||||||
|
|
||||||
// notifications
|
// extract references and create refer comment
|
||||||
Notifier() match {
|
content.map { content =>
|
||||||
case f =>
|
createReferComment(owner, name, issue, content)
|
||||||
content foreach {
|
}
|
||||||
f.toNotify(repository, issueId, _){
|
|
||||||
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
|
// notifications
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
|
Notifier() match {
|
||||||
|
case f =>
|
||||||
|
content foreach {
|
||||||
|
f.toNotify(repository, issueId, _){
|
||||||
|
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
action foreach {
|
||||||
action foreach {
|
f.toNotify(repository, issueId, _){
|
||||||
f.toNotify(repository, issueId, _){
|
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
issue -> commentId
|
issue -> commentId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
|
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
|
||||||
val owner = repository.owner
|
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||||
val repoName = repository.name
|
val filterUser = Map(filter -> params.getOrElse("userName", ""))
|
||||||
val filterUser = Map(filter -> params.getOrElse("userName", ""))
|
val page = IssueSearchCondition.page(request)
|
||||||
val page = IssueSearchCondition.page(request)
|
val sessionKey = Keys.Session.Issues(owner, repoName)
|
||||||
val sessionKey = s"${owner}/${repoName}/issues"
|
|
||||||
|
|
||||||
// retrieve search condition
|
// retrieve search condition
|
||||||
val condition = if(request.getQueryString == null){
|
val condition = session.putAndGet(sessionKey,
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
if(request.hasQueryString) IssueSearchCondition(request)
|
||||||
} else IssueSearchCondition(request)
|
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||||
|
)
|
||||||
|
|
||||||
session.put(sessionKey, condition)
|
issues.html.list(
|
||||||
|
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||||
issues.html.list(
|
page,
|
||||||
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||||
page,
|
getMilestones(owner, repoName),
|
||||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
getLabels(owner, repoName),
|
||||||
getMilestones(owner, repoName),
|
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
|
||||||
getLabels(owner, repoName),
|
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
|
||||||
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
|
countIssue(condition, Map.empty, false, owner -> repoName),
|
||||||
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
|
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
|
||||||
countIssue(condition, Map.empty, false, owner -> repoName),
|
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
|
||||||
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
|
countIssueGroupByLabels(owner, repoName, condition, filterUser),
|
||||||
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
|
condition,
|
||||||
countIssueGroupByLabels(owner, repoName, condition, filterUser),
|
filter,
|
||||||
condition,
|
repository,
|
||||||
filter,
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
repository,
|
}
|
||||||
hasWritePermission(owner, repoName, context.loginAccount))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app
|
|||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import service._
|
import service._
|
||||||
import util.CollaboratorsAuthenticator
|
import util.CollaboratorsAuthenticator
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
class LabelsController extends LabelsControllerBase
|
class LabelsController extends LabelsControllerBase
|
||||||
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
|
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.
|
* Constraint for the identifier such as user name, repository name or page name.
|
||||||
*/
|
*/
|
||||||
private def labelName: Constraint = new Constraint(){
|
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("^[^,]+$")){
|
if(!value.matches("^[^,]+$")){
|
||||||
Some(s"${name} contains invalid character.")
|
Some(s"${name} contains invalid character.")
|
||||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import jp.sf.amateras.scalatra.forms._
|
|||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
||||||
|
import util.Implicits._
|
||||||
|
|
||||||
class MilestonesController extends MilestonesControllerBase
|
class MilestonesController extends MilestonesControllerBase
|
||||||
with MilestonesService with RepositoryService with AccountService
|
with MilestonesService with RepositoryService with AccountService
|
||||||
@@ -39,34 +40,44 @@ trait MilestonesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
|
||||||
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, params("milestoneId").toInt), repository)
|
params("milestoneId").toIntOpt.map{ milestoneId =>
|
||||||
|
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
|
||||||
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
||||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
|
||||||
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
|
||||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
|
||||||
closeMilestone(milestone)
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
closeMilestone(milestone)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
|
||||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
|
||||||
openMilestone(milestone)
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
openMilestone(milestone)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
|
||||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
|
||||||
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier}
|
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import util.Implicits._
|
import util.Implicits._
|
||||||
|
import util.ControlUtil._
|
||||||
import service._
|
import service._
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import org.eclipse.jgit.transport.RefSpec
|
import org.eclipse.jgit.transport.RefSpec
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
import org.eclipse.jgit.lib.PersonIdent
|
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
|
|
||||||
import service.IssuesService._
|
import service.IssuesService._
|
||||||
import service.PullRequestService._
|
import service.PullRequestService._
|
||||||
import util.JGitUtil.DiffInfo
|
import util.JGitUtil.DiffInfo
|
||||||
import scala.Some
|
|
||||||
import service.RepositoryService.RepositoryTreeNode
|
import service.RepositoryService.RepositoryTreeNode
|
||||||
import util.JGitUtil.CommitInfo
|
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
|
class PullRequestsController extends PullRequestsControllerBase
|
||||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
|
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
trait PullRequestsControllerBase extends ControllerBase {
|
trait PullRequestsControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService
|
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||||
|
|
||||||
val pullRequestForm = mapping(
|
val pullRequestForm = mapping(
|
||||||
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
||||||
"content" -> trim(label("Content", optional(text()))),
|
"content" -> trim(label("Content", optional(text()))),
|
||||||
"targetUserName" -> trim(text(required, maxlength(100))),
|
"targetUserName" -> trim(text(required, maxlength(100))),
|
||||||
"targetBranch" -> trim(text(required, maxlength(100))),
|
"targetBranch" -> trim(text(required, maxlength(100))),
|
||||||
"requestUserName" -> trim(text(required, maxlength(100))),
|
"requestUserName" -> trim(text(required, maxlength(100))),
|
||||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
"requestRepositoryName" -> trim(text(required, maxlength(100))),
|
||||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||||
"commitIdTo" -> trim(text(required, maxlength(40)))
|
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||||
|
"commitIdTo" -> trim(text(required, maxlength(40)))
|
||||||
)(PullRequestForm.apply)
|
)(PullRequestForm.apply)
|
||||||
|
|
||||||
val mergeForm = mapping(
|
val mergeForm = mapping(
|
||||||
@@ -47,6 +52,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
targetUserName: String,
|
targetUserName: String,
|
||||||
targetBranch: String,
|
targetBranch: String,
|
||||||
requestUserName: String,
|
requestUserName: String,
|
||||||
|
requestRepositoryName: String,
|
||||||
requestBranch: String,
|
requestBranch: String,
|
||||||
commitIdFrom: String,
|
commitIdFrom: String,
|
||||||
commitIdTo: String)
|
commitIdTo: String)
|
||||||
@@ -62,156 +68,150 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||||
val owner = repository.owner
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
val name = repository.name
|
val owner = repository.owner
|
||||||
val issueId = params("id").toInt
|
val name = repository.name
|
||||||
|
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||||
|
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||||
|
val (commits, diffs) =
|
||||||
|
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
|
||||||
|
|
||||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
pulls.html.pullreq(
|
||||||
JGitUtil.withGit(getRepositoryDir(owner, name)){ git =>
|
issue, pullreq,
|
||||||
val requestCommitId = git.getRepository.resolve(pullreq.requestBranch)
|
getComments(owner, name, issueId),
|
||||||
|
getIssueLabels(owner, name, issueId.toInt),
|
||||||
val (commits, diffs) =
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
|
getMilestonesWithIssueCount(owner, name),
|
||||||
|
getLabels(owner, name),
|
||||||
pulls.html.pullreq(
|
commits,
|
||||||
issue, pullreq,
|
diffs,
|
||||||
getComments(owner, name, issueId.toInt),
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
(getCollaborators(owner, name) :+ owner).sorted,
|
repository)
|
||||||
getMilestonesWithIssueCount(owner, name),
|
}
|
||||||
commits,
|
|
||||||
diffs,
|
|
||||||
if(issue.closed){
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch)
|
|
||||||
},
|
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
|
||||||
repository,
|
|
||||||
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
|
||||||
}
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
|
||||||
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
|
val owner = repository.owner
|
||||||
|
val name = repository.name
|
||||||
|
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||||
|
pulls.html.mergeguide(
|
||||||
|
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
|
||||||
|
pullreq,
|
||||||
|
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
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
||||||
LockUtil.lock(s"${repository.owner}/${repository.name}/merge"){
|
params("id").toIntOpt.flatMap { issueId =>
|
||||||
val issueId = params("id").toInt
|
val owner = repository.owner
|
||||||
|
val name = repository.name
|
||||||
|
LockUtil.lock(s"${owner}/${name}/merge"){
|
||||||
|
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
|
||||||
|
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)
|
||||||
|
|
||||||
getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) =>
|
// record activity
|
||||||
val remote = getRepositoryDir(repository.owner, repository.name)
|
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||||
val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}")
|
|
||||||
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
|
|
||||||
|
|
||||||
try {
|
// merge
|
||||||
// mark issue as merged and close.
|
val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
|
||||||
val loginAccount = context.loginAccount.get
|
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, form.message, "merge")
|
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
|
||||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close")
|
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||||
updateClosed(repository.owner, repository.name, issueId, true)
|
val conflicted = try {
|
||||||
|
!merger.merge(mergeBaseTip, mergeTip)
|
||||||
// record activity
|
} catch {
|
||||||
recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, form.message)
|
case e: NoMergeBaseException => true
|
||||||
|
|
||||||
// TODO apply ref comment
|
|
||||||
|
|
||||||
// fetch pull request to temporary working repository
|
|
||||||
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
|
|
||||||
|
|
||||||
git.fetch
|
|
||||||
.setRemote(getRepositoryDir(repository.owner, repository.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.userName, loginAccount.mailAddress))
|
|
||||||
.call
|
|
||||||
|
|
||||||
// push
|
|
||||||
git.push.call
|
|
||||||
|
|
||||||
val (commits, _) = getRequestCompareInfo(repository.owner, repository.name, pullreq.commitIdFrom,
|
|
||||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
|
||||||
|
|
||||||
commits.flatten.foreach { commit =>
|
|
||||||
if(!existsCommitId(repository.owner, repository.name, commit.id)){
|
|
||||||
insertCommitId(repository.owner, repository.name, commit.id)
|
|
||||||
}
|
}
|
||||||
|
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}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// notifications
|
|
||||||
Notifier().toNotify(repository, issueId, "merge"){
|
|
||||||
Notifier.msgStatus(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
git.getRepository.close
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
}
|
||||||
} getOrElse NotFound
|
}
|
||||||
}
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether conflict will be caused in merging.
|
|
||||||
* Returns true if conflict will be caused.
|
|
||||||
*/
|
|
||||||
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)
|
|
||||||
val tmpdir = new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")
|
|
||||||
if(tmpdir.exists()){
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call
|
|
||||||
try {
|
|
||||||
git.checkout.setName(branch).call
|
|
||||||
|
|
||||||
git.fetch
|
|
||||||
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
|
|
||||||
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
|
|
||||||
|
|
||||||
val result = git.merge
|
|
||||||
.include(git.getRepository.resolve("FETCH_HEAD"))
|
|
||||||
.setCommit(false).call
|
|
||||||
|
|
||||||
result.getConflicts != null
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
git.getRepository.close
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||||
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
|
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
|
||||||
withGit(
|
using(
|
||||||
getRepositoryDir(originUserName, originRepositoryName),
|
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||||
getRepositoryDir(forkedRepository.owner, forkedRepository.name)
|
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||||
){ (oldGit, newGit) =>
|
){ (oldGit, newGit) =>
|
||||||
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
|
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
|
||||||
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
|
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
|
||||||
@@ -221,59 +221,96 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
}
|
}
|
||||||
case _ => {
|
case _ => {
|
||||||
JGitUtil.withGit(getRepositoryDir(forkedRepository.owner, forkedRepository.name)){ git =>
|
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
|
||||||
val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2
|
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
|
||||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||||
|
} getOrElse {
|
||||||
|
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
|
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
|
||||||
val Seq(origin, forked) = multiParams("splat")
|
val Seq(origin, forked) = multiParams("splat")
|
||||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
|
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
|
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||||
|
|
||||||
(getRepository(originOwner, repository.name, baseUrl),
|
(for(
|
||||||
getRepository(forkedOwner, repository.name, baseUrl)) match {
|
originRepositoryName <- if(originOwner == forkedOwner){
|
||||||
case (Some(originRepository), Some(forkedRepository)) => {
|
Some(forkedRepository.name)
|
||||||
withGit(
|
} else {
|
||||||
getRepositoryDir(originOwner, repository.name),
|
forkedRepository.repository.originRepositoryName.orElse {
|
||||||
getRepositoryDir(forkedOwner, repository.name)
|
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||||
){ 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,
|
|
||||||
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch),
|
|
||||||
repository,
|
|
||||||
originRepository,
|
|
||||||
forkedRepository,
|
|
||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
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 { forkedRepository =>
|
||||||
|
val Seq(origin, forked) = multiParams("splat")
|
||||||
|
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||||
|
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||||
|
|
||||||
|
(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))
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||||
@@ -295,15 +332,15 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
issueId = issueId,
|
issueId = issueId,
|
||||||
originBranch = form.targetBranch,
|
originBranch = form.targetBranch,
|
||||||
requestUserName = form.requestUserName,
|
requestUserName = form.requestUserName,
|
||||||
requestRepositoryName = repository.name,
|
requestRepositoryName = form.requestRepositoryName,
|
||||||
requestBranch = form.requestBranch,
|
requestBranch = form.requestBranch,
|
||||||
commitIdFrom = form.commitIdFrom,
|
commitIdFrom = form.commitIdFrom,
|
||||||
commitIdTo = form.commitIdTo)
|
commitIdTo = form.commitIdTo)
|
||||||
|
|
||||||
// fetch requested branch
|
// fetch requested branch
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
git.fetch
|
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"))
|
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
|
||||||
.call
|
.call
|
||||||
}
|
}
|
||||||
@@ -320,16 +357,58 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles w Git object simultaneously.
|
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||||
*/
|
*/
|
||||||
private def withGit[T](oldDir: java.io.File, newDir: java.io.File)(action: (Git, Git) => T): T = {
|
private def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||||
val oldGit = Git.open(oldDir)
|
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
||||||
val newGit = Git.open(newDir)
|
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
|
||||||
try {
|
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
||||||
action(oldGit, newGit)
|
val remoteRefName = s"refs/heads/${branch}"
|
||||||
} finally {
|
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
||||||
oldGit.getRepository.close
|
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||||
newGit.getRepository.close
|
try {
|
||||||
|
// fetch objects from origin repository branch
|
||||||
|
git.fetch
|
||||||
|
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
||||||
|
.setRefSpecs(refSpec)
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,23 +438,22 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
|
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
|
||||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
|
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
|
||||||
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
|
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
|
||||||
existsCommitId(userName, repositoryName, commit.getName) &&
|
existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
|
||||||
JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
|
|
||||||
}.head.id
|
}.head.id
|
||||||
|
|
||||||
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
|
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
|
||||||
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
|
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
|
||||||
|
|
||||||
withGit(
|
using(
|
||||||
getRepositoryDir(userName, repositoryName),
|
Git.open(getRepositoryDir(userName, repositoryName)),
|
||||||
getRepositoryDir(requestUserName, requestRepositoryName)
|
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
|
||||||
){ (oldGit, newGit) =>
|
){ (oldGit, newGit) =>
|
||||||
val oldId = oldGit.getRepository.resolve(branch)
|
val oldId = oldGit.getRepository.resolve(branch)
|
||||||
val newId = newGit.getRepository.resolve(requestCommitId)
|
val newId = newGit.getRepository.resolve(requestCommitId)
|
||||||
|
|
||||||
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
|
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
|
||||||
new CommitInfo(revCommit)
|
new CommitInfo(revCommit)
|
||||||
}.toList.splitWith{ (commit1, commit2) =>
|
}.toList.splitWith { (commit1, commit2) =>
|
||||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,31 +463,29 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = {
|
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
|
||||||
val owner = repository.owner
|
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||||
val repoName = repository.name
|
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
|
||||||
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
|
val page = IssueSearchCondition.page(request)
|
||||||
val page = IssueSearchCondition.page(request)
|
val sessionKey = Keys.Session.Pulls(owner, repoName)
|
||||||
val sessionKey = s"${owner}/${repoName}/pulls"
|
|
||||||
|
|
||||||
// retrieve search condition
|
// retrieve search condition
|
||||||
val condition = if(request.getQueryString == null){
|
val condition = session.putAndGet(sessionKey,
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
if(request.hasQueryString) IssueSearchCondition(request)
|
||||||
} else IssueSearchCondition(request)
|
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||||
|
)
|
||||||
|
|
||||||
session.put(sessionKey, condition)
|
pulls.html.list(
|
||||||
|
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||||
pulls.html.list(
|
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
|
||||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
userName,
|
||||||
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
|
page,
|
||||||
userName,
|
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
|
||||||
page,
|
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
|
||||||
countIssue(condition.copy(state = "open"), filterUser, true, owner -> repoName),
|
countIssue(condition, Map.empty, true, owner -> repoName),
|
||||||
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
|
condition,
|
||||||
countIssue(condition, Map.empty, true, owner -> repoName),
|
repository,
|
||||||
condition,
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
repository,
|
}
|
||||||
hasWritePermission(owner, repoName, context.loginAccount))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,51 @@ import util.Directory._
|
|||||||
import util.{UsersAuthenticator, OwnerAuthenticator}
|
import util.{UsersAuthenticator, OwnerAuthenticator}
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.scalatra.FlashMapSupport
|
import org.scalatra.i18n.Messages
|
||||||
|
import service.WebHookService.WebHookPayload
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
import util.ControlUtil._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
|
||||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||||
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator
|
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 OwnerAuthenticator with UsersAuthenticator =>
|
self: RepositoryService with AccountService with WebHookService
|
||||||
|
with OwnerAuthenticator with UsersAuthenticator =>
|
||||||
|
|
||||||
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
// for repository options
|
||||||
|
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
||||||
|
|
||||||
val optionsForm = mapping(
|
val optionsForm = mapping(
|
||||||
"description" -> trim(label("Description" , optional(text()))),
|
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
|
||||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
"description" -> trim(label("Description" , optional(text()))),
|
||||||
"isPrivate" -> trim(label("Repository Type", boolean()))
|
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
||||||
|
"isPrivate" -> trim(label("Repository Type", boolean()))
|
||||||
)(OptionsForm.apply)
|
)(OptionsForm.apply)
|
||||||
|
|
||||||
|
// for collaborator addition
|
||||||
case class CollaboratorForm(userName: String)
|
case class CollaboratorForm(userName: String)
|
||||||
|
|
||||||
val collaboratorForm = mapping(
|
val collaboratorForm = mapping(
|
||||||
"userName" -> trim(label("Username", text(required, collaborator)))
|
"userName" -> trim(label("Username", text(required, collaborator)))
|
||||||
)(CollaboratorForm.apply)
|
)(CollaboratorForm.apply)
|
||||||
|
|
||||||
|
// for web hook url addition
|
||||||
|
case class WebHookForm(url: String)
|
||||||
|
|
||||||
|
val webHookForm = mapping(
|
||||||
|
"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.
|
* Redirect to the Options page.
|
||||||
*/
|
*/
|
||||||
@@ -54,8 +77,21 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
|||||||
repository.repository.isPrivate
|
repository.repository.isPrivate
|
||||||
} getOrElse form.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."
|
flash += "info" -> "Repository settings has been updated."
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/options")
|
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,10 +125,78 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the delete repository page.
|
* Display the web hook page.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/settings/delete")(ownerOnly {
|
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
|
||||||
settings.html.delete(_)
|
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the web hook URL.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
|
||||||
|
addWebHookURL(repository.owner, repository.name, form.url)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the web hook URL.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
|
||||||
|
deleteWebHookURL(repository.owner, repository.name, params("url"))
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the test request to registered web hook URLs.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
val commits = git.log
|
||||||
|
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
||||||
|
.setMaxCount(3)
|
||||||
|
.call.iterator.asScala.map(new CommitInfo(_))
|
||||||
|
|
||||||
|
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!"
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the danger zone.
|
||||||
|
*/
|
||||||
|
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}")
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,19 +212,55 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
|||||||
redirect(s"/${repository.owner}")
|
redirect(s"/${repository.owner}")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides duplication check for web hook url.
|
||||||
|
*/
|
||||||
|
private def webHook: Constraint = new Constraint(){
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides Constraint to validate the collaborator name.
|
* Provides Constraint to validate the collaborator name.
|
||||||
*/
|
*/
|
||||||
private def collaborator: Constraint = new Constraint(){
|
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] =
|
||||||
val paths = request.getRequestURI.split("/")
|
|
||||||
getAccountByUserName(value) match {
|
getAccountByUserName(value) match {
|
||||||
case None => Some("User does not exist.")
|
case None => Some("User does not exist.")
|
||||||
case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName))
|
case Some(x) if(x.isGroupAccount)
|
||||||
|
=> Some("User does not exist.")
|
||||||
|
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
|
||||||
=> Some("User can access this repository already.")
|
=> Some("User can access this repository already.")
|
||||||
case _ => None
|
case _ => None
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@ package app
|
|||||||
|
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import util.Implicits._
|
import util.Implicits._
|
||||||
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
|
import util.ControlUtil._
|
||||||
|
import _root_.util._
|
||||||
import service._
|
import service._
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -10,15 +11,17 @@ import org.eclipse.jgit.api.Git
|
|||||||
import org.eclipse.jgit.lib._
|
import org.eclipse.jgit.lib._
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.eclipse.jgit.treewalk._
|
import org.eclipse.jgit.treewalk._
|
||||||
|
import java.util.zip.{ZipEntry, ZipOutputStream}
|
||||||
|
import scala.Some
|
||||||
|
|
||||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||||
with RepositoryService with AccountService with ReferrerAuthenticator
|
with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The repository viewer.
|
* The repository viewer.
|
||||||
*/
|
*/
|
||||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with ReferrerAuthenticator =>
|
self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns converted HTML from Markdown for preview.
|
* Returns converted HTML from Markdown for preview.
|
||||||
@@ -36,7 +39,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
get("/:owner/:repository")(referrersOnly {
|
get("/:owner/:repository")(referrersOnly {
|
||||||
fileList(_)
|
fileList(_)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the file list of the specified path and branch.
|
* Displays the file list of the specified path and branch.
|
||||||
*/
|
*/
|
||||||
@@ -48,15 +51,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
fileList(repository, id, path)
|
fileList(repository, id, path)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the commit list of the specified resource.
|
* Displays the commit list of the specified resource.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
|
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
|
||||||
val (branchName, path) = splitPath(repository, multiParams("splat").head)
|
val (branchName, path) = splitPath(repository, multiParams("splat").head)
|
||||||
val page = params.getOrElse("page", "1").toInt
|
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
|
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
|
||||||
case Right((logs, hasNext)) =>
|
case Right((logs, hasNext)) =>
|
||||||
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
||||||
@@ -75,7 +78,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||||
val raw = params.get("raw").getOrElse("false").toBoolean
|
val raw = params.get("raw").getOrElse("false").toBoolean
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||||
|
|
||||||
@scala.annotation.tailrec
|
@scala.annotation.tailrec
|
||||||
@@ -84,19 +87,18 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
case true => getPathObjectId(path, walk)
|
case true => getPathObjectId(path, walk)
|
||||||
}
|
}
|
||||||
|
|
||||||
val treeWalk = new TreeWalk(git.getRepository)
|
val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||||
val objectId = try {
|
|
||||||
treeWalk.addTree(revCommit.getTree)
|
treeWalk.addTree(revCommit.getTree)
|
||||||
treeWalk.setRecursive(true)
|
treeWalk.setRecursive(true)
|
||||||
getPathObjectId(path, treeWalk)
|
getPathObjectId(path, treeWalk)
|
||||||
} finally {
|
|
||||||
treeWalk.release
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(raw){
|
if(raw){
|
||||||
// Download
|
// Download
|
||||||
contentType = "application/octet-stream"
|
defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
|
||||||
JGitUtil.getContent(git, objectId, false).get
|
contentType = FileUtil.getContentType(path, bytes)
|
||||||
|
bytes
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Viewer
|
// Viewer
|
||||||
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
|
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
|
||||||
@@ -120,39 +122,54 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays details of the specified commit.
|
* Displays details of the specified commit.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
||||||
val id = params("id")
|
val id = params("id")
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
|
||||||
|
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
|
||||||
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
|
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||||
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
repository, diffs, oldCommitId)
|
||||||
repository, diffs, oldCommitId)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays branches.
|
* Displays branches.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
// retrieve latest update date of each branch
|
// retrieve latest update date of each branch
|
||||||
val branchInfo = repository.branchList.map { branchName =>
|
val branchInfo = repository.branchList.map { branchName =>
|
||||||
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
|
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
|
||||||
(branchName, revCommit.getCommitterIdent.getWhen)
|
(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.
|
* Displays tags.
|
||||||
*/
|
*/
|
||||||
@@ -165,7 +182,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
*/
|
*/
|
||||||
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
|
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
|
||||||
val name = params("name")
|
val name = params("name")
|
||||||
|
|
||||||
if(name.endsWith(".zip")){
|
if(name.endsWith(".zip")){
|
||||||
val revision = name.replaceFirst("\\.zip$", "")
|
val revision = name.replaceFirst("\\.zip$", "")
|
||||||
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
||||||
@@ -173,26 +190,38 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
FileUtils.deleteDirectory(workDir)
|
FileUtils.deleteDirectory(workDir)
|
||||||
}
|
}
|
||||||
workDir.mkdirs
|
workDir.mkdirs
|
||||||
|
|
||||||
// clone the repository
|
val zipFile = new File(workDir, repository.name + "-" +
|
||||||
val cloneDir = new File(workDir, revision)
|
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
|
||||||
JGitUtil.withGit(Git.cloneRepository
|
|
||||||
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString)
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
.setDirectory(cloneDir)
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||||
.call){ git =>
|
using(new TreeWalk(git.getRepository)){ walk =>
|
||||||
|
val reader = walk.getObjectReader
|
||||||
// checkout the specified revision
|
val objectId = new MutableObjectId
|
||||||
git.checkout.setName(revision).call
|
|
||||||
|
using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out =>
|
||||||
|
walk.addTree(revCommit.getTree)
|
||||||
|
walk.setRecursive(true)
|
||||||
|
|
||||||
|
while(walk.next){
|
||||||
|
val name = walk.getPathString
|
||||||
|
val mode = walk.getFileMode(0)
|
||||||
|
if(mode != FileMode.TREE){
|
||||||
|
walk.getObjectId(objectId, 0)
|
||||||
|
val entry = new ZipEntry(name)
|
||||||
|
val loader = reader.open(objectId)
|
||||||
|
entry.setSize(loader.getSize)
|
||||||
|
out.putNextEntry(entry)
|
||||||
|
loader.copyTo(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove .git
|
|
||||||
FileUtils.deleteDirectory(new File(cloneDir, ".git"))
|
|
||||||
|
|
||||||
// create zip file
|
|
||||||
val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
|
|
||||||
FileUtil.createZipFile(zipFile, cloneDir)
|
|
||||||
|
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
|
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
|
||||||
zipFile
|
zipFile
|
||||||
} else {
|
} else {
|
||||||
BadRequest
|
BadRequest
|
||||||
@@ -221,6 +250,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
(id, path.substring(id.length).replaceFirst("^/", ""))
|
(id, path.substring(id.length).replaceFirst("^/", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val readmeFiles = Seq("readme.md", "readme.markdown")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides HTML of the file list.
|
* Provides HTML of the file list.
|
||||||
*
|
*
|
||||||
@@ -233,26 +265,28 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
if(repository.commitCount == 0){
|
if(repository.commitCount == 0){
|
||||||
repo.html.guide(repository)
|
repo.html.guide(repository)
|
||||||
} else {
|
} else {
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
|
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
|
||||||
// get specified commit
|
// get specified commit
|
||||||
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
|
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, objectId)
|
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
|
||||||
|
|
||||||
// get files
|
// get files
|
||||||
val files = JGitUtil.getFileList(git, revision, path)
|
val files = JGitUtil.getFileList(git, revision, path)
|
||||||
// process README.md
|
// process README.md or README.markdown
|
||||||
val readme = files.find(_.name == "README.md").map { file =>
|
val readme = files.find { file =>
|
||||||
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
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,
|
repo.html.files(revision, repository,
|
||||||
if(path == ".") Nil else path.split("/").toList, // current path
|
if(path == ".") Nil else path.split("/").toList, // current path
|
||||||
new JGitUtil.CommitInfo(revCommit), // latest commit
|
new JGitUtil.CommitInfo(revCommit), // latest commit
|
||||||
files, readme)
|
files, readme)
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import util._
|
import util._
|
||||||
|
import ControlUtil._
|
||||||
import service._
|
import service._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
class SearchController extends SearchControllerBase
|
class SearchController extends SearchControllerBase
|
||||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
|
||||||
with RepositorySearchService with IssuesService
|
|
||||||
with ReferrerAuthenticator
|
|
||||||
|
|
||||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||||
with SystemSettingsService with ActivityService with RepositorySearchService
|
with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
|
||||||
with ReferrerAuthenticator =>
|
|
||||||
|
|
||||||
val searchForm = mapping(
|
val searchForm = mapping(
|
||||||
"query" -> trim(text(required)),
|
"query" -> trim(text(required)),
|
||||||
@@ -26,25 +24,25 @@ trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
get("/:owner/:repository/search")(referrersOnly { repository =>
|
get("/:owner/:repository/search")(referrersOnly { repository =>
|
||||||
val query = params("q").trim
|
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
|
||||||
val target = params.getOrElse("type", "code")
|
val page = try {
|
||||||
val page = try {
|
val i = params.getOrElse("page", "1").toInt
|
||||||
val i = params.getOrElse("page", "1").toInt
|
if(i <= 0) 1 else i
|
||||||
if(i <= 0) 1 else i
|
} catch {
|
||||||
} catch {
|
case e: NumberFormatException => 1
|
||||||
case e: NumberFormatException => 1
|
}
|
||||||
}
|
|
||||||
|
|
||||||
target.toLowerCase match {
|
target.toLowerCase match {
|
||||||
case "issue" => search.html.issues(
|
case "issue" => search.html.issues(
|
||||||
searchIssues(repository.owner, repository.name, query),
|
searchIssues(repository.owner, repository.name, query),
|
||||||
countFiles(repository.owner, repository.name, query),
|
countFiles(repository.owner, repository.name, query),
|
||||||
query, page, repository)
|
query, page, repository)
|
||||||
|
|
||||||
case _ => search.html.code(
|
case _ => search.html.code(
|
||||||
searchFiles(repository.owner, repository.name, query),
|
searchFiles(repository.owner, repository.name, query),
|
||||||
countIssues(repository.owner, repository.name, query),
|
countIssues(repository.owner, repository.name, query),
|
||||||
query, page, repository)
|
query, page, repository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
|
|
||||||
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("REDIRECT", redirect.get)
|
|
||||||
}
|
|
||||||
html.signin(loadSystemSettings())
|
|
||||||
}
|
|
||||||
|
|
||||||
post("/signin", form){ form =>
|
|
||||||
val settings = loadSystemSettings()
|
|
||||||
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("LOGIN_ACCOUNT", account)
|
|
||||||
updateLastLoginDate(account.userName)
|
|
||||||
|
|
||||||
session.get("REDIRECT").map { redirectUrl =>
|
|
||||||
session.removeAttribute("REDIRECT")
|
|
||||||
redirect(redirectUrl.asInstanceOf[String])
|
|
||||||
}.getOrElse {
|
|
||||||
redirect("/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,15 @@ import service.{AccountService, SystemSettingsService}
|
|||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
import util.AdminAuthenticator
|
import util.AdminAuthenticator
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import org.scalatra.FlashMapSupport
|
|
||||||
|
|
||||||
class SystemSettingsController extends SystemSettingsControllerBase
|
class SystemSettingsController extends SystemSettingsControllerBase
|
||||||
with SystemSettingsService with AccountService with AdminAuthenticator
|
with SystemSettingsService with AccountService with AdminAuthenticator
|
||||||
|
|
||||||
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
trait SystemSettingsControllerBase extends ControllerBase {
|
||||||
self: SystemSettingsService with AccountService with AdminAuthenticator =>
|
self: SystemSettingsService with AccountService with AdminAuthenticator =>
|
||||||
|
|
||||||
private val form = mapping(
|
private val form = mapping(
|
||||||
|
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||||
"notification" -> trim(label("Notification", boolean())),
|
"notification" -> trim(label("Notification", boolean())),
|
||||||
@@ -21,7 +21,9 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
|||||||
"port" -> trim(label("SMTP Port", optional(number()))),
|
"port" -> trim(label("SMTP Port", optional(number()))),
|
||||||
"user" -> trim(label("SMTP User", optional(text()))),
|
"user" -> trim(label("SMTP User", optional(text()))),
|
||||||
"password" -> trim(label("SMTP Password", optional(text()))),
|
"password" -> trim(label("SMTP Password", optional(text()))),
|
||||||
"ssl" -> trim(label("Enable SSL", optional(boolean())))
|
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
|
||||||
|
"fromAddress" -> trim(label("FROM Address", optional(text()))),
|
||||||
|
"fromName" -> trim(label("FROM Name", optional(text())))
|
||||||
)(Smtp.apply)),
|
)(Smtp.apply)),
|
||||||
"ldapAuthentication" -> trim(label("LDAP", boolean())),
|
"ldapAuthentication" -> trim(label("LDAP", boolean())),
|
||||||
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
|
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
|
||||||
@@ -31,7 +33,10 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
|||||||
"bindPassword" -> trim(label("Bind Password", optional(text()))),
|
"bindPassword" -> trim(label("Bind Password", optional(text()))),
|
||||||
"baseDN" -> trim(label("Base DN", text(required))),
|
"baseDN" -> trim(label("Base DN", text(required))),
|
||||||
"userNameAttribute" -> trim(label("User name attribute", text(required))),
|
"userNameAttribute" -> trim(label("User name attribute", text(required))),
|
||||||
"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))
|
)(Ldap.apply))
|
||||||
)(SystemSettings.apply)
|
)(SystemSettings.apply)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package app
|
|||||||
import service._
|
import service._
|
||||||
import util.AdminAuthenticator
|
import util.AdminAuthenticator
|
||||||
import util.StringUtil._
|
import util.StringUtil._
|
||||||
|
import util.ControlUtil._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import util.Directory._
|
||||||
|
|
||||||
class UserManagementController extends UserManagementControllerBase
|
class UserManagementController extends UserManagementControllerBase
|
||||||
with AccountService with RepositoryService with AdminAuthenticator
|
with AccountService with RepositoryService with AdminAuthenticator
|
||||||
@@ -11,58 +14,66 @@ class UserManagementController extends UserManagementControllerBase
|
|||||||
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||||
|
|
||||||
case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
|
case class NewUserForm(userName: String, password: String, fullName: String,
|
||||||
|
mailAddress: String, isAdmin: Boolean,
|
||||||
url: Option[String], fileId: Option[String])
|
url: Option[String], fileId: Option[String])
|
||||||
|
|
||||||
case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
|
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||||
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],
|
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||||
memberNames: Option[String])
|
memberNames: Option[String])
|
||||||
|
|
||||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||||
memberNames: Option[String], clearImage: Boolean)
|
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||||
|
|
||||||
val newUserForm = mapping(
|
val newUserForm = mapping(
|
||||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||||
"fileId" -> trim(label("File ID" , optional(text())))
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||||
)(NewUserForm.apply)
|
)(NewUserForm.apply)
|
||||||
|
|
||||||
val editUserForm = mapping(
|
val editUserForm = mapping(
|
||||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
||||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
||||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
|
"removed" -> trim(label("Disable" ,boolean()))
|
||||||
)(EditUserForm.apply)
|
)(EditUserForm.apply)
|
||||||
|
|
||||||
val newGroupForm = mapping(
|
val newGroupForm = mapping(
|
||||||
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
"memberNames" -> trim(label("Member Names" , optional(text())))
|
"memberNames" -> trim(label("Member Names" ,optional(text())))
|
||||||
)(NewGroupForm.apply)
|
)(NewGroupForm.apply)
|
||||||
|
|
||||||
val editGroupForm = mapping(
|
val editGroupForm = mapping(
|
||||||
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
"memberNames" -> trim(label("Member Names" , optional(text()))),
|
"memberNames" -> trim(label("Member Names" ,optional(text()))),
|
||||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
|
"removed" -> trim(label("Disable" ,boolean()))
|
||||||
)(EditGroupForm.apply)
|
)(EditGroupForm.apply)
|
||||||
|
|
||||||
get("/admin/users")(adminOnly {
|
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) =>
|
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||||
account.userName -> getGroupMembers(account.userName)
|
account.userName -> getGroupMembers(account.userName)
|
||||||
}.toMap
|
}.toMap
|
||||||
admin.users.html.list(users, members)
|
admin.users.html.list(users, members, includeRemoved)
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/users/_newuser")(adminOnly {
|
get("/admin/users/_newuser")(adminOnly {
|
||||||
@@ -70,24 +81,39 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||||
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
|
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
||||||
updateImage(form.userName, form.fileId, false)
|
updateImage(form.userName, form.fileId, false)
|
||||||
redirect("/admin/users")
|
redirect("/admin/users")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||||
val userName = params("userName")
|
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 =>
|
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).map { account =>
|
getAccountByUserName(userName, true).map { account =>
|
||||||
updateAccount(getAccountByUserName(userName).get.copy(
|
|
||||||
|
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),
|
password = form.password.map(sha1).getOrElse(account.password),
|
||||||
|
fullName = form.fullName,
|
||||||
mailAddress = form.mailAddress,
|
mailAddress = form.mailAddress,
|
||||||
isAdmin = form.isAdmin,
|
isAdmin = form.isAdmin,
|
||||||
url = form.url))
|
url = form.url,
|
||||||
|
isRemoved = form.isRemoved))
|
||||||
|
|
||||||
updateImage(userName, form.fileId, form.clearImage)
|
updateImage(userName, form.fileId, form.clearImage)
|
||||||
redirect("/admin/users")
|
redirect("/admin/users")
|
||||||
@@ -107,33 +133,47 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||||
val groupName = params("groupName")
|
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 =>
|
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||||
val groupName = params("groupName")
|
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
|
||||||
getAccountByUserName(groupName).map { account =>
|
getAccountByUserName(groupName, true).map { account =>
|
||||||
updateGroup(groupName, form.url)
|
updateGroup(groupName, form.url, form.isRemoved)
|
||||||
|
|
||||||
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil)
|
if(form.isRemoved){
|
||||||
updateGroupMembers(form.groupName, memberNames)
|
// Remove from GROUP_MEMBER
|
||||||
|
updateGroupMembers(form.groupName, Nil)
|
||||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
// Remove repositories
|
||||||
removeCollaborators(form.groupName, repositoryName)
|
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||||
memberNames.foreach { userName =>
|
deleteRepository(groupName, repositoryName)
|
||||||
addCollaborator(form.groupName, repositoryName, userName)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
updateImage(form.groupName, form.fileId, form.clearImage)
|
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||||
redirect("/admin/users")
|
redirect("/admin/users")
|
||||||
|
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/admin/users/_usercheck")(adminOnly {
|
post("/admin/users/_usercheck")(adminOnly {
|
||||||
getAccountByUserName(params("userName")).isDefined
|
getAccountByUserName(params("userName")).isDefined
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
|
import util._
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
import scala.Some
|
||||||
|
import java.util.ResourceBundle
|
||||||
|
|
||||||
class WikiController extends WikiControllerBase
|
class WikiController extends WikiControllerBase
|
||||||
with WikiService with RepositoryService with AccountService with ActivityService
|
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||||
with CollaboratorsAuthenticator with ReferrerAuthenticator
|
|
||||||
|
|
||||||
trait WikiControllerBase extends ControllerBase {
|
trait WikiControllerBase extends ControllerBase {
|
||||||
self: WikiService with RepositoryService with ActivityService
|
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
||||||
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
|
||||||
|
|
||||||
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
|
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
|
||||||
|
|
||||||
val newForm = mapping(
|
val newForm = mapping(
|
||||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
|
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
|
||||||
"content" -> trim(label("Content" , text(required))),
|
"content" -> trim(label("Content" , text(required, conflictForNew))),
|
||||||
"message" -> trim(label("Message" , optional(text()))),
|
"message" -> trim(label("Message" , optional(text()))),
|
||||||
"currentPageName" -> trim(label("Current page name" , text()))
|
"currentPageName" -> trim(label("Current page name" , text())),
|
||||||
|
"id" -> trim(label("Latest commit id" , text()))
|
||||||
)(WikiPageEditForm.apply)
|
)(WikiPageEditForm.apply)
|
||||||
|
|
||||||
val editForm = mapping(
|
val editForm = mapping(
|
||||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
|
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
|
||||||
"content" -> trim(label("Content" , text(required))),
|
"content" -> trim(label("Content" , text(required, conflictForEdit))),
|
||||||
"message" -> trim(label("Message" , optional(text()))),
|
"message" -> trim(label("Message" , optional(text()))),
|
||||||
"currentPageName" -> trim(label("Current page name" , text(required)))
|
"currentPageName" -> trim(label("Current page name" , text(required))),
|
||||||
|
"id" -> trim(label("Latest commit id" , text(required)))
|
||||||
)(WikiPageEditForm.apply)
|
)(WikiPageEditForm.apply)
|
||||||
|
|
||||||
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
||||||
@@ -40,13 +45,13 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
||||||
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode
|
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
|
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
|
||||||
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
|
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
|
||||||
case Left(_) => NotFound
|
case Left(_) => NotFound
|
||||||
@@ -56,36 +61,60 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
val commitId = params("commitId").split("\\.\\.\\.")
|
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), 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"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
|
||||||
val commitId = params("commitId").split("\\.\\.\\.")
|
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
|
wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository =>
|
||||||
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
|
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||||
|
|
||||||
|
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
|
||||||
|
} else {
|
||||||
|
flash += "info" -> "This patch was not able to be reversed."
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository =>
|
||||||
|
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/")
|
||||||
|
} else {
|
||||||
|
flash += "info" -> "This patch was not able to be reversed."
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
|
||||||
val loginAccount = context.loginAccount.get
|
defining(context.loginAccount.get){ loginAccount =>
|
||||||
|
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
||||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
|
||||||
form.content, loginAccount, form.message.getOrElse("")).map { commitId =>
|
updateLastActivityDate(repository.owner, repository.name)
|
||||||
updateLastActivityDate(repository.owner, repository.name)
|
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
|
||||||
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
|
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
|
||||||
@@ -93,25 +122,26 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
|
||||||
val loginAccount = context.loginAccount.get
|
defining(context.loginAccount.get){ loginAccount =>
|
||||||
|
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
||||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
form.content, loginAccount, form.message.getOrElse(""), None)
|
||||||
form.content, context.loginAccount.get, form.message.getOrElse(""))
|
|
||||||
|
|
||||||
updateLastActivityDate(repository.owner, repository.name)
|
|
||||||
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
|
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
updateLastActivityDate(repository.owner, repository.name)
|
||||||
|
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
|
||||||
|
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
val account = context.loginAccount.get
|
|
||||||
|
|
||||||
deleteWikiPage(repository.owner, repository.name, pageName, account.userName, account.mailAddress, s"Delete ${pageName}")
|
|
||||||
updateLastActivityDate(repository.owner, repository.name)
|
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/wiki")
|
defining(context.loginAccount.get){ loginAccount =>
|
||||||
|
deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}")
|
||||||
|
updateLastActivityDate(repository.owner, repository.name)
|
||||||
|
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
|
||||||
@@ -120,7 +150,7 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
JGitUtil.getCommitLog(git, "master") match {
|
JGitUtil.getCommitLog(git, "master") match {
|
||||||
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
|
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
|
||||||
case Left(_) => NotFound
|
case Left(_) => NotFound
|
||||||
@@ -129,19 +159,21 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
|
||||||
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content =>
|
val path = multiParams("splat").head
|
||||||
contentType = "application/octet-stream"
|
|
||||||
content
|
getFileContent(repository.owner, repository.name, path).map { bytes =>
|
||||||
|
contentType = FileUtil.getContentType(path, bytes)
|
||||||
|
bytes
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
private def unique: Constraint = new Constraint(){
|
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.")
|
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private def pagename: Constraint = new Constraint(){
|
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(_))){
|
if(value.exists("\\/:*?\"<>|".contains(_))){
|
||||||
Some(s"${name} contains invalid character.")
|
Some(s"${name} contains invalid character.")
|
||||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||||
@@ -151,5 +183,22 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def conflictForNew: Constraint = new Constraint(){
|
||||||
|
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, 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import scala.slick.driver.H2Driver.simple._
|
|||||||
|
|
||||||
object Accounts extends Table[Account]("ACCOUNT") {
|
object Accounts extends Table[Account]("ACCOUNT") {
|
||||||
def userName = column[String]("USER_NAME", O PrimaryKey)
|
def userName = column[String]("USER_NAME", O PrimaryKey)
|
||||||
|
def fullName = column[String]("FULL_NAME")
|
||||||
def mailAddress = column[String]("MAIL_ADDRESS")
|
def mailAddress = column[String]("MAIL_ADDRESS")
|
||||||
def password = column[String]("PASSWORD")
|
def password = column[String]("PASSWORD")
|
||||||
def isAdmin = column[Boolean]("ADMINISTRATOR")
|
def isAdmin = column[Boolean]("ADMINISTRATOR")
|
||||||
@@ -13,11 +14,13 @@ object Accounts extends Table[Account]("ACCOUNT") {
|
|||||||
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
||||||
def image = column[String]("IMAGE")
|
def image = column[String]("IMAGE")
|
||||||
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
||||||
def * = userName ~ 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(
|
case class Account(
|
||||||
userName: String,
|
userName: String,
|
||||||
|
fullName: String,
|
||||||
mailAddress: String,
|
mailAddress: String,
|
||||||
password: String,
|
password: String,
|
||||||
isAdmin: Boolean,
|
isAdmin: Boolean,
|
||||||
@@ -26,5 +29,6 @@ case class Account(
|
|||||||
updatedDate: java.util.Date,
|
updatedDate: java.util.Date,
|
||||||
lastLoginDate: Option[java.util.Date],
|
lastLoginDate: Option[java.util.Date],
|
||||||
image: Option[String],
|
image: Option[String],
|
||||||
isGroupAccount: Boolean
|
isGroupAccount: Boolean,
|
||||||
|
isRemoved: Boolean
|
||||||
)
|
)
|
||||||
|
|||||||
16
src/main/scala/model/WebHook.scala
Normal file
16
src/main/scala/model/WebHook.scala
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import scala.slick.driver.H2Driver.simple._
|
||||||
|
|
||||||
|
object WebHooks extends Table[WebHook]("WEB_HOOK") with BasicTemplate {
|
||||||
|
def url = column[String]("URL")
|
||||||
|
def * = userName ~ repositoryName ~ url <> (WebHook, WebHook.unapply _)
|
||||||
|
|
||||||
|
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class WebHook(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
url: String
|
||||||
|
)
|
||||||
@@ -9,9 +9,12 @@ import model.GroupMember
|
|||||||
import scala.Some
|
import scala.Some
|
||||||
import model.Account
|
import model.Account
|
||||||
import util.LDAPUtil
|
import util.LDAPUtil
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
trait AccountService {
|
trait AccountService {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(classOf[AccountService])
|
||||||
|
|
||||||
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
|
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
|
||||||
if(settings.ldapAuthentication){
|
if(settings.ldapAuthentication){
|
||||||
ldapAuthentication(settings, userName, password)
|
ldapAuthentication(settings, userName, password)
|
||||||
@@ -33,30 +36,43 @@ trait AccountService {
|
|||||||
*/
|
*/
|
||||||
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
|
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
|
||||||
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
|
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
|
||||||
case Right(mailAddress) => {
|
case Right(ldapUserInfo) => {
|
||||||
// Create or update account by LDAP information
|
// Create or update account by LDAP information
|
||||||
getAccountByUserName(userName) match {
|
getAccountByUserName(userName, true) match {
|
||||||
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
|
case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
|
||||||
case None => createAccount(userName, "", mailAddress, false, None)
|
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)
|
getAccountByUserName(userName)
|
||||||
}
|
}
|
||||||
case Left(errorMessage) => defaultAuthentication(userName, password)
|
case Left(errorMessage) => {
|
||||||
|
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
|
||||||
|
defaultAuthentication(userName, password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getAccountByUserName(userName: String): Option[Account] =
|
def getAccountByUserName(userName: String, includeRemoved: Boolean = false): Option[Account] =
|
||||||
Query(Accounts) filter(_.userName is userName.bind) firstOption
|
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
|
||||||
|
|
||||||
def getAccountByMailAddress(mailAddress: String): Option[Account] =
|
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
|
||||||
Query(Accounts) filter(_.mailAddress is mailAddress.bind) firstOption
|
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, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
|
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
|
||||||
Accounts insert Account(
|
Accounts insert Account(
|
||||||
userName = userName,
|
userName = userName,
|
||||||
password = password,
|
password = password,
|
||||||
|
fullName = fullName,
|
||||||
mailAddress = mailAddress,
|
mailAddress = mailAddress,
|
||||||
isAdmin = isAdmin,
|
isAdmin = isAdmin,
|
||||||
url = url,
|
url = url,
|
||||||
@@ -64,20 +80,23 @@ trait AccountService {
|
|||||||
updatedDate = currentDate,
|
updatedDate = currentDate,
|
||||||
lastLoginDate = None,
|
lastLoginDate = None,
|
||||||
image = None,
|
image = None,
|
||||||
isGroupAccount = false)
|
isGroupAccount = false,
|
||||||
|
isRemoved = false)
|
||||||
|
|
||||||
def updateAccount(account: Account): Unit =
|
def updateAccount(account: Account): Unit =
|
||||||
Accounts
|
Accounts
|
||||||
.filter { a => a.userName is account.userName.bind }
|
.filter { a => a.userName is account.userName.bind }
|
||||||
.map { a => a.password ~ 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 (
|
.update (
|
||||||
account.password,
|
account.password,
|
||||||
|
account.fullName,
|
||||||
account.mailAddress,
|
account.mailAddress,
|
||||||
account.isAdmin,
|
account.isAdmin,
|
||||||
account.url,
|
account.url,
|
||||||
account.registeredDate,
|
account.registeredDate,
|
||||||
currentDate,
|
currentDate,
|
||||||
account.lastLoginDate)
|
account.lastLoginDate,
|
||||||
|
account.isRemoved)
|
||||||
|
|
||||||
def updateAvatarImage(userName: String, image: Option[String]): Unit =
|
def updateAvatarImage(userName: String, image: Option[String]): Unit =
|
||||||
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
|
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
|
||||||
@@ -89,6 +108,7 @@ trait AccountService {
|
|||||||
Accounts insert Account(
|
Accounts insert Account(
|
||||||
userName = groupName,
|
userName = groupName,
|
||||||
password = "",
|
password = "",
|
||||||
|
fullName = groupName,
|
||||||
mailAddress = groupName + "@devnull",
|
mailAddress = groupName + "@devnull",
|
||||||
isAdmin = false,
|
isAdmin = false,
|
||||||
url = url,
|
url = url,
|
||||||
@@ -96,10 +116,11 @@ trait AccountService {
|
|||||||
updatedDate = currentDate,
|
updatedDate = currentDate,
|
||||||
lastLoginDate = None,
|
lastLoginDate = None,
|
||||||
image = None,
|
image = None,
|
||||||
isGroupAccount = true)
|
isGroupAccount = true,
|
||||||
|
isRemoved = false)
|
||||||
|
|
||||||
def updateGroup(groupName: String, url: Option[String]): Unit =
|
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
|
||||||
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
|
Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
|
||||||
|
|
||||||
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
|
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
|
||||||
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
|
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
|
||||||
@@ -122,4 +143,12 @@ trait AccountService {
|
|||||||
.map(_.groupName)
|
.map(_.groupName)
|
||||||
.list
|
.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
|
||||||
@@ -109,11 +109,26 @@ trait ActivityService {
|
|||||||
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
|
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
|
||||||
None,
|
None,
|
||||||
currentDate)
|
currentDate)
|
||||||
|
|
||||||
|
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
|
||||||
|
tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
|
||||||
|
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||||
|
"delete_tag",
|
||||||
|
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
|
||||||
|
None,
|
||||||
|
currentDate)
|
||||||
|
|
||||||
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
|
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
|
||||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||||
"create_branch",
|
"create_branch",
|
||||||
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
|
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
|
||||||
|
None,
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
|
||||||
|
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||||
|
"delete_branch",
|
||||||
|
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
|
||||||
None,
|
None,
|
||||||
currentDate)
|
currentDate)
|
||||||
|
|
||||||
@@ -141,7 +156,13 @@ trait ActivityService {
|
|||||||
def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
|
def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
|
||||||
CommitLog insert (userName, repositoryName, commitId)
|
CommitLog insert (userName, repositoryName, commitId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) =
|
||||||
|
CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*)
|
||||||
|
|
||||||
|
def getAllCommitIds(userName: String, repositoryName: String): List[String] =
|
||||||
|
Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list
|
||||||
|
|
||||||
def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
|
def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
|
||||||
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined
|
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined
|
||||||
|
|
||||||
|
|||||||
@@ -49,12 +49,8 @@ trait IssuesService {
|
|||||||
* @return the count of the search result
|
* @return the count of the search result
|
||||||
*/
|
*/
|
||||||
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||||
repos: (String, String)*): Int = {
|
repos: (String, String)*): Int =
|
||||||
// TODO It must be _.length instead of map (_.issueId) list).length.
|
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
|
||||||
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
|
|
||||||
// https://github.com/slick/slick/issues/170
|
|
||||||
(searchIssueQuery(repos, condition, filterUser, onlyPullRequest) map (_.issueId) list).length
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Returns the Map which contains issue count for each labels.
|
* Returns the Map which contains issue count for each labels.
|
||||||
*
|
*
|
||||||
@@ -253,6 +249,9 @@ trait IssuesService {
|
|||||||
}
|
}
|
||||||
.update (content, currentDate)
|
.update (content, currentDate)
|
||||||
|
|
||||||
|
def deleteComment(commentId: Int) =
|
||||||
|
IssueComments filter (_.byPrimaryKey(commentId)) delete
|
||||||
|
|
||||||
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
|
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
|
||||||
Issues
|
Issues
|
||||||
.filter (_.byPrimaryKey(owner, repository, issueId))
|
.filter (_.byPrimaryKey(owner, repository, issueId))
|
||||||
@@ -332,7 +331,7 @@ object IssuesService {
|
|||||||
|
|
||||||
def toURL: String =
|
def toURL: String =
|
||||||
"?" + List(
|
"?" + List(
|
||||||
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
|
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
|
||||||
milestoneId.map { id => "milestone=" + (id match {
|
milestoneId.map { id => "milestone=" + (id match {
|
||||||
case Some(x) => x.toString
|
case Some(x) => x.toString
|
||||||
case None => "none"
|
case None => "none"
|
||||||
@@ -353,11 +352,11 @@ object IssuesService {
|
|||||||
|
|
||||||
def apply(request: HttpServletRequest): IssueSearchCondition =
|
def apply(request: HttpServletRequest): IssueSearchCondition =
|
||||||
IssueSearchCondition(
|
IssueSearchCondition(
|
||||||
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
|
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
|
||||||
param(request, "milestone").map(_ match {
|
param(request, "milestone").map{
|
||||||
case "none" => None
|
case "none" => None
|
||||||
case x => Some(x.toInt)
|
case x => x.toIntOpt
|
||||||
}),
|
},
|
||||||
param(request, "for"),
|
param(request, "for"),
|
||||||
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
||||||
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ package service
|
|||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
import scala.slick.driver.H2Driver.simple._
|
||||||
import Database.threadLocalSession
|
import Database.threadLocalSession
|
||||||
|
|
||||||
import model._
|
import model._
|
||||||
|
import util.ControlUtil._
|
||||||
|
|
||||||
trait PullRequestService { self: IssuesService =>
|
trait PullRequestService { self: IssuesService =>
|
||||||
import PullRequestService._
|
import PullRequestService._
|
||||||
|
|
||||||
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] = {
|
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
|
||||||
val issue = getIssue(owner, repository, issueId.toString)
|
getIssue(owner, repository, issueId.toString).flatMap{ issue =>
|
||||||
if(issue.isDefined){
|
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
|
||||||
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match {
|
pullreq => (issue, pullreq)
|
||||||
case Some(pullreq) => Some((issue.get, pullreq))
|
|
||||||
case None => None
|
|
||||||
}
|
}
|
||||||
} else None
|
}
|
||||||
}
|
|
||||||
|
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] =
|
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
|
||||||
Query(PullRequests)
|
Query(PullRequests)
|
||||||
@@ -46,6 +46,18 @@ trait PullRequestService { self: IssuesService =>
|
|||||||
commitIdFrom,
|
commitIdFrom,
|
||||||
commitIdTo))
|
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 {
|
object PullRequestService {
|
||||||
@@ -54,4 +66,4 @@ object PullRequestService {
|
|||||||
|
|
||||||
case class PullRequestCount(userName: String, count: Int)
|
case class PullRequestCount(userName: String, count: Int)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import util.{FileUtil, StringUtil, JGitUtil}
|
import util.{FileUtil, StringUtil, JGitUtil}
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
import model.Issue
|
import model.Issue
|
||||||
import org.eclipse.jgit.revwalk.RevWalk
|
import org.eclipse.jgit.revwalk.RevWalk
|
||||||
import org.eclipse.jgit.treewalk.TreeWalk
|
import org.eclipse.jgit.treewalk.TreeWalk
|
||||||
@@ -9,7 +10,8 @@ import scala.collection.mutable.ListBuffer
|
|||||||
import org.eclipse.jgit.lib.FileMode
|
import org.eclipse.jgit.lib.FileMode
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
|
|
||||||
trait RepositorySearchService { self: IssuesService =>
|
trait
|
||||||
|
RepositorySearchService { self: IssuesService =>
|
||||||
import RepositorySearchService._
|
import RepositorySearchService._
|
||||||
|
|
||||||
def countIssues(owner: String, repository: String, query: String): Int =
|
def countIssues(owner: String, repository: String, query: String): Int =
|
||||||
@@ -27,12 +29,12 @@ trait RepositorySearchService { self: IssuesService =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
def countFiles(owner: String, repository: String, query: String): Int =
|
def countFiles(owner: String, repository: String, query: String): Int =
|
||||||
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
|
using(Git.open(getRepositoryDir(owner, repository))){ git =>
|
||||||
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
|
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
|
||||||
}
|
}
|
||||||
|
|
||||||
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
|
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
|
||||||
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
|
using(Git.open(getRepositoryDir(owner, repository))){ git =>
|
||||||
if(JGitUtil.isEmpty(git)){
|
if(JGitUtil.isEmpty(git)){
|
||||||
Nil
|
Nil
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -39,6 +39,67 @@ trait RepositoryService { self: AccountService =>
|
|||||||
IssueId insert (userName, repositoryName, 0)
|
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 = {
|
def deleteRepository(userName: String, repositoryName: String): Unit = {
|
||||||
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
|
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
@@ -46,9 +107,11 @@ trait RepositoryService { self: AccountService =>
|
|||||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +181,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
case Some(x) if(x.isAdmin) => Query(Repositories)
|
case Some(x) if(x.isAdmin) => Query(Repositories)
|
||||||
// for Normal Users
|
// for Normal Users
|
||||||
case Some(x) if(!x.isAdmin) =>
|
case Some(x) if(!x.isAdmin) =>
|
||||||
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)
|
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
|
||||||
}
|
}
|
||||||
// for Guests
|
// for Guests
|
||||||
@@ -199,20 +262,17 @@ trait RepositoryService { self: AccountService =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO It must be _.length instead of map (_.issueId) list).length.
|
|
||||||
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
|
|
||||||
// https://github.com/slick/slick/issues/170
|
|
||||||
private def getForkedCount(userName: String, repositoryName: String): Int =
|
private def getForkedCount(userName: String, repositoryName: String): Int =
|
||||||
Query(Repositories).filter { t =>
|
Query(Repositories.filter { t =>
|
||||||
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
|
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
|
||||||
}.list.length
|
}.length).first
|
||||||
|
|
||||||
|
|
||||||
def getForkedRepositories(userName: String, repositoryName: String): List[String] =
|
def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] =
|
||||||
Query(Repositories).filter { t =>
|
Query(Repositories).filter { t =>
|
||||||
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
|
(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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,5 +28,10 @@ trait RequestCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
|
||||||
|
context.cache(s"account.${mailAddress}"){
|
||||||
|
new AccountService {}.getAccountByMailAddress(mailAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +1,95 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
trait SystemSettingsService {
|
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 = {
|
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||||
val props = new java.util.Properties()
|
defining(new java.util.Properties()){ props =>
|
||||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
settings.baseUrl.foreach(props.setProperty(BaseURL, _))
|
||||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||||
props.setProperty(Notification, settings.notification.toString)
|
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||||
if(settings.notification) {
|
props.setProperty(Notification, settings.notification.toString)
|
||||||
settings.smtp.foreach { smtp =>
|
if(settings.notification) {
|
||||||
props.setProperty(SmtpHost, smtp.host)
|
settings.smtp.foreach { smtp =>
|
||||||
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
props.setProperty(SmtpHost, smtp.host)
|
||||||
smtp.user.foreach(props.setProperty(SmtpUser, _))
|
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
||||||
smtp.password.foreach(props.setProperty(SmtpPassword, _))
|
smtp.user.foreach(props.setProperty(SmtpUser, _))
|
||||||
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
|
smtp.password.foreach(props.setProperty(SmtpPassword, _))
|
||||||
|
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
|
||||||
|
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
|
||||||
|
smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
|
||||||
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
|
if(settings.ldapAuthentication){
|
||||||
if(settings.ldapAuthentication){
|
settings.ldap.map { ldap =>
|
||||||
settings.ldap.map { ldap =>
|
props.setProperty(LdapHost, ldap.host)
|
||||||
props.setProperty(LdapHost, ldap.host)
|
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
|
||||||
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
|
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
|
||||||
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
|
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
|
||||||
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
|
props.setProperty(LdapBaseDN, ldap.baseDN)
|
||||||
props.setProperty(LdapBaseDN, ldap.baseDN)
|
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
||||||
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
|
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)
|
||||||
}
|
}
|
||||||
props.store(new java.io.FileOutputStream(GitBucketConf), null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def loadSystemSettings(): SystemSettings = {
|
def loadSystemSettings(): SystemSettings = {
|
||||||
val props = new java.util.Properties()
|
defining(new java.util.Properties()){ props =>
|
||||||
if(GitBucketConf.exists){
|
if(GitBucketConf.exists){
|
||||||
props.load(new java.io.FileInputStream(GitBucketConf))
|
props.load(new java.io.FileInputStream(GitBucketConf))
|
||||||
}
|
|
||||||
SystemSettings(
|
|
||||||
getValue(props, AllowAccountRegistration, false),
|
|
||||||
getValue(props, Gravatar, true),
|
|
||||||
getValue(props, Notification, false),
|
|
||||||
if(getValue(props, Notification, false)){
|
|
||||||
Some(Smtp(
|
|
||||||
getValue(props, SmtpHost, ""),
|
|
||||||
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
|
||||||
getOptionValue(props, SmtpUser, None),
|
|
||||||
getOptionValue(props, SmtpPassword, None),
|
|
||||||
getOptionValue[Boolean](props, SmtpSsl, None)))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
getValue(props, LdapAuthentication, false),
|
|
||||||
if(getValue(props, LdapAuthentication, false)){
|
|
||||||
Some(Ldap(
|
|
||||||
getValue(props, LdapHost, ""),
|
|
||||||
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
|
|
||||||
getOptionValue(props, LdapBindDN, None),
|
|
||||||
getOptionValue(props, LdapBindPassword, None),
|
|
||||||
getValue(props, LdapBaseDN, ""),
|
|
||||||
getValue(props, LdapUserNameAttribute, ""),
|
|
||||||
getValue(props, LdapMailAddressAttribute, "")))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
)
|
SystemSettings(
|
||||||
|
getOptionValue(props, BaseURL, None),
|
||||||
|
getValue(props, AllowAccountRegistration, false),
|
||||||
|
getValue(props, Gravatar, true),
|
||||||
|
getValue(props, Notification, false),
|
||||||
|
if(getValue(props, Notification, false)){
|
||||||
|
Some(Smtp(
|
||||||
|
getValue(props, SmtpHost, ""),
|
||||||
|
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
||||||
|
getOptionValue(props, SmtpUser, None),
|
||||||
|
getOptionValue(props, SmtpPassword, None),
|
||||||
|
getOptionValue[Boolean](props, SmtpSsl, None),
|
||||||
|
getOptionValue(props, SmtpFromAddress, None),
|
||||||
|
getOptionValue(props, SmtpFromName, None)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
getValue(props, LdapAuthentication, false),
|
||||||
|
if(getValue(props, LdapAuthentication, false)){
|
||||||
|
Some(Ldap(
|
||||||
|
getValue(props, LdapHost, ""),
|
||||||
|
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
|
||||||
|
getOptionValue(props, LdapBindDN, None),
|
||||||
|
getOptionValue(props, LdapBindPassword, None),
|
||||||
|
getValue(props, LdapBaseDN, ""),
|
||||||
|
getValue(props, LdapUserNameAttribute, ""),
|
||||||
|
getOptionValue(props, LdapFullNameAttribute, None),
|
||||||
|
getValue(props, LdapMailAddressAttribute, ""),
|
||||||
|
getOptionValue[Boolean](props, LdapTls, None),
|
||||||
|
getOptionValue(props, LdapKeystore, None)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -76,6 +98,7 @@ object SystemSettingsService {
|
|||||||
import scala.reflect.ClassTag
|
import scala.reflect.ClassTag
|
||||||
|
|
||||||
case class SystemSettings(
|
case class SystemSettings(
|
||||||
|
baseUrl: Option[String],
|
||||||
allowAccountRegistration: Boolean,
|
allowAccountRegistration: Boolean,
|
||||||
gravatar: Boolean,
|
gravatar: Boolean,
|
||||||
notification: Boolean,
|
notification: Boolean,
|
||||||
@@ -90,18 +113,24 @@ object SystemSettingsService {
|
|||||||
bindPassword: Option[String],
|
bindPassword: Option[String],
|
||||||
baseDN: String,
|
baseDN: String,
|
||||||
userNameAttribute: String,
|
userNameAttribute: String,
|
||||||
mailAttribute: String)
|
fullNameAttribute: Option[String],
|
||||||
|
mailAttribute: String,
|
||||||
|
tls: Option[Boolean],
|
||||||
|
keystore: Option[String])
|
||||||
|
|
||||||
case class Smtp(
|
case class Smtp(
|
||||||
host: String,
|
host: String,
|
||||||
port: Option[Int],
|
port: Option[Int],
|
||||||
user: Option[String],
|
user: Option[String],
|
||||||
password: Option[String],
|
password: Option[String],
|
||||||
ssl: Option[Boolean])
|
ssl: Option[Boolean],
|
||||||
|
fromAddress: Option[String],
|
||||||
|
fromName: Option[String])
|
||||||
|
|
||||||
val DefaultSmtpPort = 25
|
val DefaultSmtpPort = 25
|
||||||
val DefaultLdapPort = 389
|
val DefaultLdapPort = 389
|
||||||
|
|
||||||
|
private val BaseURL = "base_url"
|
||||||
private val AllowAccountRegistration = "allow_account_registration"
|
private val AllowAccountRegistration = "allow_account_registration"
|
||||||
private val Gravatar = "gravatar"
|
private val Gravatar = "gravatar"
|
||||||
private val Notification = "notification"
|
private val Notification = "notification"
|
||||||
@@ -110,6 +139,8 @@ object SystemSettingsService {
|
|||||||
private val SmtpUser = "smtp.user"
|
private val SmtpUser = "smtp.user"
|
||||||
private val SmtpPassword = "smtp.password"
|
private val SmtpPassword = "smtp.password"
|
||||||
private val SmtpSsl = "smtp.ssl"
|
private val SmtpSsl = "smtp.ssl"
|
||||||
|
private val SmtpFromAddress = "smtp.from_address"
|
||||||
|
private val SmtpFromName = "smtp.from_name"
|
||||||
private val LdapAuthentication = "ldap_authentication"
|
private val LdapAuthentication = "ldap_authentication"
|
||||||
private val LdapHost = "ldap.host"
|
private val LdapHost = "ldap.host"
|
||||||
private val LdapPort = "ldap.port"
|
private val LdapPort = "ldap.port"
|
||||||
@@ -117,25 +148,28 @@ object SystemSettingsService {
|
|||||||
private val LdapBindPassword = "ldap.bind_password"
|
private val LdapBindPassword = "ldap.bind_password"
|
||||||
private val LdapBaseDN = "ldap.baseDN"
|
private val LdapBaseDN = "ldap.baseDN"
|
||||||
private val LdapUserNameAttribute = "ldap.username_attribute"
|
private val LdapUserNameAttribute = "ldap.username_attribute"
|
||||||
|
private val LdapFullNameAttribute = "ldap.fullname_attribute"
|
||||||
private val LdapMailAddressAttribute = "ldap.mail_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 = {
|
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
|
||||||
val value = props.getProperty(key)
|
defining(props.getProperty(key)){ value =>
|
||||||
if(value == null || value.isEmpty) default
|
if(value == null || value.isEmpty) default
|
||||||
else convertType(value).asInstanceOf[A]
|
else convertType(value).asInstanceOf[A]
|
||||||
}
|
}
|
||||||
|
|
||||||
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = {
|
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
|
||||||
val value = props.getProperty(key)
|
defining(props.getProperty(key)){ value =>
|
||||||
if(value == null || value.isEmpty) default
|
if(value == null || value.isEmpty) default
|
||||||
else Some(convertType(value)).asInstanceOf[Option[A]]
|
else Some(convertType(value)).asInstanceOf[Option[A]]
|
||||||
}
|
}
|
||||||
|
|
||||||
private def convertType[A: ClassTag](value: String) = {
|
private def convertType[A: ClassTag](value: String) =
|
||||||
val c = implicitly[ClassTag[A]].runtimeClass
|
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
|
||||||
if(c == classOf[Boolean]) value.toBoolean
|
if(c == classOf[Boolean]) value.toBoolean
|
||||||
else if(c == classOf[Int]) value.toInt
|
else if(c == classOf[Int]) value.toInt
|
||||||
else value
|
else value
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
145
src/main/scala/service/WebHookService.scala
Normal file
145
src/main/scala/service/WebHookService.scala
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import scala.slick.driver.H2Driver.simple._
|
||||||
|
import Database.threadLocalSession
|
||||||
|
|
||||||
|
import model._
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import service.RepositoryService.RepositoryInfo
|
||||||
|
import util.JGitUtil
|
||||||
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.apache.http.message.BasicNameValuePair
|
||||||
|
import org.apache.http.client.entity.UrlEncodedFormEntity
|
||||||
|
import org.apache.http.protocol.HTTP
|
||||||
|
import org.apache.http.NameValuePair
|
||||||
|
|
||||||
|
trait WebHookService {
|
||||||
|
import WebHookService._
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
|
||||||
|
|
||||||
|
def getWebHookURLs(owner: String, repository: String): List[WebHook] =
|
||||||
|
Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list
|
||||||
|
|
||||||
|
def addWebHookURL(owner: String, repository: String, url :String): Unit =
|
||||||
|
WebHooks.insert(WebHook(owner, repository, url))
|
||||||
|
|
||||||
|
def deleteWebHookURL(owner: String, repository: String, url :String): Unit =
|
||||||
|
Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete
|
||||||
|
|
||||||
|
def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
|
||||||
|
import org.json4s._
|
||||||
|
import org.json4s.jackson.Serialization
|
||||||
|
import org.json4s.jackson.Serialization.{read, write}
|
||||||
|
import org.apache.http.client.methods.HttpPost
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder
|
||||||
|
import scala.concurrent._
|
||||||
|
import ExecutionContext.Implicits.global
|
||||||
|
|
||||||
|
logger.debug("start callWebHook")
|
||||||
|
implicit val formats = Serialization.formats(NoTypeHints)
|
||||||
|
|
||||||
|
if(webHookURLs.nonEmpty){
|
||||||
|
val json = write(payload)
|
||||||
|
val httpClient = HttpClientBuilder.create.build
|
||||||
|
|
||||||
|
webHookURLs.foreach { webHookUrl =>
|
||||||
|
val f = future {
|
||||||
|
logger.debug(s"start web hook invocation for ${webHookUrl}")
|
||||||
|
val httpPost = new HttpPost(webHookUrl.url)
|
||||||
|
|
||||||
|
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||||
|
params.add(new BasicNameValuePair("payload", json))
|
||||||
|
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
||||||
|
|
||||||
|
httpClient.execute(httpPost)
|
||||||
|
httpPost.releaseConnection()
|
||||||
|
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
||||||
|
}
|
||||||
|
f.onSuccess {
|
||||||
|
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
|
||||||
|
}
|
||||||
|
f.onFailure {
|
||||||
|
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("end callWebHook")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object WebHookService {
|
||||||
|
|
||||||
|
case class WebHookPayload(
|
||||||
|
pusher: WebHookUser,
|
||||||
|
ref: String,
|
||||||
|
commits: List[WebHookCommit],
|
||||||
|
repository: WebHookRepository)
|
||||||
|
|
||||||
|
object WebHookPayload {
|
||||||
|
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)
|
||||||
|
val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
|
||||||
|
|
||||||
|
WebHookCommit(
|
||||||
|
id = commit.id,
|
||||||
|
message = commit.fullMessage,
|
||||||
|
timestamp = commit.time.toString,
|
||||||
|
url = commitUrl,
|
||||||
|
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
|
||||||
|
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
|
||||||
|
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
|
||||||
|
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
|
||||||
|
author = WebHookUser(
|
||||||
|
name = commit.committer,
|
||||||
|
email = commit.mailAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.toList,
|
||||||
|
WebHookRepository(
|
||||||
|
name = repositoryInfo.name,
|
||||||
|
url = repositoryInfo.url,
|
||||||
|
description = repositoryInfo.repository.description.getOrElse(""),
|
||||||
|
watchers = 0,
|
||||||
|
forks = repositoryInfo.forkedCount,
|
||||||
|
`private` = repositoryInfo.repository.isPrivate,
|
||||||
|
owner = WebHookUser(
|
||||||
|
name = repositoryOwner.userName,
|
||||||
|
email = repositoryOwner.mailAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class WebHookCommit(
|
||||||
|
id: String,
|
||||||
|
message: String,
|
||||||
|
timestamp: String,
|
||||||
|
url: String,
|
||||||
|
added: List[String],
|
||||||
|
removed: List[String],
|
||||||
|
modified: List[String],
|
||||||
|
author: WebHookUser)
|
||||||
|
|
||||||
|
case class WebHookRepository(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
description: String,
|
||||||
|
watchers: Int,
|
||||||
|
forks: Int,
|
||||||
|
`private`: Boolean,
|
||||||
|
owner: WebHookUser)
|
||||||
|
|
||||||
|
case class WebHookUser(
|
||||||
|
name: String,
|
||||||
|
email: String)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import util.{Directory, JGitUtil, LockUtil}
|
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 {
|
object WikiService {
|
||||||
|
|
||||||
@@ -15,8 +26,9 @@ object WikiService {
|
|||||||
* @param content the page content
|
* @param content the page content
|
||||||
* @param committer the last committer
|
* @param committer the last committer
|
||||||
* @param time the last modified time
|
* @param time the last modified time
|
||||||
|
* @param id the latest commit id
|
||||||
*/
|
*/
|
||||||
case class WikiPageInfo(name: String, content: String, committer: String, time: Date)
|
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The model for wiki page history.
|
* The model for wiki page history.
|
||||||
@@ -33,29 +45,25 @@ object WikiService {
|
|||||||
trait WikiService {
|
trait WikiService {
|
||||||
import WikiService._
|
import WikiService._
|
||||||
|
|
||||||
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = {
|
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
|
||||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||||
val dir = Directory.getWikiRepositoryDir(owner, repository)
|
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
|
||||||
if(!dir.exists){
|
if(!dir.exists){
|
||||||
try {
|
|
||||||
JGitUtil.initRepository(dir)
|
JGitUtil.initRepository(dir)
|
||||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
|
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the wiki page.
|
* Returns the wiki page.
|
||||||
*/
|
*/
|
||||||
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
||||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
if(!JGitUtil.isEmpty(git)){
|
if(!JGitUtil.isEmpty(git)){
|
||||||
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
||||||
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
|
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
|
||||||
|
file.committer, file.time, file.commitId)
|
||||||
}
|
}
|
||||||
} else None
|
} else None
|
||||||
}
|
}
|
||||||
@@ -64,8 +72,8 @@ trait WikiService {
|
|||||||
/**
|
/**
|
||||||
* Returns the content of the specified file.
|
* Returns the content of the specified file.
|
||||||
*/
|
*/
|
||||||
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = {
|
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
|
||||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
if(!JGitUtil.isEmpty(git)){
|
if(!JGitUtil.isEmpty(git)){
|
||||||
val index = path.lastIndexOf('/')
|
val index = path.lastIndexOf('/')
|
||||||
val parentPath = if(index < 0) "." else path.substring(0, index)
|
val parentPath = if(index < 0) "." else path.substring(0, index)
|
||||||
@@ -76,58 +84,181 @@ trait WikiService {
|
|||||||
}
|
}
|
||||||
} else None
|
} else None
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of wiki page names.
|
* Returns the list of wiki page names.
|
||||||
*/
|
*/
|
||||||
def getWikiPageList(owner: String, repository: String): List[String] = {
|
def getWikiPageList(owner: String, repository: String): List[String] = {
|
||||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
JGitUtil.getFileList(git, "master", ".")
|
JGitUtil.getFileList(git, "master", ".")
|
||||||
.filter(_.name.endsWith(".md"))
|
.filter(_.name.endsWith(".md"))
|
||||||
.map(_.name.replaceFirst("\\.md$", ""))
|
.map(_.name.replaceFirst("\\.md$", ""))
|
||||||
.sortBy(x => x)
|
.sortBy(x => x)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverts specified changes.
|
||||||
|
*/
|
||||||
|
def revertWikiPage(owner: String, repository: String, from: String, to: String,
|
||||||
|
committer: model.Account, pageName: Option[String]): Boolean = {
|
||||||
|
|
||||||
|
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}"))
|
||||||
|
|
||||||
|
val newTreeIter = new CanonicalTreeParser
|
||||||
|
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
|
||||||
|
|
||||||
|
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
|
||||||
|
pageName match {
|
||||||
|
case Some(x) => diff.getNewPath == x + ".md"
|
||||||
|
case None => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val patch = using(new java.io.ByteArrayOutputStream()){ out =>
|
||||||
|
val formatter = new DiffFormatter(out)
|
||||||
|
formatter.setRepository(git.getRepository)
|
||||||
|
formatter.format(diffs.asJava)
|
||||||
|
new String(out.toByteArray, "UTF-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
* Save the wiki page.
|
||||||
*/
|
*/
|
||||||
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
||||||
content: String, committer: model.Account, message: String): Option[String] = {
|
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
|
||||||
|
|
||||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||||
// clone working copy
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
val workDir = Directory.getWikiWorkDir(owner, repository)
|
val builder = DirCache.newInCore.builder()
|
||||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
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
|
if(headId != null){
|
||||||
JGitUtil.withGit(workDir){ git =>
|
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||||
val file = new File(workDir, newPageName + ".md")
|
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||||
val added = if(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
|
val index = treeWalk.addTree(revWalk.parseTree(headId))
|
||||||
FileUtils.writeStringToFile(file, content, "UTF-8")
|
treeWalk.setRecursive(true)
|
||||||
git.add.addFilepattern(file.getName).call
|
while(treeWalk.next){
|
||||||
true
|
val path = treeWalk.getPathString
|
||||||
} else {
|
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
|
||||||
false
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete file
|
if(created || updated || removed){
|
||||||
val deleted = if(currentPageName != "" && currentPageName != newPageName){
|
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||||
git.rm.addFilepattern(currentPageName + ".md").call
|
builder.finish()
|
||||||
true
|
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||||
} else {
|
if(message.trim.length == 0) {
|
||||||
false
|
if(removed){
|
||||||
}
|
s"Rename ${currentPageName} to ${newPageName}"
|
||||||
|
} else if(created){
|
||||||
|
s"Created ${newPageName}"
|
||||||
|
} else {
|
||||||
|
s"Updated ${newPageName}"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
})
|
||||||
|
|
||||||
// commit and push
|
Some(newHeadId)
|
||||||
if(added || deleted){
|
} else None
|
||||||
val commit = git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call
|
|
||||||
git.push.call
|
|
||||||
Some(commit.getName)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,37 +268,35 @@ trait WikiService {
|
|||||||
*/
|
*/
|
||||||
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
||||||
committer: String, mailAddress: String, message: String): Unit = {
|
committer: String, mailAddress: String, message: String): Unit = {
|
||||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||||
// clone working copy
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
val workDir = Directory.getWikiWorkDir(owner, repository)
|
val builder = DirCache.newInCore.builder()
|
||||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
|
var removed = false
|
||||||
|
|
||||||
// delete file
|
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||||
new File(workDir, pageName + ".md").delete
|
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||||
|
val index = treeWalk.addTree(revWalk.parseTree(headId))
|
||||||
JGitUtil.withGit(workDir){ git =>
|
treeWalk.setRecursive(true)
|
||||||
git.rm.addFilepattern(pageName + ".md").call
|
while(treeWalk.next){
|
||||||
|
val path = treeWalk.getPathString
|
||||||
// commit and push
|
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
|
||||||
git.commit.setAuthor(committer, mailAddress).setMessage(message).call
|
if(path != pageName + ".md"){
|
||||||
git.push.call
|
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||||
|
} else {
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(removed){
|
||||||
|
builder.finish()
|
||||||
|
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
|
}
|
||||||
if(!workDir.exists){
|
|
||||||
val git =
|
|
||||||
Git.cloneRepository
|
|
||||||
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
|
|
||||||
.setDirectory(workDir)
|
|
||||||
.call
|
|
||||||
git.getRepository.close // close .git resources.
|
|
||||||
} else {
|
|
||||||
JGitUtil.withGit(workDir){ git =>
|
|
||||||
git.pull.call
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package servlet
|
package servlet
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.sql.Connection
|
import java.sql.{DriverManager, Connection}
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import javax.servlet.ServletContextEvent
|
import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import util.Directory
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
|
||||||
object AutoUpdate {
|
object AutoUpdate {
|
||||||
|
|
||||||
@@ -26,15 +28,14 @@ object AutoUpdate {
|
|||||||
*/
|
*/
|
||||||
def update(conn: Connection): Unit = {
|
def update(conn: Connection): Unit = {
|
||||||
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
|
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
|
||||||
val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
|
|
||||||
if(in != null){
|
using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
|
||||||
val sql = IOUtils.toString(in, "UTF-8")
|
if(in != null){
|
||||||
val stmt = conn.createStatement()
|
val sql = IOUtils.toString(in, "UTF-8")
|
||||||
try {
|
using(conn.createStatement()){ stmt =>
|
||||||
logger.debug(sqlPath + "=" + sql)
|
logger.debug(sqlPath + "=" + sql)
|
||||||
stmt.executeUpdate(sql)
|
stmt.executeUpdate(sql)
|
||||||
} finally {
|
}
|
||||||
stmt.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,28 +50,36 @@ object AutoUpdate {
|
|||||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||||
*/
|
*/
|
||||||
val versions = Seq(
|
val versions = Seq(
|
||||||
|
Version(1, 11),
|
||||||
|
Version(1, 10),
|
||||||
|
Version(1, 9),
|
||||||
|
Version(1, 8),
|
||||||
|
Version(1, 7),
|
||||||
|
Version(1, 6),
|
||||||
Version(1, 5),
|
Version(1, 5),
|
||||||
Version(1, 4),
|
Version(1, 4),
|
||||||
new Version(1, 3){
|
new Version(1, 3){
|
||||||
override def update(conn: Connection): Unit = {
|
override def update(conn: Connection): Unit = {
|
||||||
super.update(conn)
|
super.update(conn)
|
||||||
// Fix wiki repository configuration
|
// Fix wiki repository configuration
|
||||||
val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")
|
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
|
||||||
while(rs.next){
|
while(rs.next){
|
||||||
val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))
|
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
||||||
val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository
|
defining(git.getRepository.getConfig){ config =>
|
||||||
val config = repository.getConfig
|
if(!config.getBoolean("http", "receivepack", false)){
|
||||||
if(!config.getBoolean("http", "receivepack", false)){
|
config.setBoolean("http", null, "receivepack", true)
|
||||||
config.setBoolean("http", null, "receivepack", true)
|
config.save
|
||||||
config.save
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
repository.close
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Version(1, 2),
|
Version(1, 2),
|
||||||
Version(1, 1),
|
Version(1, 1),
|
||||||
Version(1, 0)
|
Version(1, 0),
|
||||||
|
Version(0, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +90,7 @@ object AutoUpdate {
|
|||||||
/**
|
/**
|
||||||
* The version file (GITBUCKET_HOME/version).
|
* The version file (GITBUCKET_HOME/version).
|
||||||
*/
|
*/
|
||||||
val versionFile = new File(Directory.GitBucketHome, "version")
|
lazy val versionFile = new File(GitBucketHome, "version")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current version from the version file.
|
* Returns the current version from the version file.
|
||||||
@@ -96,46 +105,60 @@ object AutoUpdate {
|
|||||||
}
|
}
|
||||||
case _ => Version(0, 0)
|
case _ => Version(0, 0)
|
||||||
}
|
}
|
||||||
} else {
|
} else Version(0, 0)
|
||||||
Version(0, 0)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start H2 database and update schema automatically.
|
* Update database schema automatically in the context initializing.
|
||||||
*/
|
*/
|
||||||
class AutoUpdateListener extends org.h2.server.web.DbStarter {
|
class AutoUpdateListener extends ServletContextListener {
|
||||||
import AutoUpdate._
|
import AutoUpdate._
|
||||||
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
||||||
|
|
||||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||||
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${Directory.DatabaseHome}")
|
val datadir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||||
super.contextInitialized(event)
|
if(datadir != null){
|
||||||
logger.debug("H2 started")
|
System.setProperty("gitbucket.home", datadir)
|
||||||
|
}
|
||||||
|
org.h2.Driver.load()
|
||||||
|
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
|
||||||
|
|
||||||
logger.debug("Start schema update")
|
logger.debug("Start schema update")
|
||||||
val conn = getConnection()
|
defining(getConnection(event.getServletContext)){ conn =>
|
||||||
try {
|
try {
|
||||||
val currentVersion = getCurrentVersion()
|
defining(getCurrentVersion()){ currentVersion =>
|
||||||
if(currentVersion == headVersion){
|
if(currentVersion == headVersion){
|
||||||
logger.debug("No update")
|
logger.debug("No update")
|
||||||
} else {
|
} else if(!versions.contains(currentVersion)){
|
||||||
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
|
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
|
||||||
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
|
} else {
|
||||||
conn.commit()
|
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
|
||||||
logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString)
|
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
|
||||||
}
|
conn.commit()
|
||||||
} catch {
|
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
|
||||||
case ex: Throwable => {
|
}
|
||||||
logger.error("Failed to schema update", ex)
|
}
|
||||||
ex.printStackTrace()
|
} catch {
|
||||||
conn.rollback()
|
case ex: Throwable => {
|
||||||
|
logger.error("Failed to schema update", ex)
|
||||||
|
ex.printStackTrace()
|
||||||
|
conn.rollback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug("End schema update")
|
logger.debug("End schema update")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
def contextDestroyed(sce: ServletContextEvent): Unit = {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getConnection(servletContext: ServletContext): Connection =
|
||||||
|
DriverManager.getConnection(
|
||||||
|
servletContext.getInitParameter("db.url"),
|
||||||
|
servletContext.getInitParameter("db.user"),
|
||||||
|
servletContext.getInitParameter("db.password"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import javax.servlet._
|
|||||||
import javax.servlet.http._
|
import javax.servlet.http._
|
||||||
import service.{SystemSettingsService, AccountService, RepositoryService}
|
import service.{SystemSettingsService, AccountService, RepositoryService}
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import util.Implicits._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Keys
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
|
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
|
||||||
@@ -25,29 +28,30 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
|
||||||
val repositoryOwner = paths(2)
|
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
|
||||||
val repositoryName = paths(3).replaceFirst("\\.git$", "")
|
case Some(repository) => {
|
||||||
|
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
|
||||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
|
|
||||||
case Some(repository) => {
|
|
||||||
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
|
|
||||||
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
|
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
|
||||||
chain.doFilter(req, wrappedResponse)
|
chain.doFilter(req, wrappedResponse)
|
||||||
} else {
|
} else {
|
||||||
request.getHeader("Authorization") match {
|
request.getHeader("Authorization") match {
|
||||||
case null => requireAuth(response)
|
case null => requireAuth(response)
|
||||||
case auth => decodeAuthHeader(auth).split(":") match {
|
case auth => decodeAuthHeader(auth).split(":") match {
|
||||||
case Array(username, password) if(isWritableUser(username, password, repository)) => {
|
case Array(username, password) if(isWritableUser(username, password, repository)) => {
|
||||||
request.setAttribute("USER_NAME", username)
|
request.setAttribute(Keys.Request.UserName, username)
|
||||||
chain.doFilter(req, wrappedResponse)
|
chain.doFilter(req, wrappedResponse)
|
||||||
|
}
|
||||||
|
case _ => requireAuth(response)
|
||||||
}
|
}
|
||||||
case _ => requireAuth(response)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case None => {
|
||||||
|
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
|
||||||
|
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case None => response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
case ex: Exception => {
|
case ex: Exception => {
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ import org.slf4j.LoggerFactory
|
|||||||
import javax.servlet.ServletConfig
|
import javax.servlet.ServletConfig
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
import util.{JGitUtil, Directory}
|
import util.{StringUtil, Keys, JGitUtil, Directory}
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Implicits._
|
||||||
import service._
|
import service._
|
||||||
|
import WebHookService._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides Git repository via HTTP.
|
* Provides Git repository via HTTP.
|
||||||
@@ -38,81 +43,152 @@ class GitRepositoryServlet extends GitServlet {
|
|||||||
|
|
||||||
def getServletContext(): ServletContext = config.getServletContext
|
def getServletContext(): ServletContext = config.getServletContext
|
||||||
def getServletName(): String = config.getServletName
|
def getServletName(): String = config.getServletName
|
||||||
});
|
})
|
||||||
|
|
||||||
super.init(config)
|
super.init(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] {
|
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
|
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
|
||||||
|
|
||||||
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
||||||
val receivePack = new ReceivePack(db)
|
val receivePack = new ReceivePack(db)
|
||||||
val userName = request.getAttribute("USER_NAME").asInstanceOf[String]
|
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
||||||
|
|
||||||
logger.debug("requestURI: " + request.getRequestURI)
|
logger.debug("requestURI: " + request.getRequestURI)
|
||||||
logger.debug("userName:" + userName)
|
logger.debug("pusher:" + pusher)
|
||||||
|
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
defining(request.paths){ paths =>
|
||||||
val owner = paths(2)
|
val owner = paths(1)
|
||||||
val repository = paths(3).replaceFirst("\\.git$", "")
|
val repository = paths(2).replaceFirst("\\.git$", "")
|
||||||
|
|
||||||
logger.debug("repository:" + owner + "/" + repository)
|
|
||||||
|
|
||||||
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName))
|
logger.debug("repository:" + owner + "/" + repository)
|
||||||
receivePack
|
|
||||||
|
if(!repository.endsWith(".wiki")){
|
||||||
|
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
|
||||||
|
}
|
||||||
|
receivePack
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
|
|
||||||
class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook
|
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook
|
||||||
with RepositoryService with AccountService with IssuesService with ActivityService {
|
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||||
|
|
||||||
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||||
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git =>
|
try {
|
||||||
commands.asScala.foreach { command =>
|
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||||
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
|
commands.asScala.foreach { command =>
|
||||||
val refName = command.getRefName.split("/")
|
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
|
||||||
|
val commits = command.getType match {
|
||||||
// apply issue comment
|
case ReceiveCommand.Type.DELETE => Nil
|
||||||
val newCommits = commits.flatMap { commit =>
|
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
|
||||||
if(!existsCommitId(owner, repository, commit.id)){
|
|
||||||
insertCommitId(owner, repository, commit.id)
|
|
||||||
"(^|\\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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(commit)
|
|
||||||
} else None
|
|
||||||
}.toList
|
|
||||||
|
|
||||||
// record activity
|
|
||||||
if(refName(1) == "heads"){
|
|
||||||
command.getType match {
|
|
||||||
case ReceiveCommand.Type.CREATE => {
|
|
||||||
recordCreateBranchActivity(owner, repository, userName, refName(2))
|
|
||||||
recordPushActivity(owner, repository, userName, refName(2), newCommits)
|
|
||||||
}
|
|
||||||
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, refName(2), newCommits)
|
|
||||||
case _ =>
|
|
||||||
}
|
}
|
||||||
} else if(refName(1) == "tags"){
|
val refName = command.getRefName.split("/")
|
||||||
command.getType match {
|
val branchName = refName.drop(2).mkString("/")
|
||||||
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, refName(2), newCommits)
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(refName(1) == "heads"){
|
||||||
|
command.getType match {
|
||||||
|
case ReceiveCommand.Type.CREATE |
|
||||||
|
ReceiveCommand.Type.UPDATE |
|
||||||
|
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
|
||||||
|
updatePullRequests(branchName)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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) = {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ class TransactionFilter extends Filter {
|
|||||||
chain.doFilter(req, res)
|
chain.doFilter(req, res)
|
||||||
} else {
|
} else {
|
||||||
Database(req.getServletContext) withTransaction {
|
Database(req.getServletContext) withTransaction {
|
||||||
logger.debug("TODO begin transaction")
|
logger.debug("begin transaction")
|
||||||
chain.doFilter(req, res)
|
chain.doFilter(req, res)
|
||||||
logger.debug("TODO end transaction")
|
logger.debug("end transaction")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package util
|
|||||||
import app.ControllerBase
|
import app.ControllerBase
|
||||||
import service._
|
import service._
|
||||||
import RepositoryService.RepositoryInfo
|
import RepositoryService.RepositoryInfo
|
||||||
|
import util.Implicits._
|
||||||
|
import util.ControlUtil._
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows only oneself and administrators.
|
* Allows only oneself and administrators.
|
||||||
@@ -13,11 +15,12 @@ trait OneselfAuthenticator { self: ControllerBase =>
|
|||||||
|
|
||||||
private def authenticate(action: => Any) = {
|
private def authenticate(action: => Any) = {
|
||||||
{
|
{
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
defining(request.paths){ paths =>
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
case Some(x) if(x.isAdmin) => action
|
case Some(x) if(x.isAdmin) => action
|
||||||
case Some(x) if(paths(1) == x.userName) => action
|
case Some(x) if(paths(0) == x.userName) => action
|
||||||
case _ => Unauthorized()
|
case _ => Unauthorized()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,14 +35,15 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
|
|||||||
|
|
||||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||||
{
|
{
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
defining(request.paths){ paths =>
|
||||||
getRepository(paths(1), paths(2), baseUrl).map { repository =>
|
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
case Some(x) if(x.isAdmin) => action(repository)
|
case Some(x) if(x.isAdmin) => action(repository)
|
||||||
case Some(x) if(repository.owner == x.userName) => action(repository)
|
case Some(x) if(repository.owner == x.userName) => action(repository)
|
||||||
case _ => Unauthorized()
|
case _ => Unauthorized()
|
||||||
}
|
}
|
||||||
} getOrElse NotFound()
|
} getOrElse NotFound()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,15 +91,16 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
|
|||||||
|
|
||||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||||
{
|
{
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
defining(request.paths){ paths =>
|
||||||
getRepository(paths(1), paths(2), baseUrl).map { repository =>
|
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
case Some(x) if(x.isAdmin) => action(repository)
|
case Some(x) if(x.isAdmin) => action(repository)
|
||||||
case Some(x) if(paths(1) == x.userName) => action(repository)
|
case Some(x) if(paths(0) == x.userName) => action(repository)
|
||||||
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
|
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
|
||||||
case _ => Unauthorized()
|
case _ => Unauthorized()
|
||||||
}
|
}
|
||||||
} getOrElse NotFound()
|
} getOrElse NotFound()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,19 +114,20 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
|
|||||||
|
|
||||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||||
{
|
{
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
defining(request.paths){ paths =>
|
||||||
getRepository(paths(1), paths(2), baseUrl).map { repository =>
|
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
||||||
if(!repository.repository.isPrivate){
|
if(!repository.repository.isPrivate){
|
||||||
action(repository)
|
action(repository)
|
||||||
} else {
|
} else {
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
case Some(x) if(x.isAdmin) => action(repository)
|
case Some(x) if(x.isAdmin) => action(repository)
|
||||||
case Some(x) if(paths(1) == x.userName) => action(repository)
|
case Some(x) if(paths(0) == x.userName) => action(repository)
|
||||||
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
|
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
|
||||||
case _ => Unauthorized()
|
case _ => Unauthorized()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} getOrElse NotFound()
|
||||||
} getOrElse NotFound()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,16 +141,17 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
|
|||||||
|
|
||||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||||
{
|
{
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
defining(request.paths){ paths =>
|
||||||
getRepository(paths(1), paths(2), baseUrl).map { repository =>
|
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
case Some(x) if(x.isAdmin) => action(repository)
|
case Some(x) if(x.isAdmin) => action(repository)
|
||||||
case Some(x) if(!repository.repository.isPrivate) => action(repository)
|
case Some(x) if(!repository.repository.isPrivate) => action(repository)
|
||||||
case Some(x) if(paths(1) == x.userName) => action(repository)
|
case Some(x) if(paths(0) == x.userName) => action(repository)
|
||||||
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
|
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
|
||||||
case _ => Unauthorized()
|
case _ => Unauthorized()
|
||||||
}
|
}
|
||||||
} getOrElse NotFound()
|
} getOrElse NotFound()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/main/scala/util/ControlUtil.scala
Normal file
51
src/main/scala/util/ControlUtil.scala
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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.
|
||||||
|
*/
|
||||||
|
object ControlUtil {
|
||||||
|
|
||||||
|
def defining[A, B](value: A)(f: A => B): B = f(value)
|
||||||
|
|
||||||
|
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
|
||||||
|
try f(resource) finally {
|
||||||
|
if(resource != null){
|
||||||
|
allCatch {
|
||||||
|
resource.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def using[T](git: Git)(f: Git => T): T =
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
|
||||||
|
try f(revWalk) finally revWalk.release()
|
||||||
|
|
||||||
|
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
|
||||||
|
try f(treeWalk) finally treeWalk.release()
|
||||||
|
|
||||||
|
|
||||||
|
// def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
|
||||||
|
// try {
|
||||||
|
// f(ref)
|
||||||
|
// } finally {
|
||||||
|
// val refUpdate = git.getRepository.updateRef(ref.getDestination)
|
||||||
|
// refUpdate.setForceUpdate(true)
|
||||||
|
// refUpdate.delete()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,15 +1,31 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import util.ControlUtil._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides directories used by GitBucket.
|
* Provides directories used by GitBucket.
|
||||||
*/
|
*/
|
||||||
object Directory {
|
object Directory {
|
||||||
|
|
||||||
val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
|
val GitBucketHome = (System.getProperty("gitbucket.home") match {
|
||||||
case Some(env) => new File(env)
|
// -Dgitbucket.home=<path>
|
||||||
case None => new File(System.getProperty("user.home"), "gitbucket")
|
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
|
}).getAbsolutePath
|
||||||
|
|
||||||
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
|
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
|
||||||
@@ -21,17 +37,17 @@ object Directory {
|
|||||||
/**
|
/**
|
||||||
* Repository names of the specified user.
|
* Repository names of the specified user.
|
||||||
*/
|
*/
|
||||||
def getRepositories(owner: String): List[String] = {
|
def getRepositories(owner: String): List[String] =
|
||||||
val dir = new File(s"${RepositoryHome}/${owner}")
|
defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
|
||||||
if(dir.exists){
|
if(dir.exists){
|
||||||
dir.listFiles.filter { file =>
|
dir.listFiles.filter { file =>
|
||||||
file.isDirectory && !file.getName.endsWith(".wiki.git")
|
file.isDirectory && !file.getName.endsWith(".wiki.git")
|
||||||
}.map(_.getName.replaceFirst("\\.git$", "")).toList
|
}.map(_.getName.replaceFirst("\\.git$", "")).toList
|
||||||
} else {
|
} else {
|
||||||
Nil
|
Nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Substance directory of the repository.
|
* Substance directory of the repository.
|
||||||
*/
|
*/
|
||||||
@@ -55,25 +71,10 @@ object Directory {
|
|||||||
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
|
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
|
||||||
new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
|
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.
|
* Substance directory of the wiki repository.
|
||||||
*/
|
*/
|
||||||
def getWikiRepositoryDir(owner: String, repository: String): File =
|
def getWikiRepositoryDir(owner: String, repository: String): File =
|
||||||
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
|
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")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,72 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import org.apache.commons.io.{IOUtils, FileUtils}
|
import org.apache.commons.io.FileUtils
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
|
import util.ControlUtil._
|
||||||
|
|
||||||
object FileUtil {
|
object FileUtil {
|
||||||
|
|
||||||
def getMimeType(name: String): String = {
|
def getMimeType(name: String): String =
|
||||||
val fileNameMap = URLConnection.getFileNameMap()
|
defining(URLConnection.getFileNameMap()){ fileNameMap =>
|
||||||
val mimeType = fileNameMap.getContentTypeFor(name)
|
fileNameMap.getContentTypeFor(name) match {
|
||||||
if(mimeType == null){
|
case null => "application/octet-stream"
|
||||||
"application/octeat-stream"
|
case mimeType => mimeType
|
||||||
} else {
|
}
|
||||||
mimeType
|
}
|
||||||
|
|
||||||
|
def getContentType(name: String, bytes: Array[Byte]): String = {
|
||||||
|
defining(getMimeType(name)){ mimeType =>
|
||||||
|
if(mimeType == "application/octet-stream" && isText(bytes)){
|
||||||
|
"text/plain"
|
||||||
|
} else {
|
||||||
|
mimeType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
|
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
|
||||||
|
|
||||||
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
|
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
|
||||||
|
|
||||||
def isText(content: Array[Byte]): Boolean = !content.contains(0)
|
def isText(content: Array[Byte]): Boolean = !content.contains(0)
|
||||||
|
|
||||||
def createZipFile(dest: File, dir: File): Unit = {
|
// def createZipFile(dest: File, dir: File): Unit = {
|
||||||
def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
|
// def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
|
||||||
dir.listFiles.map { file =>
|
// dir.listFiles.map { file =>
|
||||||
if(file.isFile){
|
// if(file.isFile){
|
||||||
out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
|
// out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
|
||||||
out.write(FileUtils.readFileToByteArray(file))
|
// out.write(FileUtils.readFileToByteArray(file))
|
||||||
out.closeArchiveEntry
|
// out.closeArchiveEntry
|
||||||
} else if(file.isDirectory){
|
// } else if(file.isDirectory){
|
||||||
addDirectoryToZip(out, file, path + "/" + file.getName)
|
// addDirectoryToZip(out, file, path + "/" + file.getName)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// using(new ZipArchiveOutputStream(dest)){ out =>
|
||||||
|
// addDirectoryToZip(out, dir, dir.getName)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
|
||||||
|
if(i >= 0) path.substring(i + 1) else path
|
||||||
|
}
|
||||||
|
|
||||||
|
def getExtension(name: String): String =
|
||||||
|
name.lastIndexOf('.') match {
|
||||||
|
case i if(i >= 0) => name.substring(i + 1)
|
||||||
|
case _ => ""
|
||||||
}
|
}
|
||||||
|
|
||||||
val out = new ZipArchiveOutputStream(dest)
|
def withTmpDir[A](dir: File)(action: File => A): A = {
|
||||||
|
if(dir.exists()){
|
||||||
|
FileUtils.deleteDirectory(dir)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
addDirectoryToZip(out, dir, dir.getName)
|
action(dir)
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.closeQuietly(out)
|
FileUtils.deleteDirectory(dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
def getExtension(name: String): String = {
|
|
||||||
val index = name.lastIndexOf('.')
|
|
||||||
if(index >= 0){
|
|
||||||
name.substring(index + 1)
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import scala.util.matching.Regex
|
import scala.util.matching.Regex
|
||||||
|
import scala.util.control.Exception._
|
||||||
|
import javax.servlet.http.{HttpSession, HttpServletRequest}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides some usable implicit conversions.
|
* Provides some usable implicit conversions.
|
||||||
@@ -12,7 +14,7 @@ object Implicits {
|
|||||||
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
|
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
|
||||||
|
|
||||||
@scala.annotation.tailrec
|
@scala.annotation.tailrec
|
||||||
private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] = {
|
private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] =
|
||||||
list match {
|
list match {
|
||||||
case x :: xs => {
|
case x :: xs => {
|
||||||
xs.span(condition(x, _)) match {
|
xs.span(condition(x, _)) match {
|
||||||
@@ -21,7 +23,6 @@ object Implicits {
|
|||||||
}
|
}
|
||||||
case Nil => result
|
case Nil => result
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit class RichString(value: String){
|
implicit class RichString(value: String){
|
||||||
@@ -41,6 +42,36 @@ object Implicits {
|
|||||||
}
|
}
|
||||||
sb.toString
|
sb.toString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt {
|
||||||
|
Integer.parseInt(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
implicit class RichRequest(request: HttpServletRequest){
|
||||||
|
|
||||||
|
def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/")
|
||||||
|
|
||||||
|
def hasQueryString: Boolean = request.getQueryString != null
|
||||||
|
|
||||||
|
def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit class RichSession(session: HttpSession){
|
||||||
|
|
||||||
|
def putAndGet[T](key: String, value: T): T = {
|
||||||
|
session.setAttribute(key, value)
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAndRemove[T](key: String): Option[T] = {
|
||||||
|
val value = session.getAttribute(key).asInstanceOf[T]
|
||||||
|
if(value == null){
|
||||||
|
session.removeAttribute(key)
|
||||||
|
}
|
||||||
|
Option(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package util
|
|||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import util.StringUtil._
|
import util.StringUtil._
|
||||||
|
import util.ControlUtil._
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
import org.eclipse.jgit.lib._
|
import org.eclipse.jgit.lib._
|
||||||
import org.eclipse.jgit.revwalk._
|
import org.eclipse.jgit.revwalk._
|
||||||
@@ -14,6 +15,7 @@ import org.eclipse.jgit.errors.MissingObjectException
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import org.eclipse.jgit.api.errors.NoHeadException
|
import org.eclipse.jgit.api.errors.NoHeadException
|
||||||
import service.RepositoryService
|
import service.RepositoryService
|
||||||
|
import org.eclipse.jgit.dircache.DirCacheEntry
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides complex JGit operations.
|
* Provides complex JGit operations.
|
||||||
@@ -42,8 +44,10 @@ object JGitUtil {
|
|||||||
* @param message the last commit message
|
* @param message the last commit message
|
||||||
* @param commitId the last commit id
|
* @param commitId the last commit id
|
||||||
* @param committer the last committer name
|
* @param committer the last committer name
|
||||||
|
* @param mailAddress the committer's mail address
|
||||||
*/
|
*/
|
||||||
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, committer: String)
|
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
|
||||||
|
committer: String, mailAddress: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The commit data.
|
* The commit data.
|
||||||
@@ -68,29 +72,17 @@ object JGitUtil {
|
|||||||
rev.getFullMessage,
|
rev.getFullMessage,
|
||||||
rev.getParents().map(_.name).toList)
|
rev.getParents().map(_.name).toList)
|
||||||
|
|
||||||
val summary = {
|
val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
|
||||||
val i = fullMessage.trim.indexOf("\n")
|
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
|
||||||
val firstLine = if(i >= 0){
|
if(firstLine.length > shortMessage.length) shortMessage else firstLine
|
||||||
fullMessage.trim.substring(0, i).trim
|
|
||||||
} else {
|
|
||||||
fullMessage
|
|
||||||
}
|
|
||||||
if(firstLine.length > shortMessage.length){
|
|
||||||
shortMessage
|
|
||||||
} else {
|
|
||||||
firstLine
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val description = {
|
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
|
||||||
val i = fullMessage.trim.indexOf("\n")
|
|
||||||
if(i >= 0){
|
if(i >= 0){
|
||||||
Some(fullMessage.trim.substring(i).trim)
|
Some(fullMessage.trim.substring(i).trim)
|
||||||
} else {
|
} else None
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
|
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
|
||||||
@@ -112,24 +104,6 @@ object JGitUtil {
|
|||||||
*/
|
*/
|
||||||
case class TagInfo(name: String, time: Date, id: String)
|
case class TagInfo(name: String, time: Date, id: String)
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this method to use the Git object.
|
|
||||||
* Repository resources are released certainly after processing.
|
|
||||||
*/
|
|
||||||
def withGit[T](dir: java.io.File)(f: Git => T): T = withGit(Git.open(dir))(f)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this method to use the Git object.
|
|
||||||
* Repository resources are released certainly after processing.
|
|
||||||
*/
|
|
||||||
def withGit[T](git: Git)(f: Git => T): T = {
|
|
||||||
try {
|
|
||||||
f(git)
|
|
||||||
} finally {
|
|
||||||
git.getRepository.close
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns RevCommit from the commit or tag id.
|
* Returns RevCommit from the commit or tag id.
|
||||||
*
|
*
|
||||||
@@ -141,7 +115,7 @@ object JGitUtil {
|
|||||||
val revWalk = new RevWalk(git.getRepository)
|
val revWalk = new RevWalk(git.getRepository)
|
||||||
val revCommit = revWalk.parseAny(objectId) match {
|
val revCommit = revWalk.parseAny(objectId) match {
|
||||||
case r: RevTag => revWalk.parseCommit(r.getObject)
|
case r: RevTag => revWalk.parseCommit(r.getObject)
|
||||||
case _ => revWalk.parseCommit(objectId)
|
case _ => revWalk.parseCommit(objectId)
|
||||||
}
|
}
|
||||||
revWalk.dispose
|
revWalk.dispose
|
||||||
revCommit
|
revCommit
|
||||||
@@ -151,7 +125,7 @@ object JGitUtil {
|
|||||||
* Returns the repository information. It contains branch names and tag names.
|
* Returns the repository information. It contains branch names and tag names.
|
||||||
*/
|
*/
|
||||||
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
|
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
|
||||||
withGit(getRepositoryDir(owner, repository)){ git =>
|
using(Git.open(getRepositoryDir(owner, repository))){ git =>
|
||||||
try {
|
try {
|
||||||
// get commit count
|
// get commit count
|
||||||
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
|
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
|
||||||
@@ -188,45 +162,43 @@ object JGitUtil {
|
|||||||
* @return HTML of the file list
|
* @return HTML of the file list
|
||||||
*/
|
*/
|
||||||
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
|
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
|
||||||
val revWalk = new RevWalk(git.getRepository)
|
|
||||||
val objectId = git.getRepository.resolve(revision)
|
|
||||||
val revCommit = revWalk.parseCommit(objectId)
|
|
||||||
|
|
||||||
val treeWalk = new TreeWalk(git.getRepository)
|
|
||||||
treeWalk.addTree(revCommit.getTree)
|
|
||||||
if(path != "."){
|
|
||||||
treeWalk.setRecursive(true)
|
|
||||||
treeWalk.setFilter(new TreeFilter(){
|
|
||||||
|
|
||||||
var stopRecursive = false
|
|
||||||
|
|
||||||
def include(walker: TreeWalk): Boolean = {
|
|
||||||
val targetPath = walker.getPathString
|
|
||||||
if((path + "/").startsWith(targetPath)){
|
|
||||||
true
|
|
||||||
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
|
|
||||||
stopRecursive = true
|
|
||||||
treeWalk.setRecursive(false)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def shouldBeRecursive(): Boolean = !stopRecursive
|
|
||||||
|
|
||||||
override def clone: TreeFilter = return this
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
|
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
|
||||||
|
|
||||||
while (treeWalk.next()) {
|
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||||
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
|
val objectId = git.getRepository.resolve(revision)
|
||||||
|
val revCommit = revWalk.parseCommit(objectId)
|
||||||
|
|
||||||
|
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||||
|
treeWalk.addTree(revCommit.getTree)
|
||||||
|
if(path != "."){
|
||||||
|
treeWalk.setRecursive(true)
|
||||||
|
treeWalk.setFilter(new TreeFilter(){
|
||||||
|
|
||||||
|
var stopRecursive = false
|
||||||
|
|
||||||
|
def include(walker: TreeWalk): Boolean = {
|
||||||
|
val targetPath = walker.getPathString
|
||||||
|
if((path + "/").startsWith(targetPath)){
|
||||||
|
true
|
||||||
|
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
|
||||||
|
stopRecursive = true
|
||||||
|
treeWalk.setRecursive(false)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def shouldBeRecursive(): Boolean = !stopRecursive
|
||||||
|
|
||||||
|
override def clone: TreeFilter = return this
|
||||||
|
})
|
||||||
|
}
|
||||||
|
while (treeWalk.next()) {
|
||||||
|
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
treeWalk.release
|
|
||||||
revWalk.dispose
|
|
||||||
|
|
||||||
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
|
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
|
||||||
list.map { case (objectId, fileMode, path, name) =>
|
list.map { case (objectId, fileMode, path, name) =>
|
||||||
@@ -237,7 +209,8 @@ object JGitUtil {
|
|||||||
commits(path).getCommitterIdent.getWhen,
|
commits(path).getCommitterIdent.getWhen,
|
||||||
commits(path).getShortMessage,
|
commits(path).getShortMessage,
|
||||||
commits(path).getName,
|
commits(path).getName,
|
||||||
commits(path).getCommitterIdent.getName)
|
commits(path).getCommitterIdent.getName,
|
||||||
|
commits(path).getCommitterIdent.getEmailAddress)
|
||||||
}.sortWith { (file1, file2) =>
|
}.sortWith { (file1, file2) =>
|
||||||
(file1.isDirectory, file2.isDirectory) match {
|
(file1.isDirectory, file2.isDirectory) match {
|
||||||
case (true , false) => true
|
case (true , false) => true
|
||||||
@@ -270,25 +243,23 @@ object JGitUtil {
|
|||||||
case _ => (logs, i.hasNext)
|
case _ => (logs, i.hasNext)
|
||||||
}
|
}
|
||||||
|
|
||||||
val revWalk = new RevWalk(git.getRepository)
|
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||||
val objectId = git.getRepository.resolve(revision)
|
defining(git.getRepository.resolve(revision)){ objectId =>
|
||||||
if(objectId == null){
|
if(objectId == null){
|
||||||
Left(s"${revision} can't be resolved.")
|
Left(s"${revision} can't be resolved.")
|
||||||
} else {
|
} else {
|
||||||
revWalk.markStart(revWalk.parseCommit(objectId))
|
revWalk.markStart(revWalk.parseCommit(objectId))
|
||||||
if(path.nonEmpty){
|
if(path.nonEmpty){
|
||||||
revWalk.setRevFilter(new RevFilter(){
|
revWalk.setRevFilter(new RevFilter(){
|
||||||
def include(walk: RevWalk, commit: RevCommit): Boolean = {
|
def include(walk: RevWalk, commit: RevCommit): Boolean = {
|
||||||
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
|
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
|
||||||
|
}
|
||||||
|
override def clone(): RevFilter = this
|
||||||
|
})
|
||||||
}
|
}
|
||||||
override def clone(): RevFilter = this
|
Right(getCommitLog(revWalk.iterator, 0, Nil))
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val commits = getCommitLog(revWalk.iterator, 0, Nil)
|
|
||||||
revWalk.release
|
|
||||||
|
|
||||||
Right(commits)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,13 +279,10 @@ object JGitUtil {
|
|||||||
case false => logs
|
case false => logs
|
||||||
}
|
}
|
||||||
|
|
||||||
val revWalk = new RevWalk(git.getRepository)
|
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||||
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
|
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
|
||||||
|
getCommitLog(revWalk.iterator, Nil).reverse
|
||||||
val commits = getCommitLog(revWalk.iterator, Nil)
|
}
|
||||||
revWalk.release
|
|
||||||
|
|
||||||
commits.reverse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -370,11 +338,8 @@ object JGitUtil {
|
|||||||
if(large == false && FileUtil.isLarge(loader.getSize)){
|
if(large == false && FileUtil.isLarge(loader.getSize)){
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
val db = git.getRepository.getObjectDatabase
|
using(git.getRepository.getObjectDatabase){ db =>
|
||||||
try {
|
|
||||||
Some(db.open(id).getBytes)
|
Some(db.open(id).getBytes)
|
||||||
} finally {
|
|
||||||
db.close
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -392,34 +357,32 @@ object JGitUtil {
|
|||||||
case _ => logs
|
case _ => logs
|
||||||
}
|
}
|
||||||
|
|
||||||
val revWalk = new RevWalk(git.getRepository)
|
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||||
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
|
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
|
||||||
|
val commits = getCommitLog(revWalk.iterator, Nil)
|
||||||
val commits = getCommitLog(revWalk.iterator, Nil)
|
val revCommit = commits(0)
|
||||||
revWalk.release
|
|
||||||
|
|
||||||
val revCommit = commits(0)
|
|
||||||
|
|
||||||
if(commits.length >= 2){
|
|
||||||
// not initial commit
|
|
||||||
val oldCommit = commits(1)
|
|
||||||
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
|
|
||||||
|
|
||||||
} else {
|
if(commits.length >= 2){
|
||||||
// initial commit
|
// not initial commit
|
||||||
val walk = new TreeWalk(git.getRepository)
|
val oldCommit = commits(1)
|
||||||
walk.addTree(revCommit.getTree)
|
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
|
||||||
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
|
|
||||||
while(walk.next){
|
} else {
|
||||||
buffer.append((if(!fetchContent){
|
// initial commit
|
||||||
DiffInfo(ChangeType.ADD, null, walk.getPathString, None, None)
|
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||||
} else {
|
treeWalk.addTree(revCommit.getTree)
|
||||||
DiffInfo(ChangeType.ADD, null, walk.getPathString, None,
|
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
|
||||||
JGitUtil.getContent(git, walk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
|
while(treeWalk.next){
|
||||||
}))
|
buffer.append((if(!fetchContent){
|
||||||
|
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
|
||||||
|
} else {
|
||||||
|
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
|
||||||
|
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
(buffer.toList, None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
walk.release
|
|
||||||
(buffer.toList, None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,76 +410,88 @@ object JGitUtil {
|
|||||||
/**
|
/**
|
||||||
* Returns the list of branch names of the specified commit.
|
* Returns the list of branch names of the specified commit.
|
||||||
*/
|
*/
|
||||||
def getBranchesOfCommit(git: Git, commitId: String): List[String] = {
|
def getBranchesOfCommit(git: Git, commitId: String): List[String] =
|
||||||
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository)
|
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||||
try {
|
defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
|
||||||
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0"))
|
git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
|
||||||
|
(e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
|
||||||
git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
|
}.map { e =>
|
||||||
(e.getKey.startsWith(Constants.R_HEADS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId)))
|
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
|
||||||
}.map { e =>
|
}.toList.sorted
|
||||||
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
|
}
|
||||||
}.toList.sorted
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
walk.release
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of tags of the specified commit.
|
* Returns the list of tags of the specified commit.
|
||||||
*/
|
*/
|
||||||
def getTagsOfCommit(git: Git, commitId: String): List[String] = {
|
def getTagsOfCommit(git: Git, commitId: String): List[String] =
|
||||||
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository)
|
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||||
try {
|
defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
|
||||||
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0"))
|
git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
|
||||||
|
(e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
|
||||||
git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
|
}.map { e =>
|
||||||
(e.getKey.startsWith(Constants.R_TAGS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId)))
|
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
|
||||||
}.map { e =>
|
}.toList.sorted.reverse
|
||||||
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
|
}
|
||||||
}.toList.sorted.reverse
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
walk.release
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def initRepository(dir: java.io.File): Unit = {
|
def initRepository(dir: java.io.File): Unit =
|
||||||
val repository = new RepositoryBuilder().setGitDir(dir).setBare.build
|
using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository =>
|
||||||
try {
|
|
||||||
repository.create
|
repository.create
|
||||||
setReceivePack(repository)
|
setReceivePack(repository)
|
||||||
} finally {
|
|
||||||
repository.close
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def cloneRepository(from: java.io.File, to: java.io.File): Unit = {
|
def cloneRepository(from: java.io.File, to: java.io.File): Unit =
|
||||||
val git = Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call
|
using(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call){ git =>
|
||||||
try {
|
|
||||||
setReceivePack(git.getRepository)
|
setReceivePack(git.getRepository)
|
||||||
} finally {
|
|
||||||
git.getRepository.close
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
|
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
|
||||||
|
|
||||||
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
|
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit =
|
||||||
val config = repository.getConfig
|
defining(repository.getConfig){ config =>
|
||||||
config.setBoolean("http", null, "receivepack", true)
|
config.setBoolean("http", null, "receivepack", true)
|
||||||
config.save
|
config.save
|
||||||
}
|
}
|
||||||
|
|
||||||
def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
|
def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
|
||||||
revstr: String = ""): Option[(ObjectId, String)] = {
|
revstr: String = ""): Option[(ObjectId, String)] = {
|
||||||
Seq(
|
Seq(
|
||||||
if(revstr.isEmpty) repository.repository.defaultBranch else revstr,
|
Some(if(revstr.isEmpty) repository.repository.defaultBranch else revstr),
|
||||||
repository.branchList.head
|
repository.branchList.headOption
|
||||||
).map { rev =>
|
).flatMap {
|
||||||
(git.getRepository.resolve(rev), rev)
|
case Some(rev) => Some((git.getRepository.resolve(rev), rev))
|
||||||
|
case None => None
|
||||||
}.find(_._1 != null)
|
}.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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
81
src/main/scala/util/Keys.scala
Normal file
81
src/main/scala/util/Keys.scala
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define key strings for request attributes, session attributes or flash attributes.
|
||||||
|
*/
|
||||||
|
object Keys {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define session keys.
|
||||||
|
*/
|
||||||
|
object Session {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session key for the logged in account information.
|
||||||
|
*/
|
||||||
|
val LoginAccount = "loginAccount"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session key for the issue search condition in dashboard.
|
||||||
|
*/
|
||||||
|
val DashboardIssues = "dashboard/issues"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session key for the pull request search condition in dashboard.
|
||||||
|
*/
|
||||||
|
val DashboardPulls = "dashboard/pulls"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate session key for the issue search condition.
|
||||||
|
*/
|
||||||
|
def Issues(owner: String, name: String) = s"${owner}/${name}/issues"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate session key for the pull request search condition.
|
||||||
|
*/
|
||||||
|
def Pulls(owner: String, name: String) = s"${owner}/${name}/pulls"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate session key for the upload filename.
|
||||||
|
*/
|
||||||
|
def Upload(fileId: String) = s"upload_${fileId}"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object Flash {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flash key for the redirect URL.
|
||||||
|
*/
|
||||||
|
val Redirect = "redirect"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flash key for the information message.
|
||||||
|
*/
|
||||||
|
val Info = "info"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define request keys.
|
||||||
|
*/
|
||||||
|
object Request {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request key for the Ajax request flag.
|
||||||
|
*/
|
||||||
|
val Ajax = "AJAX"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request key for the username which is used during Git repository access.
|
||||||
|
*/
|
||||||
|
val UserName = "USER_NAME"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate request key for the request cache.
|
||||||
|
*/
|
||||||
|
def Cache(key: String) = s"cache.${key}"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import service.SystemSettingsService.Ldap
|
import util.ControlUtil._
|
||||||
import service.SystemSettingsService
|
import service.SystemSettingsService
|
||||||
import com.novell.ldap._
|
import com.novell.ldap._
|
||||||
|
import java.security.Security
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import service.SystemSettingsService.Ldap
|
import service.SystemSettingsService.Ldap
|
||||||
import scala.Some
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,72 +13,98 @@ import scala.annotation.tailrec
|
|||||||
*/
|
*/
|
||||||
object LDAPUtil {
|
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.
|
* 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(
|
bind(
|
||||||
ldapSettings.host,
|
host = ldapSettings.host,
|
||||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||||
ldapSettings.bindDN.getOrElse(""),
|
dn = ldapSettings.bindDN.getOrElse(""),
|
||||||
ldapSettings.bindPassword.getOrElse("")
|
password = ldapSettings.bindPassword.getOrElse(""),
|
||||||
) match {
|
tls = ldapSettings.tls.getOrElse(false),
|
||||||
case Some(conn) => {
|
keystore = ldapSettings.keystore.getOrElse(""),
|
||||||
withConnection(conn) { conn =>
|
error = "System LDAP authentication failed."
|
||||||
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
|
){ conn =>
|
||||||
case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
|
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
|
||||||
case None => Left("User does not exist")
|
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(
|
bind(
|
||||||
ldapSettings.host,
|
host = ldapSettings.host,
|
||||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||||
userDN,
|
dn = userDN,
|
||||||
password
|
password = password,
|
||||||
) match {
|
tls = ldapSettings.tls.getOrElse(false),
|
||||||
case Some(conn) => {
|
keystore = ldapSettings.keystore.getOrElse(""),
|
||||||
withConnection(conn) { conn =>
|
error = "User LDAP Authentication Failed."
|
||||||
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
|
){ conn =>
|
||||||
case Some(mailAddress) => Right(mailAddress)
|
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
|
||||||
case None => Left("Can't find mail address.")
|
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] = {
|
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
|
||||||
val conn: LDAPConnection = new LDAPConnection
|
(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 {
|
try {
|
||||||
|
// Connect to the server
|
||||||
conn.connect(host, port)
|
conn.connect(host, port)
|
||||||
|
|
||||||
|
if (tls) {
|
||||||
|
// Secure the connection
|
||||||
|
conn.startTLS()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind to the server
|
||||||
conn.bind(LDAP_VERSION, dn, password.getBytes)
|
conn.bind(LDAP_VERSION, dn, password.getBytes)
|
||||||
Some(conn)
|
|
||||||
|
// Execute a given function and returns a its result
|
||||||
|
f(conn)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
case e: Exception => {
|
case e: Exception => {
|
||||||
if (conn.isConnected) conn.disconnect()
|
// Provide more information if something goes wrong
|
||||||
None
|
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 {
|
* Search a specified user and returns userDN if exists.
|
||||||
f(conn)
|
*/
|
||||||
} finally {
|
|
||||||
conn.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
|
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
|
||||||
@tailrec
|
@tailrec
|
||||||
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
|
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
|
||||||
@@ -96,12 +123,20 @@ object LDAPUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = {
|
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
|
||||||
val results = conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)
|
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
|
||||||
if (results.hasMore) {
|
if(results.hasMore) {
|
||||||
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
|
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
|
||||||
} else {
|
} else None
|
||||||
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package util
|
|||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.locks.{ReentrantLock, Lock}
|
import java.util.concurrent.locks.{ReentrantLock, Lock}
|
||||||
|
import util.ControlUtil._
|
||||||
|
|
||||||
object LockUtil {
|
object LockUtil {
|
||||||
|
|
||||||
@@ -23,8 +24,7 @@ object LockUtil {
|
|||||||
/**
|
/**
|
||||||
* Synchronizes a given function which modifies the working copy of the wiki repository.
|
* Synchronizes a given function which modifies the working copy of the wiki repository.
|
||||||
*/
|
*/
|
||||||
def lock[T](key: String)(f: => T): T = {
|
def lock[T](key: String)(f: => T): T = defining(getLockObject(key)){ lock =>
|
||||||
val lock = getLockObject(key)
|
|
||||||
try {
|
try {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
f
|
f
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import app.Context
|
|||||||
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
|
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
|
||||||
import servlet.Database
|
import servlet.Database
|
||||||
import SystemSettingsService.Smtp
|
import SystemSettingsService.Smtp
|
||||||
|
import _root_.util.ControlUtil.defining
|
||||||
|
|
||||||
trait Notifier extends RepositoryService with AccountService with IssuesService {
|
trait Notifier extends RepositoryService with AccountService with IssuesService {
|
||||||
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
||||||
@@ -65,26 +66,37 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
|||||||
|
|
||||||
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
||||||
(msg: String => String)(implicit context: Context) = {
|
(msg: String => String)(implicit context: Context) = {
|
||||||
val f = future {
|
val database = Database(context.request.getServletContext)
|
||||||
val email = new HtmlEmail
|
|
||||||
email.setHostName(smtp.host)
|
|
||||||
email.setSmtpPort(smtp.port.get)
|
|
||||||
smtp.user.foreach { user =>
|
|
||||||
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
|
|
||||||
}
|
|
||||||
smtp.ssl.foreach { ssl =>
|
|
||||||
email.setSSLOnConnect(ssl)
|
|
||||||
}
|
|
||||||
email.setFrom("notifications@gitbucket.com", context.loginAccount.get.userName)
|
|
||||||
email.setHtmlMsg(msg(view.Markdown.toHtml(content, r, false, true)))
|
|
||||||
|
|
||||||
|
val f = future {
|
||||||
// TODO Can we use the Database Session in other than Transaction Filter?
|
// TODO Can we use the Database Session in other than Transaction Filter?
|
||||||
Database(context.request.getServletContext) withSession {
|
database withSession {
|
||||||
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
|
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
|
||||||
email.setSubject(s"[${r.name}] ${issue.title} (#${issueId})")
|
defining(
|
||||||
recipients(issue) {
|
s"[${r.name}] ${issue.title} (#${issueId})" ->
|
||||||
email.getToAddresses.clear
|
msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
|
||||||
email.addTo(_).send
|
recipients(issue) { to =>
|
||||||
|
val email = new HtmlEmail
|
||||||
|
email.setHostName(smtp.host)
|
||||||
|
email.setSmtpPort(smtp.port.get)
|
||||||
|
smtp.user.foreach { user =>
|
||||||
|
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
|
||||||
|
}
|
||||||
|
smtp.ssl.foreach { ssl =>
|
||||||
|
email.setSSLOnConnect(ssl)
|
||||||
|
}
|
||||||
|
smtp.fromAddress
|
||||||
|
.map (_ -> smtp.fromName.orNull)
|
||||||
|
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
|
||||||
|
.foreach { case (address, name) =>
|
||||||
|
email.setFrom(address, name)
|
||||||
|
}
|
||||||
|
email.setCharset("UTF-8")
|
||||||
|
email.setSubject(subject)
|
||||||
|
email.setHtmlMsg(msg)
|
||||||
|
|
||||||
|
email.addTo(to).send
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ package util
|
|||||||
|
|
||||||
import java.net.{URLDecoder, URLEncoder}
|
import java.net.{URLDecoder, URLEncoder}
|
||||||
import org.mozilla.universalchardet.UniversalDetector
|
import org.mozilla.universalchardet.UniversalDetector
|
||||||
|
import util.ControlUtil._
|
||||||
|
import org.apache.commons.io.input.BOMInputStream
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
|
||||||
object StringUtil {
|
object StringUtil {
|
||||||
|
|
||||||
def sha1(value: String): String = {
|
def sha1(value: String): String =
|
||||||
val md = java.security.MessageDigest.getInstance("SHA-1")
|
defining(java.security.MessageDigest.getInstance("SHA-1")){ md =>
|
||||||
md.update(value.getBytes)
|
md.update(value.getBytes)
|
||||||
md.digest.map(b => "%02x".format(b)).mkString
|
md.digest.map(b => "%02x".format(b)).mkString
|
||||||
}
|
}
|
||||||
|
|
||||||
def md5(value: String): String = {
|
def md5(value: String): String = {
|
||||||
val md = java.security.MessageDigest.getInstance("MD5")
|
val md = java.security.MessageDigest.getInstance("MD5")
|
||||||
@@ -26,15 +29,30 @@ object StringUtil {
|
|||||||
def escapeHtml(value: String): String =
|
def escapeHtml(value: String): String =
|
||||||
value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
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 = {
|
def detectEncoding(content: Array[Byte]): String =
|
||||||
val detector = new UniversalDetector(null)
|
defining(new UniversalDetector(null)){ detector =>
|
||||||
detector.handleData(content, 0, content.length)
|
detector.handleData(content, 0, content.length)
|
||||||
detector.dataEnd()
|
detector.dataEnd()
|
||||||
detector.getDetectedCharset match {
|
detector.getDetectedCharset match {
|
||||||
case null => "UTF-8"
|
case null => "UTF-8"
|
||||||
case e => e
|
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
|
package util
|
||||||
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
trait Validations {
|
trait Validations {
|
||||||
|
|
||||||
@@ -8,8 +9,8 @@ trait Validations {
|
|||||||
* Constraint for the identifier such as user name, repository name or page name.
|
* Constraint for the identifier such as user name, repository name or page name.
|
||||||
*/
|
*/
|
||||||
def identifier: Constraint = new Constraint(){
|
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\\-_]+$")){
|
if(!value.matches("^[a-zA-Z0-9\\-_.]+$")){
|
||||||
Some(s"${name} contains invalid character.")
|
Some(s"${name} contains invalid character.")
|
||||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||||
Some(s"${name} starts with invalid character.")
|
Some(s"${name} starts with invalid character.")
|
||||||
@@ -25,10 +26,7 @@ trait Validations {
|
|||||||
*/
|
*/
|
||||||
def date(constraints: Constraint*): SingleValueType[java.util.Date] =
|
def date(constraints: Constraint*): SingleValueType[java.util.Date] =
|
||||||
new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
|
new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
|
||||||
def convert(value: String): java.util.Date = {
|
def convert(value: String, messages: Messages): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
|
||||||
val formatter = new java.text.SimpleDateFormat("yyyy-MM-dd")
|
|
||||||
formatter.parse(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,31 @@ trait AvatarImageProvider { self: RequestCache =>
|
|||||||
protected def getAvatarImageHtml(userName: String, size: Int,
|
protected def getAvatarImageHtml(userName: String, size: Int,
|
||||||
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
|
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
|
||||||
|
|
||||||
val src = getAccountByUserName(userName).map { account =>
|
val src = if(mailAddress.isEmpty){
|
||||||
if(account.image.isEmpty && getSystemSettings().gravatar){
|
// by user name
|
||||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
|
getAccountByUserName(userName).map { account =>
|
||||||
} else {
|
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||||
s"""${context.path}/${userName}/_avatar"""
|
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
|
||||||
|
} else {
|
||||||
|
s"""${context.path}/${account.userName}/_avatar"""
|
||||||
|
}
|
||||||
|
} getOrElse {
|
||||||
|
s"""${context.path}/_unknown/_avatar"""
|
||||||
}
|
}
|
||||||
} getOrElse {
|
} else {
|
||||||
if(mailAddress.nonEmpty && getSystemSettings().gravatar){
|
// by mail address
|
||||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
|
getAccountByMailAddress(mailAddress).map { account =>
|
||||||
} else {
|
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||||
s"""${context.path}/${userName}/_avatar"""
|
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"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
|
||||||
|
} else {
|
||||||
|
s"""${context.path}/_unknown/_avatar"""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,20 +14,21 @@ trait LinkConverter { self: RequestCache =>
|
|||||||
// escape HTML tags
|
// escape HTML tags
|
||||||
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
|
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
|
||||||
// convert issue id to link
|
// convert issue id to link
|
||||||
.replaceBy(("(^|\\W)" + issueIdPrefix + "(\\d+)(\\W|$)").r){ m =>
|
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
||||||
if(getIssue(repository.owner, repository.name, m.group(2)).isDefined){
|
getIssue(repository.owner, repository.name, m.group(2)) match {
|
||||||
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 Some(issue) if(issue.isPullRequest)
|
||||||
} else {
|
=> Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>""")
|
||||||
Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""")
|
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
|
// 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 { _ =>
|
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
|
// 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>""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package view
|
package view
|
||||||
|
|
||||||
import util.StringUtil
|
import util.StringUtil
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Directory._
|
||||||
import org.parboiled.common.StringUtils
|
import org.parboiled.common.StringUtils
|
||||||
import org.pegdown._
|
import org.pegdown._
|
||||||
import org.pegdown.ast._
|
import org.pegdown.ast._
|
||||||
import org.pegdown.LinkRenderer.Rendering
|
import org.pegdown.LinkRenderer.Rendering
|
||||||
|
import java.text.Normalizer
|
||||||
|
import java.util.Locale
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
import service.RequestCache
|
import service.{RequestCache, WikiService}
|
||||||
|
|
||||||
object Markdown {
|
object Markdown {
|
||||||
|
|
||||||
@@ -17,11 +21,11 @@ object Markdown {
|
|||||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
|
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
|
||||||
// escape issue id
|
// escape issue id
|
||||||
val source = if(enableRefsLink){
|
val source = if(enableRefsLink){
|
||||||
markdown.replaceAll("(^|\\W)#([0-9]+)(\\W|$)", "$1issue:$2$3")
|
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
||||||
} else markdown
|
} else markdown
|
||||||
|
|
||||||
val rootNode = new PegDownProcessor(
|
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)
|
).parseMarkdown(source.toCharArray)
|
||||||
|
|
||||||
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
|
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
|
||||||
@@ -29,7 +33,7 @@ object Markdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
|
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
|
||||||
enableWikiLink: Boolean) extends LinkRenderer {
|
enableWikiLink: Boolean) extends LinkRenderer with WikiService {
|
||||||
override def render(node: WikiLinkNode): Rendering = {
|
override def render(node: WikiLinkNode): Rendering = {
|
||||||
if(enableWikiLink){
|
if(enableWikiLink){
|
||||||
try {
|
try {
|
||||||
@@ -40,8 +44,14 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
|
|||||||
} else {
|
} else {
|
||||||
(text, text)
|
(text, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
|
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
|
||||||
new Rendering(url, label)
|
|
||||||
|
if(getWikiPage(repository.owner, repository.name, page).isDefined){
|
||||||
|
new Rendering(url, label)
|
||||||
|
} else {
|
||||||
|
new Rendering(url, label).withAttribute("class", "absent")
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
|
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
|
||||||
}
|
}
|
||||||
@@ -52,7 +62,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GitBucketVerbatimSerializer extends VerbatimSerializer {
|
class GitBucketVerbatimSerializer extends VerbatimSerializer {
|
||||||
def serialize(node: VerbatimNode, printer: Printer) {
|
def serialize(node: VerbatimNode, printer: Printer): Unit = {
|
||||||
printer.println.print("<pre")
|
printer.println.print("<pre")
|
||||||
if (!StringUtils.isEmpty(node.getType)) {
|
if (!StringUtils.isEmpty(node.getType)) {
|
||||||
printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"')
|
printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"')
|
||||||
@@ -98,11 +108,26 @@ class GitBucketHtmlSerializer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def printAttribute(name: String, value: String) {
|
private def printAttribute(name: String, value: String): Unit = {
|
||||||
printer.print(' ').print(name).print('=').print('"').print(value).print('"')
|
printer.print(' ').print(name).print('=').print('"').print(value).print('"')
|
||||||
}
|
}
|
||||||
|
|
||||||
override def visit(node: TextNode) {
|
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.
|
// convert commit id and username to link.
|
||||||
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
|
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
|
||||||
|
|
||||||
@@ -112,5 +137,16 @@ class GitBucketHtmlSerializer(
|
|||||||
printWithAbbreviations(text)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import service.RequestCache
|
|||||||
* Provides helper methods for Twirl templates.
|
* Provides helper methods for Twirl templates.
|
||||||
*/
|
*/
|
||||||
object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
|
object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
|
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
|
||||||
*/
|
*/
|
||||||
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
|
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format java.util.Date to "yyyy-MM-dd".
|
* Format java.util.Date to "yyyy-MM-dd".
|
||||||
*/
|
*/
|
||||||
@@ -31,17 +31,20 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
* Converts Markdown of Wiki pages to HTML.
|
* Converts Markdown of Wiki pages to HTML.
|
||||||
*/
|
*/
|
||||||
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
|
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
|
||||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
|
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
|
||||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
|
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns <img> which displays the avatar icon.
|
* Returns <img> which displays the avatar icon for the given user name.
|
||||||
* Looks up Gravatar if avatar icon has not been configured in user settings.
|
* This method looks up Gravatar if avatar icon has not been configured in user settings.
|
||||||
*/
|
*/
|
||||||
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
|
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
|
||||||
getAvatarImageHtml(userName, size, "", tooltip)
|
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 =
|
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
|
||||||
getAvatarImageHtml(commit.committer, size, commit.mailAddress)
|
getAvatarImageHtml(commit.committer, size, commit.mailAddress)
|
||||||
|
|
||||||
@@ -58,16 +61,31 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import scala.util.matching.Regex._
|
||||||
|
implicit class RegexReplaceString(s: String) {
|
||||||
|
def replaceAll(pattern: String, replacer: (Match) => String): String = {
|
||||||
|
pattern.r.replaceAllIn(s, replacer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert link notations in the activity message.
|
||||||
|
*/
|
||||||
def activityMessage(message: String)(implicit context: app.Context): Html =
|
def activityMessage(message: String)(implicit context: app.Context): Html =
|
||||||
Html(message
|
Html(message
|
||||||
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
|
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
|
||||||
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
|
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
|
||||||
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
|
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
|
||||||
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
|
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||||
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL encode except '/'.
|
||||||
|
*/
|
||||||
|
def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/")
|
||||||
|
|
||||||
def urlEncode(value: String): String = StringUtil.urlEncode(value)
|
def urlEncode(value: String): String = StringUtil.urlEncode(value)
|
||||||
|
|
||||||
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
|
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
|
||||||
@@ -81,15 +99,40 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
/**
|
/**
|
||||||
* Generates the url to the account page.
|
* Generates the url to the account page.
|
||||||
*/
|
*/
|
||||||
def url(userName: String)(implicit context: app.Context): String =
|
def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}"
|
||||||
s"${context.path}/${userName}"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the url to the root of assets.
|
* Returns the url to the root of assets.
|
||||||
*/
|
*/
|
||||||
def assets(implicit context: app.Context): String =
|
def assets(implicit context: app.Context): String = s"${context.path}/assets"
|
||||||
s"${context.path}/assets"
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 =
|
||||||
|
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
|
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,42 +13,52 @@
|
|||||||
<div class="span6">
|
<div class="span6">
|
||||||
@if(account.isEmpty){
|
@if(account.isEmpty){
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="userName"><strong>User name</strong></label>
|
<label for="userName" class="strong">Username:</label>
|
||||||
<input type="text" name="userName" id="userName" value=""/>
|
<input type="text" name="userName" id="userName" value=""/>
|
||||||
<span id="error-userName" class="error"></span>
|
<span id="error-userName" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
}
|
}
|
||||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="password"><strong>Password</strong>
|
<label for="password" class="strong">
|
||||||
|
Password
|
||||||
@if(account.nonEmpty){
|
@if(account.nonEmpty){
|
||||||
(Input to change password)
|
(input to change password)
|
||||||
}
|
}
|
||||||
|
:
|
||||||
</label>
|
</label>
|
||||||
<input type="password" name="password" id="password" value=""/>
|
<input type="password" name="password" id="password" value=""/>
|
||||||
<span id="error-password" class="error"></span>
|
<span id="error-password" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
}
|
}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="mailAddress"><strong>Mail Address</strong></label>
|
<label for="fullName" class="strong">Full Name:</label>
|
||||||
|
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
|
||||||
|
<span id="error-fullName" class="error"></span>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label for="mailAddress" class="strong">Mail Address:</label>
|
||||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
||||||
<span id="error-mailAddress" class="error"></span>
|
<span id="error-mailAddress" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="url"><strong>URL (Optional)</strong></label>
|
<label for="url" class="strong">URL (optional):</label>
|
||||||
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
||||||
<span id="error-url" class="error"></span>
|
<span id="error-url" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="span6">
|
<div class="span6">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="avatar"><strong>Image (Optional)</strong></label>
|
<label for="avatar" class="strong">Image (optional):</label>
|
||||||
@helper.html.uploadavatar(account)
|
@helper.html.uploadavatar(account)
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fieldset class="margin">
|
<fieldset class="margin">
|
||||||
@if(account.isDefined){
|
@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"/>
|
<input type="submit" class="btn btn-success" value="Save"/>
|
||||||
<a href="@url(account.get.userName)" class="btn">Cancel</a>
|
<a href="@url(account.get.userName)" class="btn">Cancel</a>
|
||||||
} else {
|
} else {
|
||||||
@@ -57,3 +67,10 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#delete').click(function(){
|
||||||
|
return confirm('Once you delete your account, there is no going back.\nAre you sure?');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
<div class="span4">
|
<div class="span4">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="account-image">@avatar(account.userName, 200)</div>
|
<div class="account-image">@avatar(account.userName, 200)</div>
|
||||||
<div class="block-header">@account.userName</div>
|
<div class="account-fullname">@account.fullName</div>
|
||||||
|
<div class="account-username">@account.userName</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
@if(account.url.isDefined){
|
@if(account.url.isDefined){
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
|
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
|
@import util.Directory._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("System Settings"){
|
@html.main("System Settings"){
|
||||||
@menu("system"){
|
@menu("system"){
|
||||||
@@ -8,38 +9,59 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="box-header">System Settings</div>
|
<div class="box-header">System Settings</div>
|
||||||
<div class="box-content">
|
<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 -->
|
<!-- Account registration -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<label><strong>Account registration</strong></label>
|
<hr>
|
||||||
|
<label class="strong">Account registration</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label class="radio">
|
||||||
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
|
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
|
||||||
<strong>Allow</strong> - Users can create account by themselves.
|
<span class="strong">Allow</span> - Users can create accounts by themselves.
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="radio">
|
||||||
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
|
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
|
||||||
<strong>Deny</strong> - Only administrators can create account.
|
<span class="strong">Deny</span> - Only administrators can create accounts.
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<hr>
|
<hr>
|
||||||
<label><strong>Services</strong></label>
|
<label class="strong">Services</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label class="checkbox">
|
||||||
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
|
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
|
||||||
Gravatar
|
Use Gravatar for Profile-Images
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<!-- Authentication -->
|
<!-- Authentication -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<hr>
|
<hr>
|
||||||
<label><strong>Authentication</strong></label>
|
<label class="strong">Authentication</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label class="checkbox">
|
||||||
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
|
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
|
||||||
LDAP
|
LDAP
|
||||||
</label>
|
</label>
|
||||||
@@ -87,6 +109,13 @@
|
|||||||
<span id="error-ldap_userNameAttribute" class="error"></span>
|
<span id="error-ldap_userNameAttribute" class="error"></span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="control-group">
|
||||||
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
|
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@@ -94,14 +123,28 @@
|
|||||||
<span id="error-ldap_mailAttribute" class="error"></span>
|
<span id="error-ldap_mailAttribute" class="error"></span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<!-- Notification email -->
|
<!-- Notification email -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<hr>
|
<hr>
|
||||||
<label><strong>Notification email</strong></label>
|
<label class="strong">Notification email</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label class="checkbox">
|
||||||
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
|
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
|
||||||
Send notifications
|
Send notifications
|
||||||
</label>
|
</label>
|
||||||
@@ -140,6 +183,18 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label" for="fromAddress">FROM Address</label>
|
||||||
|
<div class="controls">
|
||||||
|
<input type="text" id="fromAddress" name="smtp.fromAddress" value="@settings.smtp.map(_.fromAddress)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label" for="fromName">FROM Name</label>
|
||||||
|
<div class="controls">
|
||||||
|
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,23 +7,33 @@
|
|||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span7">
|
<div class="span7">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="groupName"><strong>Group name</strong></label>
|
<label for="groupName" class="strong">Group name</label>
|
||||||
<span id="error-groupName" class="error"></span>
|
<div>
|
||||||
|
<span id="error-groupName" class="error"></span>
|
||||||
|
</div>
|
||||||
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
<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>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><strong>URL (Optional)</strong></label>
|
<label class="strong">URL (Optional)</label>
|
||||||
<span id="error-url" class="error"></span>
|
<div>
|
||||||
|
<span id="error-url" class="error"></span>
|
||||||
|
</div>
|
||||||
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
|
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="avatar"><strong>Image (Optional)</strong></label>
|
<label for="avatar" class="strong">Image (Optional)</label>
|
||||||
@helper.html.uploadavatar(account)
|
@helper.html.uploadavatar(account)
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="span5">
|
<div class="span5">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><strong>Members</strong></label>
|
<label class="strong">Members</label>
|
||||||
<ul id="members" class="collaborator">
|
<ul id="members" class="collaborator">
|
||||||
@members.map { userName =>
|
@members.map { userName =>
|
||||||
<li data-name="@userName">
|
<li data-name="@userName">
|
||||||
@@ -32,7 +42,7 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
<input type="text" id="memberName" style="width: 200px; margin-bottom: 0px;"/>
|
@helper.html.account("memberName", 200)
|
||||||
<input type="button" class="btn" value="Add" id="addMember"/>
|
<input type="button" class="btn" value="Add" id="addMember"/>
|
||||||
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
|
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
|
||||||
<div>
|
<div>
|
||||||
@@ -50,15 +60,6 @@
|
|||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
$('#memberName').typeahead({
|
|
||||||
source: function (query, process) {
|
|
||||||
return $.get('@path/_user/proposals', { query: query },
|
|
||||||
function (data) {
|
|
||||||
return process(data.options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#addMember').click(function(){
|
$('#addMember').click(function(){
|
||||||
$('#error-memberName').text('');
|
$('#error-memberName').text('');
|
||||||
var userName = $('#memberName').val();
|
var userName = $('#memberName').val();
|
||||||
|
|||||||
@@ -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 context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Manage Users"){
|
@html.main("Manage Users"){
|
||||||
@admin.html.menu("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/_newuser" class="btn">New User</a>
|
||||||
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
|
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
|
||||||
</div>
|
</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">
|
<table class="table table-bordered table-hover">
|
||||||
@users.map { account =>
|
@users.map { account =>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td @if(account.isRemoved){style="background-color: #dddddd;"}>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@if(account.isGroupAccount){
|
@if(account.isGroupAccount){
|
||||||
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
|
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
|
||||||
@@ -57,4 +61,11 @@
|
|||||||
}
|
}
|
||||||
</table>
|
</table>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#includeRemoved').click(function(){
|
||||||
|
location.href = '@path/admin/users?includeRemoved=' + this.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -6,45 +6,67 @@
|
|||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span6">
|
<div class="span6">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="userName"><strong>Username</strong></label>
|
<label for="userName" class="strong">Username:</label>
|
||||||
<span id="error-userName" class="error"></span>
|
<div>
|
||||||
|
<span id="error-userName" class="error"></span>
|
||||||
|
</div>
|
||||||
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
<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>
|
</fieldset>
|
||||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="password">
|
<label for="password" class="strong">
|
||||||
<strong>Password</strong>
|
Password
|
||||||
@if(account.isDefined){
|
@if(account.isDefined){
|
||||||
(Input to change password)
|
(Input to change password)
|
||||||
}
|
}
|
||||||
|
:
|
||||||
</label>
|
</label>
|
||||||
<span id="error-password" class="error"></span>
|
<div>
|
||||||
|
<span id="error-password" class="error"></span>
|
||||||
|
</div>
|
||||||
<input type="password" name="password" id="password" value="" autocomplete="off"/>
|
<input type="password" name="password" id="password" value="" autocomplete="off"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
}
|
}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="mailAddress"><strong>Mail Address</strong></label>
|
<label for="fullName" class="strong">Full Name:</label>
|
||||||
<span id="error-mailAddress" class="error"></span>
|
<div>
|
||||||
|
<span id="error-fullName" class="error"></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label for="mailAddress" class="strong">Mail Address:</label>
|
||||||
|
<div>
|
||||||
|
<span id="error-mailAddress" class="error"></span>
|
||||||
|
</div>
|
||||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><strong>User Type</strong></label>
|
<label class="strong">User Type:</label>
|
||||||
<label for="userType_Normal">
|
<label class="radio" for="userType_Normal">
|
||||||
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
|
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
|
||||||
</label>
|
</label>
|
||||||
<label for="userType_Admin">
|
<label class="radio" for="userType_Admin">
|
||||||
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
|
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><strong>URL (Optional)</strong></label>
|
<label class="strong">URL (Optional):</label>
|
||||||
<span id="error-url" class="error"></span>
|
<div>
|
||||||
|
<span id="error-url" class="error"></span>
|
||||||
|
</div>
|
||||||
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="span6">
|
<div class="span6">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="avatar"><strong>Image (Optional)</strong></label>
|
<label for="avatar" class="strong">Image (Optional)</label>
|
||||||
@helper.html.uploadavatar(account)
|
@helper.html.uploadavatar(account)
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,4 +77,4 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
<div class="pull-right">
|
|
||||||
<div class="input-prepend">
|
@if(repository.commitCount > 0){
|
||||||
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
|
<div class="pull-right">
|
||||||
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
|
<div class="input-prepend">
|
||||||
|
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
|
||||||
|
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
|
|
||||||
@if(repository.repository.isPrivate){
|
@if(repository.repository.isPrivate){
|
||||||
<i class="icon-lock"></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>
|
||||||
|
|
||||||
@defining(repository.repository){ x =>
|
@defining(repository.repository){ x =>
|
||||||
@if(repository.repository.originRepositoryName.isDefined){
|
@if(repository.repository.originRepositoryName.isDefined){
|
||||||
<div class="forked">
|
<div class="forked">
|
||||||
@@ -48,10 +58,8 @@
|
|||||||
<a href="@url(repository)/settings">Settings</a>
|
<a href="@url(repository)/settings">Settings</a>
|
||||||
</th>
|
</th>
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<form method="POST" id="repository_form">
|
|
||||||
</form>
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function(){
|
$(function(){
|
||||||
$('table.global-nav th.box-header').click(function(){
|
$('table.global-nav th.box-header').click(function(){
|
||||||
|
|||||||
15
src/main/twirl/helper/account.scala.html
Normal file
15
src/main/twirl/helper/account.scala.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
@(id: String, width: Int)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
<input type="text" name="@id" id="@id" style="width: @{width}px; margin-bottom: 0px;"/>
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#@id').typeahead({
|
||||||
|
source: function (query, process) {
|
||||||
|
return $.get('@path/_user/proposals', { query: query },
|
||||||
|
function (data) {
|
||||||
|
return process(data.options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
|
case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
|
||||||
case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
|
case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
|
||||||
case "create_branch" => simpleActivity(activity, "activity-branch.png")
|
case "create_branch" => simpleActivity(activity, "activity-branch.png")
|
||||||
|
case "delete_branch" => simpleActivity(activity, "activity-delete.png")
|
||||||
case "create_tag" => simpleActivity(activity, "activity-tag.png")
|
case "create_tag" => simpleActivity(activity, "activity-tag.png")
|
||||||
|
case "delete_tag" => simpleActivity(activity, "activity-delete.png")
|
||||||
case "fork" => simpleActivity(activity, "activity-fork.png")
|
case "fork" => simpleActivity(activity, "activity-fork.png")
|
||||||
case "push" => customActivity(activity, "activity-commit.png"){
|
case "push" => customActivity(activity, "activity-commit.png"){
|
||||||
<div class="small activity-message">
|
<div class="small activity-message">
|
||||||
@@ -24,10 +26,12 @@
|
|||||||
if(i == 3){
|
if(i == 3){
|
||||||
<div>...</div>
|
<div>...</div>
|
||||||
} else {
|
} else {
|
||||||
<div>
|
if(commit.nonEmpty){
|
||||||
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
|
<div>
|
||||||
<span>{commit.substring(41)}</span>
|
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
|
||||||
</div>
|
<span>{commit.substring(41)}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,39 @@
|
|||||||
@(diffs: Seq[util.JGitUtil.DiffInfo],
|
@(diffs: Seq[util.JGitUtil.DiffInfo],
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
newCommitId: Option[String],
|
newCommitId: Option[String],
|
||||||
oldCommitId: Option[String])(implicit context: app.Context)
|
oldCommitId: Option[String],
|
||||||
|
showIndex: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||||
|
@if(showIndex){
|
||||||
|
<div>
|
||||||
|
<div class="pull-right" style="margin-bottom: 10px;">
|
||||||
|
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
|
||||||
|
</div>
|
||||||
|
Showing @diffs.size changed @plural(diffs.size, "file")
|
||||||
|
</div>
|
||||||
|
<ul id="commit-file-list" style="display: none;">
|
||||||
|
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||||
|
<li@if(i > 0){ class="border"}>
|
||||||
|
<a href="#diff-@i">
|
||||||
|
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
||||||
|
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
|
||||||
|
}
|
||||||
|
@if(diff.changeType == ChangeType.ADD){
|
||||||
|
<img src="@assets/common/images/diff_add.png"/> @diff.newPath
|
||||||
|
}
|
||||||
|
@if(diff.changeType == ChangeType.MODIFY){
|
||||||
|
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath
|
||||||
|
}
|
||||||
|
@if(diff.changeType == ChangeType.DELETE){
|
||||||
|
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||||
<a name="diff-@i"></a>
|
<a name="diff-@i"></a>
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
@@ -103,6 +132,17 @@ function diffUsingJS(oldTextId, newTextId, outputId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$(function(){
|
$(function(){
|
||||||
|
@if(showIndex){
|
||||||
|
$('#toggle-file-list').click(function(){
|
||||||
|
$('#commit-file-list').toggle();
|
||||||
|
if($(this).val() == 'Show file list'){
|
||||||
|
$(this).val('Hide file list');
|
||||||
|
} else {
|
||||||
|
$(this).val('Show file list');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||||
@if(diff.newContent != None || diff.oldContent != None){
|
@if(diff.newContent != None || diff.oldContent != None){
|
||||||
if($('#oldText-@i').length > 0){
|
if($('#oldText-@i').length > 0){
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
@(buttonValue: String = "", prefix: String = "")(body: Html)
|
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html)
|
||||||
<div class="btn-group">
|
<div class="btn-group"@if(style.nonEmpty){ style="@style"}>
|
||||||
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown">
|
<button class="btn dropdown-toggle@if(mini){ btn-mini}" data-toggle="dropdown">
|
||||||
@if(buttonValue.isEmpty){
|
@if(value.isEmpty){
|
||||||
<i class="icon-cog"></i>
|
<i class="icon-cog"></i>
|
||||||
} else {
|
} else {
|
||||||
@if(prefix.nonEmpty){
|
@if(prefix.nonEmpty){
|
||||||
<span class="muted">@prefix:</span>
|
<span class="muted">@prefix:</span>
|
||||||
}
|
}
|
||||||
<strong>@buttonValue</strong>
|
<span class="strong">@value</span>
|
||||||
}
|
}
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu@if(right){ pull-right}">
|
||||||
@body
|
@body
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
|
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
|
||||||
style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context)
|
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
<div class="tabbable">
|
<div class="tabbable">
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
<script src="@assets/google-code-prettify/prettify.js"></script>
|
<script src="@assets/google-code-prettify/prettify.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
|
@if(elastic){
|
||||||
|
$('#content').elastic();
|
||||||
|
}
|
||||||
|
|
||||||
$('#preview').click(function(){
|
$('#preview').click(function(){
|
||||||
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
|
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
|
||||||
$.post('@url(repository)/_preview', {
|
$.post('@url(repository)/_preview', {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
<input type="hidden" name="fileId" value=""/>
|
<input type="hidden" name="fileId" value=""/>
|
||||||
|
@if(account.isEmpty || account.get.image.isEmpty){
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
var dropzone = new Dropzone('div#clickable', {
|
var dropzone = new Dropzone('div#clickable', {
|
||||||
@@ -32,6 +33,7 @@ $(function(){
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
}
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
div.dz-filename, div.dz-size, div.dz-progress, div.dz-success-mark, div.dz-error-mark, div.dz-error-message {
|
div.dz-filename, div.dz-size, div.dz-progress, div.dz-success-mark, div.dz-error-mark, div.dz-error-message {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -31,10 +31,19 @@
|
|||||||
@userRepositories.map { repository =>
|
@userRepositories.map { repository =>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@if(repository.owner == loginAccount.get.userName){
|
@if(repository.repository.isPrivate){
|
||||||
<a href="@url(repository)"><strong>@repository.name</strong></a>
|
<img src="@assets/common/images/repo_private.png"/>
|
||||||
} else {
|
} else {
|
||||||
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
|
@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>
|
||||||
|
} else {
|
||||||
|
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -57,7 +66,16 @@
|
|||||||
@recentRepositories.map { repository =>
|
@recentRepositories.map { repository =>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
|
@if(repository.repository.isPrivate){
|
||||||
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||||
<div class="box issue-comment-box">
|
<div class="box issue-comment-box">
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
@helper.html.preview(repository, "", false, true, "width: 680px; height: 100px;")
|
@helper.html.preview(repository, "", false, true, "width: 680px; height: 100px; max-height: 150px;", elastic = true)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
|||||||
@@ -6,21 +6,41 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@comments.map { comment =>
|
@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="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||||
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
||||||
<div class="box-header-small">
|
<div class="box-header-small">
|
||||||
<i class="icon-comment"></i>
|
<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">
|
<span class="pull-right">
|
||||||
@datetime(comment.registeredDate)
|
@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" &&
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||||
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||||
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
|
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
|
||||||
@markdown(comment.content, repository, false, true)
|
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
|
||||||
|
@defining(comment.content.substring(comment.content.length - 40)){ id =>
|
||||||
|
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
|
||||||
|
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -28,11 +48,11 @@
|
|||||||
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
|
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
|
||||||
<span class="label label-info">Merged</span>
|
<span class="label label-info">Merged</span>
|
||||||
@avatar(comment.commentedUserName, 20)
|
@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){
|
@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 {
|
} 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)
|
@datetime(comment.registeredDate)
|
||||||
</div>
|
</div>
|
||||||
@@ -42,9 +62,9 @@
|
|||||||
<span class="label label-important">Closed</span>
|
<span class="label label-important">Closed</span>
|
||||||
@avatar(comment.commentedUserName, 20)
|
@avatar(comment.commentedUserName, 20)
|
||||||
@if(issue.isPullRequest){
|
@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 {
|
} 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>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -52,7 +72,14 @@
|
|||||||
<div class="small issue-comment-action">
|
<div class="small issue-comment-action">
|
||||||
<span class="label label-success">Reopened</span>
|
<span class="label label-success">Reopened</span>
|
||||||
@avatar(comment.commentedUserName, 20)
|
@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>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,5 +96,18 @@ $(function(){
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
$('i.icon-remove-circle').click(function(){
|
||||||
|
if(confirm('Are you sure you want to delete this?')) {
|
||||||
|
var id = $(this).closest('a').data('comment-id');
|
||||||
|
$.post('@url(repository)/issue_comments/delete/' + id,
|
||||||
|
function(data){
|
||||||
|
if(data > 0) {
|
||||||
|
$('#comment-' + id).prev('div.issue-avatar-image').remove();
|
||||||
|
$('#comment-' + id).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;")
|
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px; max-height: 250px;", elastic = true)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="span3">
|
<div class="span3">
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<strong>Add Labels</strong>
|
<span class="strong">Add Labels</span>
|
||||||
<div>
|
<div>
|
||||||
<div id="label-list">
|
<div id="label-list">
|
||||||
<ul class="label-list nav nav-pills nav-stacked">
|
<ul class="label-list nav nav-pills nav-stacked">
|
||||||
@@ -97,7 +97,7 @@ $(function(){
|
|||||||
} else {
|
} else {
|
||||||
$('#label-assigned').html($('<span>')
|
$('#label-assigned').html($('<span>')
|
||||||
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
|
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
|
||||||
.append(' well be assigned'));
|
.append(' will be assigned'));
|
||||||
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
|
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
|
||||||
}
|
}
|
||||||
$('input[name=assignedUserName]').val(userName);
|
$('input[name=assignedUserName]').val(userName);
|
||||||
@@ -111,7 +111,7 @@ $(function(){
|
|||||||
if(milestoneId == ''){
|
if(milestoneId == ''){
|
||||||
$('#label-milestone').text('No milestone');
|
$('#label-milestone').text('No milestone');
|
||||||
} else {
|
} else {
|
||||||
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title)));
|
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
|
||||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||||
}
|
}
|
||||||
$('input[name=milestoneId]').val(milestoneId);
|
$('input[name=milestoneId]').val(milestoneId);
|
||||||
|
|||||||
@@ -2,15 +2,17 @@
|
|||||||
@import context._
|
@import context._
|
||||||
<span id="error-edit-content-@commentId" class="error"></span>
|
<span id="error-edit-content-@commentId" class="error"></span>
|
||||||
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||||
<input type="button" class="btn btn-small" value="Update Comment"/>
|
<div>
|
||||||
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
|
<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>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
var callback = function(data){
|
var callback = function(data){
|
||||||
$('#commentContent-@commentId').empty().html(data.content);
|
$('#commentContent-@commentId').empty().html(data.content);
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#commentContent-@commentId input.btn').click(function(){
|
$('#update-comment-@commentId').click(function(){
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '@path/@owner/@repository/issue_comments/edit/@commentId',
|
url: '@path/@owner/@repository/issue_comments/edit/@commentId',
|
||||||
type: 'POST',
|
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);
|
$.get('@path/@owner/@repository/issue_comments/_data/@commentId', callback);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,17 +2,21 @@
|
|||||||
@import context._
|
@import context._
|
||||||
<span id="error-edit-title" class="error"></span>
|
<span id="error-edit-title" class="error"></span>
|
||||||
<input type="text" style="width: 680px;" id="edit-title" value="@title"/>
|
<input type="text" style="width: 680px;" id="edit-title" value="@title"/>
|
||||||
<textarea style="width: 680px; height: 100px;" id="edit-content">@content.getOrElse("")</textarea>
|
<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"/>
|
<div>
|
||||||
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
|
<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>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
|
$('#edit-content').elastic();
|
||||||
|
|
||||||
var callback = function(data){
|
var callback = function(data){
|
||||||
$('#issueTitle').empty().text(data.title);
|
$('#issueTitle').empty().text(data.title);
|
||||||
$('#issueContent').empty().html(data.content);
|
$('#issueContent').empty().html(data.content);
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#issueContent input.btn').click(function(){
|
$('#update').click(function(){
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '@path/@owner/@repository/issues/edit/@issueId',
|
url: '@path/@owner/@repository/issues/edit/@issueId',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
@@ -27,7 +31,7 @@ $(function(){
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#issueContent a.btn').click(function(){
|
$('#cancel').click(function(){
|
||||||
$.get('@path/@owner/@repository/issues/_data/@issueId', callback);
|
$.get('@path/@owner/@repository/issues/_data/@issueId', callback);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,55 +29,11 @@
|
|||||||
}
|
}
|
||||||
<div class="small" style="text-align: center;">
|
<div class="small" style="text-align: center;">
|
||||||
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
|
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
|
||||||
<strong>@count</strong> @plural(count, "comment")
|
<span class="strong">@count</span> @plural(count, "comment")
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div style="margin-bottom: 8px;">
|
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
|
||||||
<strong>Labels</strong>
|
|
||||||
@if(hasWritePermission){
|
|
||||||
<div class="pull-right">
|
|
||||||
@helper.html.dropdown() {
|
|
||||||
@labels.map { label =>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
|
||||||
<i class="@{if(issueLabels.exists(_.labelId == label.labelId)) "icon-ok" else "icon-white"}"></i>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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,21 +14,25 @@
|
|||||||
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
|
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
|
||||||
}
|
}
|
||||||
<div class="small muted">
|
<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>
|
</div>
|
||||||
<h4 id="issueTitle">@issue.title</h4>
|
<h4 id="issueTitle">@issue.title</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="issue-info">
|
<div class="issue-info">
|
||||||
<span id="label-assigned">
|
<span id="label-assigned">
|
||||||
@issue.assignedUserName.map { userName =>
|
@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")
|
}.getOrElse("No one is assigned")
|
||||||
</span>
|
</span>
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
@helper.html.dropdown() {
|
@helper.html.dropdown() {
|
||||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||||
@collaborators.map { collaborator =>
|
@collaborators.map { collaborator =>
|
||||||
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
<li>
|
||||||
|
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
|
||||||
|
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +40,7 @@
|
|||||||
<span id="label-milestone">
|
<span id="label-milestone">
|
||||||
@issue.milestoneId.map { milestoneId =>
|
@issue.milestoneId.map { milestoneId =>
|
||||||
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
|
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
|
||||||
Milestone: <strong>@milestone.title</strong>
|
Milestone: <span class="strong">@milestone.title</span>
|
||||||
}
|
}
|
||||||
}.getOrElse("No milestone")
|
}.getOrElse("No milestone")
|
||||||
</span>
|
</span>
|
||||||
@@ -53,7 +57,7 @@
|
|||||||
@milestones.map { case (milestone, _, _) =>
|
@milestones.map { case (milestone, _, _) =>
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||||
<i class="icon-white"></i> @milestone.title
|
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
|
||||||
<div class="small" style="padding-left: 20px;">
|
<div class="small" style="padding-left: 20px;">
|
||||||
@milestone.dueDate.map { dueDate =>
|
@milestone.dueDate.map { dueDate =>
|
||||||
@if(isPast(dueDate)){
|
@if(isPast(dueDate)){
|
||||||
@@ -79,19 +83,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="issue-participants">
|
<div class="issue-participants">
|
||||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||||
<strong>@participants.size</strong> @plural(participants.size, "participant")
|
<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>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
@if(issue.assignedUserName.isDefined){
|
|
||||||
$('a.assign[data-name=@issue.assignedUserName] i').attr('class', 'icon-ok');
|
|
||||||
}
|
|
||||||
@if(issue.milestoneId.isDefined){
|
|
||||||
$('a.milestone[data-id=@issue.milestoneId] i').attr('class', 'icon-ok');
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#edit').click(function(){
|
$('#edit').click(function(){
|
||||||
$.get('@url(repository)/issues/_data/@issue.issueId',
|
$.get('@url(repository)/issues/_data/@issue.issueId',
|
||||||
{
|
{
|
||||||
@@ -138,7 +135,7 @@ $(function(){
|
|||||||
$('#label-milestone').text('No milestone');
|
$('#label-milestone').text('No milestone');
|
||||||
$('#milestone-progress-area').empty();
|
$('#milestone-progress-area').empty();
|
||||||
} else {
|
} else {
|
||||||
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title)));
|
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
|
||||||
$('#milestone-progress-area').html(data);
|
$('#milestone-progress-area').html(data);
|
||||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user