mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-05 04:56:02 +01:00
Compare commits
287 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9477cca8e8 | ||
|
|
176e427058 | ||
|
|
1bf26cacb9 | ||
|
|
711aee1c8b | ||
|
|
51be1048d5 | ||
|
|
50166f04d8 | ||
|
|
7eb6fea08b | ||
|
|
8e3c054da4 | ||
|
|
5249b67df1 | ||
|
|
dc5cb74f4d | ||
|
|
446a524b5a | ||
|
|
121110aede | ||
|
|
c76a8562ea | ||
|
|
495d8069f5 | ||
|
|
70af6d0c35 | ||
|
|
abfc6eea1e | ||
|
|
fd2f3e252c | ||
|
|
3b6d0065a2 | ||
|
|
30f072f925 | ||
|
|
72d0282613 | ||
|
|
a56f766497 | ||
|
|
fc41e282ce | ||
|
|
d3b21a4e39 | ||
|
|
84d97817e3 | ||
|
|
2c6d2176bb | ||
|
|
7840d28ed2 | ||
|
|
d4fd2e6cd6 | ||
|
|
70e8385246 | ||
|
|
3fd23f1749 | ||
|
|
a3e3aea4e0 | ||
|
|
dcfb8ad8eb | ||
|
|
35f82e91bc | ||
|
|
f1533cb168 | ||
|
|
763fbec0ca | ||
|
|
111c893d94 | ||
|
|
80c38a45c5 | ||
|
|
548860607b | ||
|
|
66b5fe7337 | ||
|
|
2b2a117912 | ||
|
|
39a10fd167 | ||
|
|
93f8dbd42a | ||
|
|
4f280d0df8 | ||
|
|
5e9ff3278a | ||
|
|
b4b68f0e17 | ||
|
|
97bd324cb0 | ||
|
|
2738406710 | ||
|
|
8e78345cbe | ||
|
|
bd9e064137 | ||
|
|
eb49365bcb | ||
|
|
de92e28c7a | ||
|
|
c18f8fd87b | ||
|
|
34272cb4c4 | ||
|
|
b3f8a02494 | ||
|
|
f767a55350 | ||
|
|
9b2c3848d9 | ||
|
|
e34d016581 | ||
|
|
b64b447b42 | ||
|
|
c6a4c13394 | ||
|
|
c8822cb4ca | ||
|
|
c05e7218f3 | ||
|
|
01872d3440 | ||
|
|
91bbb3e4dc | ||
|
|
80ebd9fb0e | ||
|
|
2ab217251a | ||
|
|
2b20f6c74c | ||
|
|
042f855cd5 | ||
|
|
720ab7e0a3 | ||
|
|
4b1b100aa5 | ||
|
|
541b7e2a79 | ||
|
|
00cd3adc7b | ||
|
|
5a97a518a6 | ||
|
|
d7219068cd | ||
|
|
a79c07f095 | ||
|
|
9f27f70c87 | ||
|
|
8fa79db368 | ||
|
|
a1efa60741 | ||
|
|
a08a212fdc | ||
|
|
6166eb3743 | ||
|
|
5194fc5f15 | ||
|
|
1a97beb8cf | ||
|
|
99d23398ad | ||
|
|
6accdefb8c | ||
|
|
98fc64deaa | ||
|
|
30a8cefc37 | ||
|
|
9d47c3ccb3 | ||
|
|
5a1ab8d485 | ||
|
|
5d526f243e | ||
|
|
d13bb47ee7 | ||
|
|
8b8c6ee861 | ||
|
|
0a2d95e434 | ||
|
|
fdd119c477 | ||
|
|
4f94ca1384 | ||
|
|
ed21ee8bdb | ||
|
|
efd257dee0 | ||
|
|
bacf391a39 | ||
|
|
e8a1543466 | ||
|
|
81c79003ec | ||
|
|
73cf9661ac | ||
|
|
6e994b0ae1 | ||
|
|
c5da975cea | ||
|
|
1f3ef962e8 | ||
|
|
f6066a0361 | ||
|
|
ace65cf261 | ||
|
|
1df537ce5c | ||
|
|
bf64f6b4f4 | ||
|
|
0283ec574d | ||
|
|
d566f64e8b | ||
|
|
bb9add9da9 | ||
|
|
75d085a2c4 | ||
|
|
4eab07ffaf | ||
|
|
cf7aaa25cd | ||
|
|
8011b22de6 | ||
|
|
a108489d71 | ||
|
|
185c132771 | ||
|
|
3b11e905a1 | ||
|
|
5a04fe7ae6 | ||
|
|
92c73062cc | ||
|
|
d07624bdc1 | ||
|
|
e1dbe80ccd | ||
|
|
6d69a52292 | ||
|
|
68af5479c8 | ||
|
|
3970eca8dc | ||
|
|
b11d36c3a5 | ||
|
|
3c53fd8618 | ||
|
|
4ca4c57fff | ||
|
|
317a6fde30 | ||
|
|
ad08d385e6 | ||
|
|
805e12aceb | ||
|
|
3a45912400 | ||
|
|
19817e2659 | ||
|
|
50dc205ef7 | ||
|
|
2402a3ac72 | ||
|
|
e1c155d09d | ||
|
|
84c3bc4ad4 | ||
|
|
353784c23e | ||
|
|
a359624f01 | ||
|
|
a0f684cfdf | ||
|
|
1ea1e74a0c | ||
|
|
8f7c5fc922 | ||
|
|
667ef680c1 | ||
|
|
972ab0df50 | ||
|
|
1fddc01f6e | ||
|
|
bb2e77d899 | ||
|
|
a3daf13c15 | ||
|
|
fb2b2e37ce | ||
|
|
c1381179aa | ||
|
|
9e2dc3f892 | ||
|
|
5aa548d613 | ||
|
|
5225a95d3a | ||
|
|
53b7a1fce5 | ||
|
|
02369a4949 | ||
|
|
1ca548991b | ||
|
|
0772070523 | ||
|
|
4bf3848856 | ||
|
|
512425de4c | ||
|
|
7f28bd6a26 | ||
|
|
4088b2c1e8 | ||
|
|
919d55c002 | ||
|
|
068bbd0c3b | ||
|
|
9f50528192 | ||
|
|
4c149cf01c | ||
|
|
c86c706406 | ||
|
|
3b0a0f55b5 | ||
|
|
4232b8184e | ||
|
|
e5f3dfe293 | ||
|
|
22af94d36a | ||
|
|
d6b6781861 | ||
|
|
2222299793 | ||
|
|
fdd9a184b5 | ||
|
|
99492e3f8e | ||
|
|
a42c40bbc1 | ||
|
|
2794f9fcfc | ||
|
|
28c0262e74 | ||
|
|
8634191bd2 | ||
|
|
f73c86d533 | ||
|
|
f042d709ac | ||
|
|
e2a6149a93 | ||
|
|
b2a7e2c7e2 | ||
|
|
89fc143075 | ||
|
|
a754a92799 | ||
|
|
dc26fcf609 | ||
|
|
b9db57eeef | ||
|
|
9b377c727d | ||
|
|
e5b8d81bb4 | ||
|
|
c85b31a7d5 | ||
|
|
6580e5458a | ||
|
|
4e4e65eaa6 | ||
|
|
9d19aad384 | ||
|
|
c16a9f234b | ||
|
|
ace551c33d | ||
|
|
1e6e686692 | ||
|
|
afdcc3f7c0 | ||
|
|
00e64bc46c | ||
|
|
a959e1820f | ||
|
|
3dfbdbfe51 | ||
|
|
5c46dc0bd3 | ||
|
|
db60db674f | ||
|
|
687a4f14e1 | ||
|
|
bb10365b8b | ||
|
|
74ed3bf6a0 | ||
|
|
d1d7fdc488 | ||
|
|
67775a4c62 | ||
|
|
317b5cb096 | ||
|
|
2929517d7e | ||
|
|
51e788396d | ||
|
|
1321653bf6 | ||
|
|
3899854854 | ||
|
|
c0ca842ba7 | ||
|
|
24b05d28db | ||
|
|
f0268b105c | ||
|
|
0a46e180a9 | ||
|
|
e6a215a9c3 | ||
|
|
8ca7117065 | ||
|
|
ba0a07b835 | ||
|
|
4a35b65c2c | ||
|
|
836fa47812 | ||
|
|
5b658ef6ff | ||
|
|
e9ff24d9a7 | ||
|
|
a92051a4c3 | ||
|
|
77b3650580 | ||
|
|
67ee6857ad | ||
|
|
5ab15c0a14 | ||
|
|
96a3f2c301 | ||
|
|
85707264c4 | ||
|
|
ed05422ea8 | ||
|
|
8f10c8051e | ||
|
|
41fff399b5 | ||
|
|
9e237647b0 | ||
|
|
1be53c6746 | ||
|
|
f2368b03c0 | ||
|
|
95284c0b36 | ||
|
|
f1e21a93fb | ||
|
|
689811f659 | ||
|
|
33ccb4e98c | ||
|
|
29f6e98f9c | ||
|
|
775c8cc064 | ||
|
|
9a974d047c | ||
|
|
3fd252d2db | ||
|
|
43edb034c8 | ||
|
|
e629ca391e | ||
|
|
0db7eba3f2 | ||
|
|
a472abc88e | ||
|
|
861c619c19 | ||
|
|
124f331963 | ||
|
|
76fcd44191 | ||
|
|
2124c0b88c | ||
|
|
2f5ab8e3b9 | ||
|
|
32c8b6914b | ||
|
|
7e9d940f64 | ||
|
|
59e826b630 | ||
|
|
88f3ee4b13 | ||
|
|
b7380a084e | ||
|
|
cb65e790ae | ||
|
|
573eabee93 | ||
|
|
f0d4c6546a | ||
|
|
b0943c87c8 | ||
|
|
4f1208ea98 | ||
|
|
02f16639ea | ||
|
|
cd5e28c0b8 | ||
|
|
6f7579f8d9 | ||
|
|
f1d2a71b49 | ||
|
|
fd4fe0dc0d | ||
|
|
3d5e4a4225 | ||
|
|
10ffb452d0 | ||
|
|
3218bddbdc | ||
|
|
59f78dcbcb | ||
|
|
0fe062a02f | ||
|
|
869eaf8cfd | ||
|
|
5b445c9736 | ||
|
|
6f3f3eaa99 | ||
|
|
a23ce92676 | ||
|
|
ad55d5199d | ||
|
|
bd05895761 | ||
|
|
c68dd6c891 | ||
|
|
daae9ae1e7 | ||
|
|
33dde75ae1 | ||
|
|
b2e1e1796d | ||
|
|
1ff68111b4 | ||
|
|
dff816324d | ||
|
|
a33e2c6e36 | ||
|
|
75ef82d18a | ||
|
|
eb3c522122 | ||
|
|
6c8bcfc62e | ||
|
|
70c386a934 | ||
|
|
08eb21844a | ||
|
|
7b37d6b571 | ||
|
|
f52bd2bcc0 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
*.class
|
*.class
|
||||||
*.log
|
*.log
|
||||||
|
.ensime
|
||||||
|
.ensime_cache
|
||||||
|
|
||||||
# sbt specific
|
# sbt specific
|
||||||
dist/*
|
dist/*
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
language: scala
|
language: scala
|
||||||
sudo: false
|
sudo: false
|
||||||
script:
|
script:
|
||||||
- . env.sh
|
|
||||||
- sbt test
|
- sbt test
|
||||||
|
jdk:
|
||||||
|
- oraclejdk8
|
||||||
|
|||||||
7
CONTRIBUTING.md
Normal file
7
CONTRIBUTING.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Guideline for Issues
|
||||||
|
|
||||||
|
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue.
|
||||||
|
- Make sure check whether there is a same question or request in the past.
|
||||||
|
- When raise a new issue, write subject in **English** at least.
|
||||||
|
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
||||||
|
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
|
||||||
81
README.md
81
README.md
@@ -1,8 +1,7 @@
|
|||||||
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://travis-ci.org/takezoe/gitbucket)
|
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://travis-ci.org/gitbucket/gitbucket)
|
||||||
=========
|
=========
|
||||||
|
|
||||||
GitBucket is the easily installable GitHub clone powered by Scala.
|
GitBucket is a GitHub clone powered by Scala which has easy installation and high extensibility.
|
||||||
|
|
||||||
|
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
@@ -14,29 +13,22 @@ The current version of GitBucket provides a basic features below:
|
|||||||
- Wiki
|
- Wiki
|
||||||
- Issues
|
- Issues
|
||||||
- Fork / Pull request
|
- Fork / Pull request
|
||||||
- Mail notification
|
- Email notification
|
||||||
- Activity timeline
|
- Activity timeline
|
||||||
- User management (for Administrators)
|
- Simple user and group management with LDAP integration
|
||||||
- Group (like Organization in Github)
|
|
||||||
- LDAP integration
|
|
||||||
- Gravatar support
|
- Gravatar support
|
||||||
|
- Plug-in system
|
||||||
|
|
||||||
Following features are not implemented, but we will make them in the future release!
|
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/gitbucket/gitbucket/wiki).
|
||||||
|
|
||||||
- Network graph
|
|
||||||
- Statistics
|
|
||||||
- Watch / Star
|
|
||||||
|
|
||||||
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
|
|
||||||
|
|
||||||
Installation
|
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/gitbucket/gitbucket/releases).
|
||||||
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
|
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
|
||||||
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
|
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
|
||||||
|
|
||||||
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx)
|
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nginx)
|
||||||
|
|
||||||
The default administrator account is **root** and password is **root**.
|
The default administrator account is **root** and password is **root**.
|
||||||
|
|
||||||
@@ -47,9 +39,9 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
|
|||||||
- --host=[HOSTNAME]
|
- --host=[HOSTNAME]
|
||||||
- --gitbucket.home=[DATA_DIR]
|
- --gitbucket.home=[DATA_DIR]
|
||||||
|
|
||||||
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
|
To upgrade GitBucket, only replace gitbucket.war after stop GitBucket. 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)
|
For Installation on Windows Server with IIS see [this wiki page](https://github.com/gitbucket/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
|
||||||
|
|
||||||
### Mac OS X
|
### Mac OS X
|
||||||
#### Installing Via Homebrew
|
#### Installing Via Homebrew
|
||||||
@@ -72,15 +64,66 @@ Or, if you don't want/need launchctl, you can just run:
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Manual Installation
|
#### Manual Installation
|
||||||
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
|
On OS X, generate `gitbucket.plist` by [this script](https://raw.githubusercontent.com/gitbucket/gitbucket/master/contrib/macosx/makePlist) and copy it to `~/Library/LaunchAgents/`
|
||||||
|
|
||||||
Run the following commands in `Terminal` to
|
Run the following commands in `Terminal` to
|
||||||
|
|
||||||
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
|
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
|
||||||
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
|
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
|
||||||
|
|
||||||
|
Plug-ins
|
||||||
|
--------
|
||||||
|
GitBucket has the plug-in system to extend GitBucket from outside of GitBucket. Some plug-ins are available now:
|
||||||
|
|
||||||
|
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
|
||||||
|
- [gitbucket-announce-plugin](https://github.com/gitbucket-plugins/gitbucket-announce-plugin)
|
||||||
|
- [gitbucket-h2-backup-plugin](https://github.com/gitbucket-plugins/gitbucket-h2-backup-plugin)
|
||||||
|
- [gitbucket-desktopnotify-plugin](https://github.com/yoshiyoshifujii/gitbucket-desktopnotify-plugin)
|
||||||
|
- [gitbucket-commitgraphs-plugin](https://github.com/yoshiyoshifujii/gitbucket-commitgraphs-plugin)
|
||||||
|
- [gitbucket-asciidoctor-plugin](https://github.com/lefou/gitbucket-asciidoctor-plugin)
|
||||||
|
|
||||||
|
You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
|
||||||
|
|
||||||
|
Support
|
||||||
|
--------
|
||||||
|
|
||||||
|
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue.
|
||||||
|
- Make sure check whether there is a same question or request in the past.
|
||||||
|
- When raise a new issue, write subject in **English** at least.
|
||||||
|
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
||||||
|
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
|
||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
--------
|
--------
|
||||||
|
### 3.9 - 5 Dec 2015
|
||||||
|
- GFM inline breaks support in Markdown
|
||||||
|
- WebHook on create review comment is available
|
||||||
|
- WebHook event trigger is selectable
|
||||||
|
|
||||||
|
### 3.8 - 31 Oct 2015
|
||||||
|
- Moved to GitHub organization
|
||||||
|
- Omit diff view for large differences
|
||||||
|
- Repository creation API
|
||||||
|
- Render url as link in repository description
|
||||||
|
- Expand attachable file types
|
||||||
|
|
||||||
|
### 3.7 - 3 Oct 2015
|
||||||
|
- Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown
|
||||||
|
- Clone in desktop button
|
||||||
|
- Providing MD5 and SHA-1 checksum for `gitbucket.war` has started
|
||||||
|
|
||||||
|
### 3.6 - 30 Aug 2015
|
||||||
|
- User interface Improvements: Especially, commit list, issues and pull request have been updated largely.
|
||||||
|
- Installed plugins list has been available at the system administration console.
|
||||||
|
- Pages and repository list in the sidebar have been limited and more pages and repositories link is available.
|
||||||
|
- More reference link notation in Markdown has been supported.
|
||||||
|
|
||||||
|
### 3.5 - 1 Aug 2015
|
||||||
|
- Octicons has been applied
|
||||||
|
- Global header has been enhanced. Now it's further similar to GitHub.
|
||||||
|
- Default compare / pull request target has been changed to the parent repository
|
||||||
|
- A lot of updates for [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
|
||||||
|
|
||||||
### 3.4 - 27 Jun 2015
|
### 3.4 - 27 Jun 2015
|
||||||
- Declarative style plug-in definition
|
- Declarative style plug-in definition
|
||||||
- New extension point to add markup render
|
- New extension point to add markup render
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
|
|||||||
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
|
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
|
||||||
|
|
||||||
# GitBucket version to fetch when installing
|
# GitBucket version to fetch when installing
|
||||||
GITBUCKET_VERSION=2.1
|
GITBUCKET_VERSION=3.5
|
||||||
|
|
||||||
#
|
#
|
||||||
# End of configuration section. Ignore this part
|
# End of configuration section. Ignore this part
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ createDir "$GITBUCKET_DIR"
|
|||||||
createDir "$GITBUCKET_LOG_DIR"
|
createDir "$GITBUCKET_LOG_DIR"
|
||||||
|
|
||||||
echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
|
echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
|
||||||
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
|
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/gitbucket/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
|
||||||
|
|
||||||
sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
|
sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Summary: GitHub clone written with Scala.
|
|||||||
Version: 2.6
|
Version: 2.6
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
License: Apache
|
License: Apache
|
||||||
URL: https://github.com/takezoe/gitbucket
|
URL: https://github.com/gitbucket/gitbucket
|
||||||
Group: System/Servers
|
Group: System/Servers
|
||||||
Source0: %{name}.war
|
Source0: %{name}.war
|
||||||
Source1: %{name}.init
|
Source1: %{name}.init
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Automatic Schema Updating
|
|||||||
========
|
========
|
||||||
GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading.
|
GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading.
|
||||||
|
|
||||||
To release a new version of GitBucket, add the version definition to the [servlet.AutoUpdate](https://github.com/takezoe/gitbucket/blob/master/src/main/scala/servlet/AutoUpdateListener.scala) at first.
|
To release a new version of GitBucket, add the version definition to the [gitbucket.core.servlet.AutoUpdate](https://github.com/gitbucket/gitbucket/blob/master/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala) at first.
|
||||||
|
|
||||||
```scala
|
```scala
|
||||||
object AutoUpdate {
|
object AutoUpdate {
|
||||||
@@ -16,7 +16,7 @@ object AutoUpdate {
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/takezoe/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```.
|
Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/gitbucket/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```.
|
||||||
|
|
||||||
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.
|
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ for Developers
|
|||||||
--------
|
--------
|
||||||
If you want to modify source code and confirm it, you can run GitBucket in auto reloading mode as following:
|
If you want to modify source code and confirm it, you can run GitBucket in auto reloading mode as following:
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
```
|
```
|
||||||
C:\gitbucket> sbt
|
C:\gitbucket> sbt
|
||||||
...
|
...
|
||||||
@@ -24,15 +26,38 @@ C:\gitbucket> sbt
|
|||||||
> ~ ;copy-resources;aux-compile
|
> ~ ;copy-resources;aux-compile
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/gitbucket$ ./sbt.sh
|
||||||
|
...
|
||||||
|
> container:start
|
||||||
|
...
|
||||||
|
> ~ ;copy-resources;aux-compile
|
||||||
|
```
|
||||||
|
|
||||||
Build war file
|
Build war file
|
||||||
--------
|
--------
|
||||||
|
|
||||||
To build war file, run the following command:
|
To build war file, run the following command:
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
```
|
```
|
||||||
C:\gitbucket> sbt package
|
C:\gitbucket> sbt package
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/gitbucket$ ./sbt.sh package
|
||||||
|
```
|
||||||
|
|
||||||
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
|
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
|
||||||
|
|
||||||
To build executable war file, run Ant at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact. Please note the current build.xml works on Windows only. Replace `sbt.bat` with `sbt.sh` in build.xml if you want to run it on Linux.
|
To build executable war file, run
|
||||||
|
|
||||||
|
* Windows: Not available
|
||||||
|
* Linux: `./release/make-release-war.sh`
|
||||||
|
|
||||||
|
at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact.
|
||||||
|
|||||||
@@ -379,21 +379,21 @@
|
|||||||
<path d="M588.909,926.673 L560.094,910.545 L564.713,935.73 L588.909,926.673 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1.313" stroke-linecap="round"/>
|
<path d="M588.909,926.673 L560.094,910.545 L564.713,935.73 L588.909,926.673 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1.313" stroke-linecap="round"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="rect3075-11">
|
<g id="rect3075-11">
|
||||||
<path d="M779.562,898.094 C779.396,912.75 779.229,927.406 779.062,942.062 C781.26,942.084 783.458,942.104 785.656,942.125 C784.104,943.688 782.552,945.25 781,946.813 C801.708,967.521 822.417,988.229 843.125,1008.938 C858.531,993.531 873.938,978.125 889.344,962.719 C868.635,942 847.927,921.281 827.219,900.563 C825.49,902.302 823.76,904.042 822.031,905.781 C822.052,903.386 822.073,900.99 822.094,898.594 C807.917,898.427 793.74,898.261 779.563,898.094 z" fill="#FFFFFF"/>
|
<path d="M1389.845,733.625 C1389.679,748.281 1389.512,762.937 1389.345,777.593 C1391.543,777.615 1393.741,777.635 1395.939,777.656 C1394.387,779.219 1392.835,780.781 1391.283,782.344 C1411.991,803.052 1432.7,823.76 1453.408,844.469 C1468.814,829.062 1484.221,813.656 1499.627,798.25 C1478.918,777.531 1458.21,756.812 1437.502,736.094 C1435.773,737.833 1434.043,739.573 1432.314,741.312 C1432.335,738.917 1432.356,736.521 1432.377,734.125 C1418.2,733.958 1404.023,733.792 1389.846,733.625 z" fill="#FFFFFF"/>
|
||||||
<path d="M779.562,898.094 C779.396,912.75 779.229,927.406 779.062,942.062 C781.26,942.084 783.458,942.104 785.656,942.125 C784.104,943.688 782.552,945.25 781,946.813 C801.708,967.521 822.417,988.229 843.125,1008.938 C858.531,993.531 873.938,978.125 889.344,962.719 C868.635,942 847.927,921.281 827.219,900.563 C825.49,902.302 823.76,904.042 822.031,905.781 C822.052,903.386 822.073,900.99 822.094,898.594 C807.917,898.427 793.74,898.261 779.563,898.094 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9" stroke-linecap="round"/>
|
<path d="M1389.845,733.625 C1389.679,748.281 1389.512,762.937 1389.345,777.593 C1391.543,777.615 1393.741,777.635 1395.939,777.656 C1394.387,779.219 1392.835,780.781 1391.283,782.344 C1411.991,803.052 1432.7,823.76 1453.408,844.469 C1468.814,829.062 1484.221,813.656 1499.627,798.25 C1478.918,777.531 1458.21,756.812 1437.502,736.094 C1435.773,737.833 1434.043,739.573 1432.314,741.312 C1432.335,738.917 1432.356,736.521 1432.377,734.125 C1418.2,733.958 1404.023,733.792 1389.846,733.625 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9" stroke-linecap="round"/>
|
||||||
</g>
|
</g>
|
||||||
<path d="M606.483,964.91 L606.483,951.243 L672.089,951.243 L672.089,964.91 z" fill="#B3B3B3" id="rect2995-0-2-8-6"/>
|
<path d="M606.483,964.91 L606.483,951.243 L672.089,951.243 L672.089,964.91 z" fill="#B3B3B3" id="rect2995-0-2-8-6"/>
|
||||||
<g id="rect3075-11-7">
|
<g id="rect3075-11-7">
|
||||||
<path d="M786.383,905.075 C786.256,916.229 786.129,927.383 786.003,938.537 C787.675,938.553 789.348,938.568 791.021,938.584 C789.839,939.773 788.658,940.963 787.477,942.152 C803.237,957.911 818.997,973.671 834.756,989.431 C846.481,977.706 858.206,965.982 869.93,954.257 C854.171,938.489 838.411,922.722 822.651,906.954 C821.335,908.278 820.019,909.602 818.703,910.926 C818.719,909.102 818.734,907.279 818.751,905.456 C807.961,905.329 797.172,905.202 786.383,905.075 z" fill="#FFFFFF"/>
|
<path d="M1396.666,740.606 C1396.539,751.76 1396.412,762.914 1396.286,774.068 C1397.958,774.084 1399.631,774.099 1401.304,774.115 C1400.122,775.304 1398.941,776.494 1397.76,777.683 C1413.52,793.442 1429.28,809.202 1445.039,824.962 C1456.764,813.237 1468.489,801.513 1480.213,789.788 C1464.454,774.02 1448.694,758.253 1432.934,742.485 C1431.618,743.809 1430.302,745.133 1428.986,746.457 C1429.002,744.633 1429.017,742.81 1429.034,740.987 C1418.244,740.86 1407.455,740.733 1396.666,740.606 z" fill="#FFFFFF"/>
|
||||||
<path d="M786.383,905.075 C786.256,916.229 786.129,927.383 786.003,938.537 C787.675,938.553 789.348,938.568 791.021,938.584 C789.839,939.773 788.658,940.963 787.477,942.152 C803.237,957.911 818.997,973.671 834.756,989.431 C846.481,977.706 858.206,965.982 869.93,954.257 C854.171,938.489 838.411,922.722 822.651,906.954 C821.335,908.278 820.019,909.602 818.703,910.926 C818.719,909.102 818.734,907.279 818.751,905.456 C807.961,905.329 797.172,905.202 786.383,905.075 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="6.849" stroke-linecap="round"/>
|
<path d="M1396.666,740.606 C1396.539,751.76 1396.412,762.914 1396.286,774.068 C1397.958,774.084 1399.631,774.099 1401.304,774.115 C1400.122,775.304 1398.941,776.494 1397.76,777.683 C1413.52,793.442 1429.28,809.202 1445.039,824.962 C1456.764,813.237 1468.489,801.513 1480.213,789.788 C1464.454,774.02 1448.694,758.253 1432.934,742.485 C1431.618,743.809 1430.302,745.133 1428.986,746.457 C1429.002,744.633 1429.017,742.81 1429.034,740.987 C1418.244,740.86 1407.455,740.733 1396.666,740.606 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="6.849" stroke-linecap="round"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="path3100-2">
|
<g id="path3100-2">
|
||||||
<path d="M813.748,916.688 C818.255,921.195 818.255,928.501 813.748,933.008 C809.242,937.514 801.935,937.514 797.429,933.008 C792.922,928.501 792.922,921.195 797.429,916.688 C801.935,912.182 809.242,912.182 813.748,916.688 z" fill="#FFFFFF"/>
|
<path d="M1424.031,752.219 C1428.538,756.726 1428.538,764.032 1424.031,768.539 C1419.525,773.045 1412.218,773.045 1407.712,768.539 C1403.205,764.032 1403.205,756.726 1407.712,752.219 C1412.218,747.713 1419.525,747.713 1424.031,752.219 z" fill="#FFFFFF"/>
|
||||||
<path d="M813.748,916.688 C818.255,921.195 818.255,928.501 813.748,933.008 C809.242,937.514 801.935,937.514 797.429,933.008 C792.922,928.501 792.922,921.195 797.429,916.688 C801.935,912.182 809.242,912.182 813.748,916.688 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.585" stroke-linecap="round"/>
|
<path d="M1424.031,752.219 C1428.538,756.726 1428.538,764.032 1424.031,768.539 C1419.525,773.045 1412.218,773.045 1407.712,768.539 C1403.205,764.032 1403.205,756.726 1407.712,752.219 C1412.218,747.713 1419.525,747.713 1424.031,752.219 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.585" stroke-linecap="round"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="rect4114">
|
<g id="rect4114">
|
||||||
<path d="M813.845,955.107 L834.888,934.064 L864.012,963.188 L842.969,984.231 z" fill="#FFFFFF"/>
|
<path d="M1424.128,790.638 L1445.171,769.595 L1474.295,798.719 L1453.252,819.762 z" fill="#FFFFFF"/>
|
||||||
<path d="M813.845,955.107 L834.888,934.064 L864.012,963.188 L842.969,984.231 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.133"/>
|
<path d="M1424.128,790.638 L1445.171,769.595 L1474.295,798.719 L1453.252,819.762 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.133"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="path2991-7-6">
|
<g id="path2991-7-6">
|
||||||
<path d="M969.889,84.636 C969.889,122.652 939.072,153.469 901.056,153.469 C863.04,153.469 832.223,122.652 832.223,84.636 C832.223,46.62 863.04,15.803 901.056,15.803 C939.072,15.803 969.889,46.62 969.889,84.636 z" fill="#A0A0A0"/>
|
<path d="M969.889,84.636 C969.889,122.652 939.072,153.469 901.056,153.469 C863.04,153.469 832.223,122.652 832.223,84.636 C832.223,46.62 863.04,15.803 901.056,15.803 C939.072,15.803 969.889,46.62 969.889,84.636 z" fill="#A0A0A0"/>
|
||||||
@@ -750,5 +750,45 @@
|
|||||||
</g>
|
</g>
|
||||||
<path d="M1396.792,592.168 C1426.908,592.168 1450.613,610.989 1450.613,639.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
|
<path d="M1396.792,592.168 C1426.908,592.168 1450.613,610.989 1450.613,639.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
|
||||||
<path d="M1397.792,545.653 C1453.613,544.493 1499.627,588.735 1499.627,636.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
|
<path d="M1397.792,545.653 C1453.613,544.493 1499.627,588.735 1499.627,636.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
|
||||||
|
<path d="M871.125,1039.025 C871.125,1039.025 873.794,1016.889 908.043,1011.524 C919.748,1009.691 945.861,1005.107 945.861,978.522" fill-opacity="0" stroke="#A0A0A0" stroke-width="17.059" id="path3207"/>
|
||||||
|
<g id="rect3818-4-8-4">
|
||||||
|
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill="#A0A0A0"/>
|
||||||
|
<g>
|
||||||
|
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill="#A0A0A0"/>
|
||||||
|
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="15"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="path3795-4-8-7-8">
|
||||||
|
<path d="M888.074,1051.656 C888.074,1060.906 880.5,1068.405 871.159,1068.405 C861.817,1068.405 854.243,1060.906 854.243,1051.656 C854.243,1042.406 861.817,1034.908 871.159,1034.908 C880.5,1034.908 888.074,1042.406 888.074,1051.656 z" fill="#FFFFFF"/>
|
||||||
|
<path d="M888.074,1051.656 C888.074,1060.906 880.5,1068.405 871.159,1068.405 C861.817,1068.405 854.243,1060.906 854.243,1051.656 C854.243,1042.406 861.817,1034.908 871.159,1034.908 C880.5,1034.908 888.074,1042.406 888.074,1051.656 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
|
||||||
|
</g>
|
||||||
|
<g id="path3795-8-4-8">
|
||||||
|
<path d="M886.883,935.155 C886.883,944.404 879.31,951.903 869.968,951.903 C860.626,951.903 853.054,944.404 853.054,935.155 C853.054,925.904 860.626,918.405 869.968,918.405 C879.31,918.405 886.883,925.904 886.883,935.155 z" fill="#FFFFFF"/>
|
||||||
|
<path d="M886.883,935.155 C886.883,944.404 879.31,951.903 869.968,951.903 C860.626,951.903 853.054,944.404 853.054,935.155 C853.054,925.904 860.626,918.405 869.968,918.405 C879.31,918.405 886.883,925.904 886.883,935.155 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
|
||||||
|
</g>
|
||||||
|
<g id="path3795-8-4-8-2">
|
||||||
|
<path d="M965.046,971.602 C965.046,980.852 957.472,988.351 948.13,988.351 C938.789,988.351 931.215,980.852 931.215,971.602 C931.215,962.352 938.789,954.854 948.13,954.854 C957.472,954.854 965.046,962.352 965.046,971.602 z" fill="#FFFFFF"/>
|
||||||
|
<path d="M965.046,971.602 C965.046,980.852 957.472,988.351 948.13,988.351 C938.789,988.351 931.215,980.852 931.215,971.602 C931.215,962.352 938.789,954.854 948.13,954.854 C957.472,954.854 965.046,962.352 965.046,971.602 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
|
||||||
|
</g>
|
||||||
|
<path d="M1114.353,1042.412 C1114.353,1042.412 1117.022,1020.275 1151.271,1014.91 C1162.976,1013.077 1189.089,1008.493 1189.089,981.909" fill-opacity="0" stroke="#000000" stroke-width="17.059" id="path3207"/>
|
||||||
|
<g id="rect3818-4-8-4">
|
||||||
|
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill="#A0A0A0"/>
|
||||||
|
<g>
|
||||||
|
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill="#A0A0A0"/>
|
||||||
|
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill-opacity="0" stroke="#000000" stroke-width="15"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="path3795-4-8-7-8">
|
||||||
|
<path d="M1131.302,1055.043 C1131.302,1064.293 1123.728,1071.791 1114.386,1071.791 C1105.045,1071.791 1097.471,1064.293 1097.471,1055.043 C1097.471,1045.792 1105.045,1038.294 1114.386,1038.294 C1123.728,1038.294 1131.302,1045.792 1131.302,1055.043 z" fill="#FFFFFF"/>
|
||||||
|
<path d="M1131.302,1055.043 C1131.302,1064.293 1123.728,1071.791 1114.386,1071.791 C1105.045,1071.791 1097.471,1064.293 1097.471,1055.043 C1097.471,1045.792 1105.045,1038.294 1114.386,1038.294 C1123.728,1038.294 1131.302,1045.792 1131.302,1055.043 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
|
||||||
|
</g>
|
||||||
|
<g id="path3795-8-4-8">
|
||||||
|
<path d="M1130.111,938.542 C1130.111,947.791 1122.537,955.29 1113.196,955.29 C1103.854,955.29 1096.282,947.791 1096.282,938.542 C1096.282,929.291 1103.854,921.792 1113.196,921.792 C1122.537,921.792 1130.111,929.291 1130.111,938.542 z" fill="#FFFFFF"/>
|
||||||
|
<path d="M1130.111,938.542 C1130.111,947.791 1122.537,955.29 1113.196,955.29 C1103.854,955.29 1096.282,947.791 1096.282,938.542 C1096.282,929.291 1103.854,921.792 1113.196,921.792 C1122.537,921.792 1130.111,929.291 1130.111,938.542 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
|
||||||
|
</g>
|
||||||
|
<g id="path3795-8-4-8-2">
|
||||||
|
<path d="M1208.274,974.989 C1208.274,984.239 1200.7,991.738 1191.358,991.738 C1182.016,991.738 1174.443,984.239 1174.443,974.989 C1174.443,965.739 1182.016,958.241 1191.358,958.241 C1200.7,958.241 1208.274,965.739 1208.274,974.989 z" fill="#FFFFFF"/>
|
||||||
|
<path d="M1208.274,974.989 C1208.274,984.239 1200.7,991.738 1191.358,991.738 C1182.016,991.738 1174.443,984.239 1174.443,974.989 C1174.443,965.739 1182.016,958.241 1191.358,958.241 C1200.7,958.241 1208.274,965.739 1208.274,974.989 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 82 KiB |
@@ -6,6 +6,17 @@ Update version number
|
|||||||
|
|
||||||
Note to update version number in files below:
|
Note to update version number in files below:
|
||||||
|
|
||||||
|
### project/build.scala
|
||||||
|
|
||||||
|
```scala
|
||||||
|
object MyBuild extends Build {
|
||||||
|
val Organization = "gitbucket"
|
||||||
|
val Name = "gitbucket"
|
||||||
|
val Version = "3.3.0" // <---- update version!!
|
||||||
|
val ScalaVersion = "2.11.6"
|
||||||
|
val ScalatraVersion = "2.3.1"
|
||||||
|
```
|
||||||
|
|
||||||
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
|
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
|
||||||
|
|
||||||
```scala
|
```scala
|
||||||
@@ -19,13 +30,6 @@ object AutoUpdate {
|
|||||||
new Version(3, 2),
|
new Version(3, 2),
|
||||||
```
|
```
|
||||||
|
|
||||||
### env.sh
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/sh
|
|
||||||
export GITBUCKET_VERSION=3.3.0 # <---- update here!!
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate release files
|
Generate release files
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
|
||||||
<output url="file://$MODULE_DIR$/target/classes" />
|
|
||||||
<output-test url="file://$MODULE_DIR$/target/test-classes" />
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@@ -10,7 +10,7 @@ import sbtassembly.AssemblyKeys._
|
|||||||
object MyBuild extends Build {
|
object MyBuild extends Build {
|
||||||
val Organization = "gitbucket"
|
val Organization = "gitbucket"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val Version = System.getenv("GITBUCKET_VERSION")
|
val Version = "3.9.0"
|
||||||
val ScalaVersion = "2.11.6"
|
val ScalaVersion = "2.11.6"
|
||||||
val ScalatraVersion = "2.3.1"
|
val ScalatraVersion = "2.3.1"
|
||||||
|
|
||||||
@@ -38,7 +38,8 @@ object MyBuild extends Build {
|
|||||||
scalaVersion := ScalaVersion,
|
scalaVersion := ScalaVersion,
|
||||||
resolvers ++= Seq(
|
resolvers ++= Seq(
|
||||||
Classpaths.typesafeReleases,
|
Classpaths.typesafeReleases,
|
||||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
|
||||||
|
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
||||||
),
|
),
|
||||||
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
@@ -48,13 +49,14 @@ object MyBuild extends Build {
|
|||||||
"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.11",
|
"org.json4s" %% "json4s-jackson" % "3.2.11",
|
||||||
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
|
"jp.sf.amateras" %% "scalatra-forms" % "0.2.0",
|
||||||
"commons-io" % "commons-io" % "2.4",
|
"commons-io" % "commons-io" % "2.4",
|
||||||
"org.pegdown" % "pegdown" % "1.4.1", // 1.4.2 has incompatible APi changes
|
"io.github.gitbucket" % "markedj" % "1.0.5",
|
||||||
"org.apache.commons" % "commons-compress" % "1.9",
|
"org.apache.commons" % "commons-compress" % "1.9",
|
||||||
"org.apache.commons" % "commons-email" % "1.3.3",
|
"org.apache.commons" % "commons-email" % "1.3.3",
|
||||||
"org.apache.httpcomponents" % "httpclient" % "4.3.6",
|
"org.apache.httpcomponents" % "httpclient" % "4.3.6",
|
||||||
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||||
|
"org.apache.tika" % "tika-core" % "1.10",
|
||||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
"com.h2database" % "h2" % "1.4.180",
|
"com.h2database" % "h2" % "1.4.180",
|
||||||
@@ -64,9 +66,8 @@ object MyBuild extends Build {
|
|||||||
"junit" % "junit" % "4.12" % "test",
|
"junit" % "junit" % "4.12" % "test",
|
||||||
"com.mchange" % "c3p0" % "0.9.5",
|
"com.mchange" % "c3p0" % "0.9.5",
|
||||||
"com.typesafe" % "config" % "1.2.1",
|
"com.typesafe" % "config" % "1.2.1",
|
||||||
"com.typesafe.play" %% "twirl-compiler" % "1.0.4",
|
|
||||||
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
|
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
|
||||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x"
|
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x" exclude("c3p0","c3p0")
|
||||||
),
|
),
|
||||||
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
|
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
|
||||||
EclipseKeys.withSource := true,
|
EclipseKeys.withSource := true,
|
||||||
|
|||||||
@@ -55,7 +55,12 @@
|
|||||||
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||||
</target>
|
</target>
|
||||||
|
|
||||||
<target name="all" depends="rename">
|
<target name="checksum" depends="rename">
|
||||||
|
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="MD5" format="MD5SUM" forceOverwrite="yes" fileext=".md5"/>
|
||||||
|
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="SHA" format="MD5SUM" forceOverwrite="yes" fileext=".sha1"/>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="all" depends="checksum">
|
||||||
</target>
|
</target>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
source ../env.sh
|
. ./env.sh
|
||||||
|
|
||||||
cd ../
|
cd ../
|
||||||
./sbt.sh clean assembly
|
./sbt.sh clean assembly
|
||||||
|
|||||||
3
release/env.sh
Normal file
3
release/env.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
export GITBUCKET_VERSION=`cat ../project/build.scala | grep 'val Version' | cut -d \" -f 2`
|
||||||
|
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
source ../env.sh
|
D="$(dirname "$0")"
|
||||||
ant -f build.xml all
|
D="$(cd "${D}"; pwd)"
|
||||||
|
DD="$(dirname "${D}")"
|
||||||
|
(
|
||||||
|
for f in "${D}/env.sh" "${D}/build.xml"; do
|
||||||
|
if [ ! -s "${f}" ]; then
|
||||||
|
echo >&2 "$0: Unable to access file '${f}'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
. "${D}/env.sh"
|
||||||
|
cd "${DD}"
|
||||||
|
ant -f "${D}/build.xml" all
|
||||||
|
)
|
||||||
|
|||||||
17
release/pom.xml
Normal file
17
release/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>jp.sf.amateras</groupId>
|
||||||
|
<artifactId>gitbucket-assembly</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<build>
|
||||||
|
<extensions>
|
||||||
|
<extension>
|
||||||
|
<groupId>org.apache.maven.wagon</groupId>
|
||||||
|
<artifactId>wagon-ssh</artifactId>
|
||||||
|
<version>1.0-beta-6</version>
|
||||||
|
</extension>
|
||||||
|
</extensions>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
|||||||
set SCRIPT_DIR=%~dp0
|
set SCRIPT_DIR=%~dp0
|
||||||
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %*
|
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %*
|
||||||
|
|||||||
3
sbt.sh
3
sbt.sh
@@ -1,3 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
source ./env.sh
|
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.8.jar "$@"
|
||||||
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.8.jar "$@"
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ INSERT INTO ACCOUNT (
|
|||||||
'root@localhost',
|
'root@localhost',
|
||||||
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
||||||
true,
|
true,
|
||||||
'https://github.com/takezoe/gitbucket',
|
'https://github.com/gitbucket/gitbucket',
|
||||||
SYSDATE,
|
SYSDATE,
|
||||||
SYSDATE,
|
SYSDATE,
|
||||||
NULL
|
NULL
|
||||||
|
|||||||
55
src/main/resources/update/3_9.sql
Normal file
55
src/main/resources/update/3_9.sql
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
DROP TABLE IF EXISTS WEB_HOOK_EVENT;
|
||||||
|
|
||||||
|
CREATE TABLE WEB_HOOK_EVENT(
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
URL VARCHAR(200) NOT NULL,
|
||||||
|
EVENT VARCHAR(30) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL, EVENT);
|
||||||
|
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, URL) REFERENCES WEB_HOOK (USER_NAME, REPOSITORY_NAME, URL)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
CREATE TEMPORARY TABLE TMP_EVENTS (EVENT VARCHAR(30));
|
||||||
|
|
||||||
|
INSERT INTO TMP_EVENTS VALUES ('push'),('issue_comment'),('issues'),('pull_request');
|
||||||
|
|
||||||
|
INSERT INTO WEB_HOOK_EVENT (USER_NAME, REPOSITORY_NAME, URL, EVENT)
|
||||||
|
SELECT USER_NAME, REPOSITORY_NAME, URL, EVENT
|
||||||
|
FROM WEB_HOOK, TMP_EVENTS;
|
||||||
|
|
||||||
|
DROP TABLE TMP_EVENTS;
|
||||||
|
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD COLUMN ISSUE_ID INT;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||||
|
SELECT
|
||||||
|
A.USER_NAME,
|
||||||
|
A.REPOSITORY_NAME,
|
||||||
|
A.ISSUE_ID,
|
||||||
|
NVL(B.COMMENT_COUNT, 0) + NVL(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
||||||
|
FROM ISSUE A
|
||||||
|
LEFT OUTER JOIN (
|
||||||
|
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||||
|
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||||
|
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||||
|
) B
|
||||||
|
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
|
||||||
|
LEFT OUTER JOIN (
|
||||||
|
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
|
||||||
|
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||||
|
) C
|
||||||
|
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID);
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE COMMIT_COMMENT C SET (ISSUE_ID) = (
|
||||||
|
SELECT MAX(P.ISSUE_ID)
|
||||||
|
FROM PULL_REQUEST P
|
||||||
|
WHERE
|
||||||
|
C.USER_NAME = P.USER_NAME AND
|
||||||
|
C.REPOSITORY_NAME = P.REPOSITORY_NAME AND
|
||||||
|
C.COMMIT_ID = P.COMMIT_ID_TO
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE COMMIT_COMMENT DROP COLUMN PULL_REQUEST;
|
||||||
@@ -32,6 +32,7 @@ class ScalatraBootstrap extends LifeCycle {
|
|||||||
context.mount(new DashboardController, "/*")
|
context.mount(new DashboardController, "/*")
|
||||||
context.mount(new UserManagementController, "/*")
|
context.mount(new UserManagementController, "/*")
|
||||||
context.mount(new SystemSettingsController, "/*")
|
context.mount(new SystemSettingsController, "/*")
|
||||||
|
context.mount(new PluginsController, "/*")
|
||||||
context.mount(new AccountController, "/*")
|
context.mount(new AccountController, "/*")
|
||||||
context.mount(new RepositoryViewerController, "/*")
|
context.mount(new RepositoryViewerController, "/*")
|
||||||
context.mount(new WikiController, "/*")
|
context.mount(new WikiController, "/*")
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ case class ApiComment(
|
|||||||
user: ApiUser,
|
user: ApiUser,
|
||||||
body: String,
|
body: String,
|
||||||
created_at: Date,
|
created_at: Date,
|
||||||
updated_at: Date)(repositoryName: RepositoryName, issueId: Int){
|
updated_at: Date)(repositoryName: RepositoryName, issueId: Int, isPullRequest: Boolean){
|
||||||
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${issueId}#comment-${id}")
|
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${issueId}#comment-${id}")
|
||||||
}
|
}
|
||||||
|
|
||||||
object ApiComment{
|
object ApiComment{
|
||||||
def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser): ApiComment =
|
def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser, isPullRequest: Boolean): ApiComment =
|
||||||
ApiComment(
|
ApiComment(
|
||||||
id = comment.commentId,
|
id = comment.commentId,
|
||||||
user = user,
|
user = user,
|
||||||
body = comment.content,
|
body = comment.content,
|
||||||
created_at = comment.registeredDate,
|
created_at = comment.registeredDate,
|
||||||
updated_at = comment.updatedDate)(repositoryName, issueId)
|
updated_at = comment.updatedDate)(repositoryName, issueId, isPullRequest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,21 @@ case class ApiCommit(
|
|||||||
removed: List[String],
|
removed: List[String],
|
||||||
modified: List[String],
|
modified: List[String],
|
||||||
author: ApiPersonIdent,
|
author: ApiPersonIdent,
|
||||||
committer: ApiPersonIdent)(repositoryName:RepositoryName){
|
committer: ApiPersonIdent)(repositoryName:RepositoryName, urlIsHtmlUrl: Boolean) extends FieldSerializable{
|
||||||
val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
|
val url = if(urlIsHtmlUrl){
|
||||||
val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}")
|
ApiPath(s"/${repositoryName.fullName}/commit/${id}")
|
||||||
|
}else{
|
||||||
|
ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
|
||||||
|
}
|
||||||
|
val html_url = if(urlIsHtmlUrl){
|
||||||
|
None
|
||||||
|
}else{
|
||||||
|
Some(ApiPath(s"/${repositoryName.fullName}/commit/${id}"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ApiCommit{
|
object ApiCommit{
|
||||||
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = {
|
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo, urlIsHtmlUrl: Boolean = false): ApiCommit = {
|
||||||
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||||
ApiCommit(
|
ApiCommit(
|
||||||
id = commit.id,
|
id = commit.id,
|
||||||
@@ -43,6 +51,7 @@ object ApiCommit{
|
|||||||
},
|
},
|
||||||
author = ApiPersonIdent.author(commit),
|
author = ApiPersonIdent.author(commit),
|
||||||
committer = ApiPersonIdent.committer(commit)
|
committer = ApiPersonIdent.committer(commit)
|
||||||
)(repositoryName)
|
)(repositoryName, urlIsHtmlUrl)
|
||||||
}
|
}
|
||||||
|
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ case class ApiIssue(
|
|||||||
state: String,
|
state: String,
|
||||||
created_at: Date,
|
created_at: Date,
|
||||||
updated_at: Date,
|
updated_at: Date,
|
||||||
body: String)(repositoryName: RepositoryName){
|
body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
|
||||||
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
||||||
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${number}")
|
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
|
||||||
}
|
}
|
||||||
|
|
||||||
object ApiIssue{
|
object ApiIssue{
|
||||||
@@ -31,5 +31,5 @@ object ApiIssue{
|
|||||||
state = if(issue.closed){ "closed" }else{ "open" },
|
state = if(issue.closed){ "closed" }else{ "open" },
|
||||||
body = issue.content.getOrElse(""),
|
body = issue.content.getOrElse(""),
|
||||||
created_at = issue.registeredDate,
|
created_at = issue.registeredDate,
|
||||||
updated_at = issue.updatedDate)(repositoryName)
|
updated_at = issue.updatedDate)(repositoryName, issue.isPullRequest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.util.RepositoryName
|
||||||
|
import gitbucket.core.model.CommitComment
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
|
||||||
|
*/
|
||||||
|
case class ApiPullRequestReviewComment(
|
||||||
|
id: Int, // 29724692
|
||||||
|
// "diff_hunk": "@@ -1 +1 @@\n-# public-repo",
|
||||||
|
path: String, // "README.md",
|
||||||
|
// "position": 1,
|
||||||
|
// "original_position": 1,
|
||||||
|
commit_id: String, // "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||||
|
// "original_commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||||
|
user: ApiUser,
|
||||||
|
body: String, // "Maybe you should use more emojji on this line.",
|
||||||
|
created_at: Date, // "2015-05-05T23:40:27Z",
|
||||||
|
updated_at: Date // "2015-05-05T23:40:27Z",
|
||||||
|
)(repositoryName:RepositoryName, issueId: Int) extends FieldSerializable {
|
||||||
|
// "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments/29724692",
|
||||||
|
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/comments/${id}")
|
||||||
|
// "html_url": "https://github.com/baxterthehacker/public-repo/pull/1#discussion_r29724692",
|
||||||
|
val html_url = ApiPath(s"/${repositoryName.fullName}/pull/${issueId}#discussion_r${id}")
|
||||||
|
// "pull_request_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1",
|
||||||
|
val pull_request_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/${issueId}")
|
||||||
|
|
||||||
|
/*
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments/29724692"
|
||||||
|
},
|
||||||
|
"html": {
|
||||||
|
"href": "https://github.com/baxterthehacker/public-repo/pull/1#discussion_r29724692"
|
||||||
|
},
|
||||||
|
"pull_request": {
|
||||||
|
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
val _links = Map(
|
||||||
|
"self" -> Map("href" -> url),
|
||||||
|
"html" -> Map("href" -> html_url),
|
||||||
|
"pull_request" -> Map("href" -> pull_request_url))
|
||||||
|
}
|
||||||
|
|
||||||
|
object ApiPullRequestReviewComment{
|
||||||
|
def apply(comment: CommitComment, commentedUser: ApiUser, repositoryName: RepositoryName, issueId: Int): ApiPullRequestReviewComment =
|
||||||
|
new ApiPullRequestReviewComment(
|
||||||
|
id = comment.commentId,
|
||||||
|
path = comment.fileName.getOrElse(""),
|
||||||
|
commit_id = comment.commitId,
|
||||||
|
user = commentedUser,
|
||||||
|
body = comment.content,
|
||||||
|
created_at = comment.registeredDate,
|
||||||
|
updated_at = comment.updatedDate
|
||||||
|
)(repositoryName, issueId)
|
||||||
|
}
|
||||||
@@ -13,10 +13,14 @@ case class ApiRepository(
|
|||||||
forks: Int,
|
forks: Int,
|
||||||
`private`: Boolean,
|
`private`: Boolean,
|
||||||
default_branch: String,
|
default_branch: String,
|
||||||
owner: ApiUser) {
|
owner: ApiUser)(urlIsHtmlUrl: Boolean) {
|
||||||
val forks_count = forks
|
val forks_count = forks
|
||||||
val watchers_coun = watchers
|
val watchers_count = watchers
|
||||||
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
val url = if(urlIsHtmlUrl){
|
||||||
|
ApiPath(s"/${full_name}")
|
||||||
|
}else{
|
||||||
|
ApiPath(s"/api/v3/repos/${full_name}")
|
||||||
|
}
|
||||||
val http_url = ApiPath(s"/git/${full_name}.git")
|
val http_url = ApiPath(s"/git/${full_name}.git")
|
||||||
val clone_url = ApiPath(s"/git/${full_name}.git")
|
val clone_url = ApiPath(s"/git/${full_name}.git")
|
||||||
val html_url = ApiPath(s"/${full_name}")
|
val html_url = ApiPath(s"/${full_name}")
|
||||||
@@ -27,7 +31,8 @@ object ApiRepository{
|
|||||||
repository: Repository,
|
repository: Repository,
|
||||||
owner: ApiUser,
|
owner: ApiUser,
|
||||||
forkedCount: Int =0,
|
forkedCount: Int =0,
|
||||||
watchers: Int = 0): ApiRepository =
|
watchers: Int = 0,
|
||||||
|
urlIsHtmlUrl: Boolean = false): ApiRepository =
|
||||||
ApiRepository(
|
ApiRepository(
|
||||||
name = repository.repositoryName,
|
name = repository.repositoryName,
|
||||||
full_name = s"${repository.userName}/${repository.repositoryName}",
|
full_name = s"${repository.userName}/${repository.repositoryName}",
|
||||||
@@ -37,7 +42,7 @@ object ApiRepository{
|
|||||||
`private` = repository.isPrivate,
|
`private` = repository.isPrivate,
|
||||||
default_branch = repository.defaultBranch,
|
default_branch = repository.defaultBranch,
|
||||||
owner = owner
|
owner = owner
|
||||||
)
|
)(urlIsHtmlUrl)
|
||||||
|
|
||||||
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||||
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
|
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
|
||||||
@@ -45,4 +50,7 @@ object ApiRepository{
|
|||||||
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
|
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
|
||||||
this(repositoryInfo.repository, ApiUser(owner))
|
this(repositoryInfo.repository, ApiUser(owner))
|
||||||
|
|
||||||
|
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||||
|
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main/scala/gitbucket/core/api/CreateARepository.scala
Normal file
19
src/main/scala/gitbucket/core/api/CreateARepository.scala
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/#create
|
||||||
|
* api form
|
||||||
|
*/
|
||||||
|
case class CreateARepository(
|
||||||
|
name: String,
|
||||||
|
description: Option[String],
|
||||||
|
`private`: Boolean = false,
|
||||||
|
auto_init: Boolean = false
|
||||||
|
) {
|
||||||
|
def isValid: Boolean = {
|
||||||
|
name.length<=40 &&
|
||||||
|
name.matches("[a-zA-Z0-9\\-\\+_.]+") &&
|
||||||
|
!name.startsWith("_") &&
|
||||||
|
!name.startsWith("-")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
/** export fields for json */
|
||||||
|
trait FieldSerializable
|
||||||
@@ -22,7 +22,7 @@ object JsonFormat {
|
|||||||
)
|
)
|
||||||
) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() +
|
) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() +
|
||||||
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() +
|
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() +
|
||||||
FieldSerializer[ApiCommitStatus]() + FieldSerializer[ApiCommit]() + FieldSerializer[ApiCombinedCommitStatus]() +
|
FieldSerializer[ApiCommitStatus]() + FieldSerializer[FieldSerializable]() + FieldSerializer[ApiCombinedCommitStatus]() +
|
||||||
FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]()
|
FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
|
|
||||||
val newRepositoryForm = mapping(
|
val newRepositoryForm = mapping(
|
||||||
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
|
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
|
||||||
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
|
"name" -> trim(label("Repository name", text(required, maxlength(40), repository, uniqueRepository))),
|
||||||
"description" -> trim(label("Description" , optional(text()))),
|
"description" -> trim(label("Description" , optional(text()))),
|
||||||
"isPrivate" -> trim(label("Repository Type", boolean())),
|
"isPrivate" -> trim(label("Repository Type", boolean())),
|
||||||
"createReadme" -> trim(label("Create README" , boolean()))
|
"createReadme" -> trim(label("Create README" , boolean()))
|
||||||
@@ -212,6 +212,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||||
// removeUserRelatedData(userName)
|
// removeUserRelatedData(userName)
|
||||||
|
|
||||||
|
removeUserRelatedData(userName)
|
||||||
updateAccount(account.copy(isRemoved = true))
|
updateAccount(account.copy(isRemoved = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,56 +367,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
post("/new", newRepositoryForm)(usersOnly { form =>
|
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||||
LockUtil.lock(s"${form.owner}/${form.name}"){
|
LockUtil.lock(s"${form.owner}/${form.name}"){
|
||||||
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
|
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
|
||||||
val ownerAccount = getAccountByUserName(form.owner).get
|
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||||
val loginAccount = context.loginAccount.get
|
|
||||||
val loginUserName = loginAccount.userName
|
|
||||||
|
|
||||||
// Insert to the database at first
|
|
||||||
createRepository(form.name, form.owner, form.description, form.isPrivate)
|
|
||||||
|
|
||||||
// Add collaborators for group repository
|
|
||||||
if(ownerAccount.isGroupAccount){
|
|
||||||
getGroupMembers(form.owner).foreach { member =>
|
|
||||||
addCollaborator(form.owner, form.name, member.userName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert default labels
|
|
||||||
insertDefaultLabels(form.owner, form.name)
|
|
||||||
|
|
||||||
// Create the actual repository
|
|
||||||
val gitdir = getRepositoryDir(form.owner, form.name)
|
|
||||||
JGitUtil.initRepository(gitdir)
|
|
||||||
|
|
||||||
if(form.createReadme){
|
|
||||||
using(Git.open(gitdir)){ git =>
|
|
||||||
val builder = DirCache.newInCore.builder()
|
|
||||||
val inserter = git.getRepository.newObjectInserter()
|
|
||||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
|
||||||
val content = if(form.description.nonEmpty){
|
|
||||||
form.name + "\n" +
|
|
||||||
"===============\n" +
|
|
||||||
"\n" +
|
|
||||||
form.description.get
|
|
||||||
} else {
|
|
||||||
form.name + "\n" +
|
|
||||||
"===============\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
|
||||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
|
||||||
builder.finish()
|
|
||||||
|
|
||||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
|
||||||
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Wiki repository
|
|
||||||
createWikiRepository(loginAccount, form.owner, form.name)
|
|
||||||
|
|
||||||
// Record activity
|
|
||||||
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to the repository
|
// redirect to the repository
|
||||||
@@ -423,6 +375,54 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create user repository
|
||||||
|
* https://developer.github.com/v3/repos/#create
|
||||||
|
*/
|
||||||
|
post("/api/v3/user/repos")(usersOnly {
|
||||||
|
val owner = context.loginAccount.get.userName
|
||||||
|
(for {
|
||||||
|
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
||||||
|
} yield {
|
||||||
|
LockUtil.lock(s"${owner}/${data.name}") {
|
||||||
|
if(getRepository(owner, data.name, context.baseUrl).isEmpty){
|
||||||
|
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
|
||||||
|
val repository = getRepository(owner, data.name, context.baseUrl).get
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
|
||||||
|
} else {
|
||||||
|
ApiError(
|
||||||
|
"A repository with this name already exists on this account",
|
||||||
|
Some("https://developer.github.com/v3/repos/#create")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create group repository
|
||||||
|
* https://developer.github.com/v3/repos/#create
|
||||||
|
*/
|
||||||
|
post("/api/v3/orgs/:org/repos")(managersOnly {
|
||||||
|
val groupName = params("org")
|
||||||
|
(for {
|
||||||
|
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
||||||
|
} yield {
|
||||||
|
LockUtil.lock(s"${groupName}/${data.name}") {
|
||||||
|
if(getRepository(groupName, data.name, context.baseUrl).isEmpty){
|
||||||
|
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
|
||||||
|
val repository = getRepository(groupName, data.name, context.baseUrl).get
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
|
||||||
|
} else {
|
||||||
|
ApiError(
|
||||||
|
"A repository with this name already exists for this group",
|
||||||
|
Some("https://developer.github.com/v3/repos/#create")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||||
val loginAccount = context.loginAccount.get
|
val loginAccount = context.loginAccount.get
|
||||||
val loginUserName = loginAccount.userName
|
val loginUserName = loginAccount.userName
|
||||||
@@ -467,6 +467,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
parentUserName = Some(repository.owner)
|
parentUserName = Some(repository.owner)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Add collaborators for group repository
|
||||||
|
val ownerAccount = getAccountByUserName(accountName).get
|
||||||
|
if(ownerAccount.isGroupAccount){
|
||||||
|
getGroupMembers(accountName).foreach { member =>
|
||||||
|
addCollaborator(accountName, repository.name, member.userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Insert default labels
|
// Insert default labels
|
||||||
insertDefaultLabels(accountName, repository.name)
|
insertDefaultLabels(accountName, repository.name)
|
||||||
|
|
||||||
@@ -488,6 +496,59 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) {
|
||||||
|
val ownerAccount = getAccountByUserName(owner).get
|
||||||
|
val loginAccount = context.loginAccount.get
|
||||||
|
val loginUserName = loginAccount.userName
|
||||||
|
|
||||||
|
// Insert to the database at first
|
||||||
|
createRepository(name, owner, description, isPrivate)
|
||||||
|
|
||||||
|
// Add collaborators for group repository
|
||||||
|
if(ownerAccount.isGroupAccount){
|
||||||
|
getGroupMembers(owner).foreach { member =>
|
||||||
|
addCollaborator(owner, name, member.userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert default labels
|
||||||
|
insertDefaultLabels(owner, name)
|
||||||
|
|
||||||
|
// Create the actual repository
|
||||||
|
val gitdir = getRepositoryDir(owner, name)
|
||||||
|
JGitUtil.initRepository(gitdir)
|
||||||
|
|
||||||
|
if(createReadme){
|
||||||
|
using(Git.open(gitdir)){ git =>
|
||||||
|
val builder = DirCache.newInCore.builder()
|
||||||
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
|
val content = if(description.nonEmpty){
|
||||||
|
name + "\n" +
|
||||||
|
"===============\n" +
|
||||||
|
"\n" +
|
||||||
|
description.get
|
||||||
|
} else {
|
||||||
|
name + "\n" +
|
||||||
|
"===============\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||||
|
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||||
|
builder.finish()
|
||||||
|
|
||||||
|
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||||
|
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Wiki repository
|
||||||
|
createWikiRepository(loginAccount, owner, name)
|
||||||
|
|
||||||
|
// Record activity
|
||||||
|
recordCreateRepositoryActivity(owner, name, loginUserName)
|
||||||
|
}
|
||||||
|
|
||||||
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
|
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
|
||||||
createLabel(userName, repositoryName, "bug", "fc2929")
|
createLabel(userName, repositoryName, "bug", "fc2929")
|
||||||
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
||||||
|
|||||||
@@ -181,6 +181,13 @@ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount:
|
|||||||
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
|
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
|
||||||
val baseUrl = settings.baseUrl(request)
|
val baseUrl = settings.baseUrl(request)
|
||||||
val host = new java.net.URL(baseUrl).getHost
|
val host = new java.net.URL(baseUrl).getHost
|
||||||
|
val platform = request.getHeader("User-Agent") match {
|
||||||
|
case null => null
|
||||||
|
case agent if agent.contains("Mac") => "mac"
|
||||||
|
case agent if agent.contains("Linux") => "linux"
|
||||||
|
case agent if agent.contains("Win") => "windows"
|
||||||
|
case _ => null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get object from cache.
|
* Get object from cache.
|
||||||
|
|||||||
@@ -17,22 +17,22 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
|||||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||||
|
|
||||||
post("/image"){
|
post("/image"){
|
||||||
execute { (file, fileId) =>
|
execute({ (file, fileId) =>
|
||||||
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
|
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
|
||||||
session += Keys.Session.Upload(fileId) -> file.name
|
session += Keys.Session.Upload(fileId) -> file.name
|
||||||
}
|
}, FileUtil.isImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/image/:owner/:repository"){
|
post("/file/:owner/:repository"){
|
||||||
execute { (file, fileId) =>
|
execute({ (file, fileId) =>
|
||||||
FileUtils.writeByteArrayToFile(new java.io.File(
|
FileUtils.writeByteArrayToFile(new java.io.File(
|
||||||
getAttachedDir(params("owner"), params("repository")),
|
getAttachedDir(params("owner"), params("repository")),
|
||||||
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
|
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
|
||||||
}
|
}, FileUtil.isUploadableType)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
|
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
|
||||||
case Some(file) if(FileUtil.isImage(file.name)) =>
|
case Some(file) if(mimeTypeChcker(file.name)) =>
|
||||||
defining(FileUtil.generateFileId){ fileId =>
|
defining(FileUtil.generateFileId){ fileId =>
|
||||||
f(file, fileId)
|
f(file, fileId)
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON APU for checking user existence.
|
* JSON API for checking user existence.
|
||||||
*/
|
*/
|
||||||
post("/_user/existence")(usersOnly {
|
post("/_user/existence")(usersOnly {
|
||||||
getAccountByUserName(params("userName")).isDefined
|
getAccountByUserName(params("userName")).isDefined
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
issueId <- params("id").toIntOpt
|
issueId <- params("id").toIntOpt
|
||||||
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
||||||
} yield {
|
} yield {
|
||||||
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user)) })
|
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
|
||||||
}).getOrElse(NotFound)
|
}).getOrElse(NotFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
||||||
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
||||||
} yield {
|
} yield {
|
||||||
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get)))
|
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
|
||||||
}) getOrElse NotFound
|
}) getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
org.json4s.jackson.Serialization.write(
|
org.json4s.jackson.Serialization.write(
|
||||||
Map("title" -> x.title,
|
Map("title" -> x.title,
|
||||||
"content" -> Markdown.toHtml(x.content getOrElse "No description given.",
|
"content" -> Markdown.toHtml(x.content getOrElse "No description given.",
|
||||||
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
|
repository, false, true, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else Unauthorized
|
} else Unauthorized
|
||||||
@@ -257,6 +257,12 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/new/label")(collaboratorsOnly { repository =>
|
||||||
|
val labelNames = params("labelNames").split(",")
|
||||||
|
val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName))
|
||||||
|
html.labellist(labels)
|
||||||
|
})
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
||||||
defining(params("id").toInt){ issueId =>
|
defining(params("id").toInt){ issueId =>
|
||||||
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||||
@@ -326,6 +332,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
||||||
case dir if(dir.exists && dir.isDirectory) =>
|
case dir if(dir.exists && dir.isDirectory) =>
|
||||||
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
||||||
|
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
|
||||||
RawData(FileUtil.getMimeType(file.getName), file)
|
RawData(FileUtil.getMimeType(file.getName), file)
|
||||||
}
|
}
|
||||||
case _ => None
|
case _ => None
|
||||||
@@ -346,6 +353,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Same method exists in PullRequestController. Should it moved to IssueService?
|
||||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
||||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
val content = fromIssue.issueId + ":" + fromIssue.title
|
||||||
@@ -459,7 +467,11 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
"issues",
|
"issues",
|
||||||
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||||
page,
|
page,
|
||||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
|
||||||
|
(getCollaborators(owner, repoName) :+ owner).sorted
|
||||||
|
} else {
|
||||||
|
getCollaborators(owner, repoName)
|
||||||
|
},
|
||||||
getMilestones(owner, repoName),
|
getMilestones(owner, repoName),
|
||||||
getLabels(owner, repoName),
|
getLabels(owner, repoName),
|
||||||
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
|
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.admin.plugins.html
|
||||||
|
import gitbucket.core.plugin.PluginRegistry
|
||||||
|
import gitbucket.core.util.AdminAuthenticator
|
||||||
|
|
||||||
|
class PluginsController extends ControllerBase with AdminAuthenticator {
|
||||||
|
get("/admin/plugins")(adminOnly {
|
||||||
|
html.plugins(PluginRegistry().getPlugins())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -46,7 +46,10 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
"requestRepositoryName" -> trim(text(required, maxlength(100))),
|
"requestRepositoryName" -> trim(text(required, maxlength(100))),
|
||||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||||
"commitIdTo" -> trim(text(required, maxlength(40)))
|
"commitIdTo" -> trim(text(required, maxlength(40))),
|
||||||
|
"assignedUserName" -> trim(optional(text())),
|
||||||
|
"milestoneId" -> trim(optional(number())),
|
||||||
|
"labelNames" -> trim(optional(text()))
|
||||||
)(PullRequestForm.apply)
|
)(PullRequestForm.apply)
|
||||||
|
|
||||||
val mergeForm = mapping(
|
val mergeForm = mapping(
|
||||||
@@ -62,7 +65,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
requestRepositoryName: String,
|
requestRepositoryName: String,
|
||||||
requestBranch: String,
|
requestBranch: String,
|
||||||
commitIdFrom: String,
|
commitIdFrom: String,
|
||||||
commitIdTo: String)
|
commitIdTo: String,
|
||||||
|
assignedUserName: Option[String],
|
||||||
|
milestoneId: Option[Int],
|
||||||
|
labelNames: Option[String]
|
||||||
|
)
|
||||||
|
|
||||||
case class MergeForm(message: String)
|
case class MergeForm(message: String)
|
||||||
|
|
||||||
@@ -176,7 +183,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
pullreq,
|
pullreq,
|
||||||
statuses,
|
statuses,
|
||||||
repository,
|
repository,
|
||||||
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
|
||||||
}
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
@@ -232,6 +239,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePullRequests(owner, name, pullreq.branch)
|
||||||
|
|
||||||
// call web hook
|
// call web hook
|
||||||
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||||
|
|
||||||
@@ -284,6 +294,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
originRepositoryName <- if(originOwner == forkedOwner) {
|
originRepositoryName <- if(originOwner == forkedOwner) {
|
||||||
// Self repository
|
// Self repository
|
||||||
Some(forkedRepository.name)
|
Some(forkedRepository.name)
|
||||||
|
} else if(forkedRepository.repository.originUserName.isEmpty){
|
||||||
|
// when ForkedRepository is the original repository
|
||||||
|
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||||
} else if(Some(originOwner) == forkedRepository.repository.originUserName){
|
} else if(Some(originOwner) == forkedRepository.repository.originUserName){
|
||||||
// Original repository
|
// Original repository
|
||||||
forkedRepository.repository.originRepositoryName
|
forkedRepository.repository.originRepositoryName
|
||||||
@@ -307,12 +320,14 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
originRepository.owner, originRepository.name, originId,
|
originRepository.owner, originRepository.name, originId,
|
||||||
forkedRepository.owner, forkedRepository.name, forkedId)
|
forkedRepository.owner, forkedRepository.name, forkedId)
|
||||||
|
|
||||||
(oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId))
|
(Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||||
} else {
|
} else {
|
||||||
// Commit id
|
// Commit id
|
||||||
(oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId))
|
(Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(oldId, newId) match {
|
||||||
|
case (Some(oldId), Some(newId)) => {
|
||||||
val (commits, diffs) = getRequestCompareInfo(
|
val (commits, diffs) = getRequestCompareInfo(
|
||||||
originRepository.owner, originRepository.name, oldId.getName,
|
originRepository.owner, originRepository.name, oldId.getName,
|
||||||
forkedRepository.owner, forkedRepository.name, newId.getName)
|
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||||
@@ -332,7 +347,17 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
forkedRepository,
|
forkedRepository,
|
||||||
originRepository,
|
originRepository,
|
||||||
forkedRepository,
|
forkedRepository,
|
||||||
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
|
hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount),
|
||||||
|
(getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.isGroupAccount) Nil else List(originRepository.owner))).sorted,
|
||||||
|
getMilestones(originRepository.owner, originRepository.name),
|
||||||
|
getLabels(originRepository.owner, originRepository.name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case (oldId, newId) =>
|
||||||
|
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
|
||||||
|
s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." +
|
||||||
|
s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) getOrElse NotFound
|
}) getOrElse NotFound
|
||||||
})
|
})
|
||||||
@@ -368,6 +393,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
val writable = hasWritePermission(owner, name, context.loginAccount)
|
||||||
val loginUserName = context.loginAccount.get.userName
|
val loginUserName = context.loginAccount.get.userName
|
||||||
|
|
||||||
val issueId = createIssue(
|
val issueId = createIssue(
|
||||||
@@ -376,8 +403,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
loginUser = loginUserName,
|
loginUser = loginUserName,
|
||||||
title = form.title,
|
title = form.title,
|
||||||
content = form.content,
|
content = form.content,
|
||||||
assignedUserName = None,
|
assignedUserName = if(writable) form.assignedUserName else None,
|
||||||
milestoneId = None,
|
milestoneId = if(writable) form.milestoneId else None,
|
||||||
isPullRequest = true)
|
isPullRequest = true)
|
||||||
|
|
||||||
createPullRequest(
|
createPullRequest(
|
||||||
@@ -391,25 +418,54 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
commitIdFrom = form.commitIdFrom,
|
commitIdFrom = form.commitIdFrom,
|
||||||
commitIdTo = form.commitIdTo)
|
commitIdTo = form.commitIdTo)
|
||||||
|
|
||||||
|
// insert labels
|
||||||
|
if(writable){
|
||||||
|
form.labelNames.map { value =>
|
||||||
|
val labels = getLabels(owner, name)
|
||||||
|
value.split(",").foreach { labelName =>
|
||||||
|
labels.find(_.labelName == labelName).map { label =>
|
||||||
|
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// fetch requested branch
|
// fetch requested branch
|
||||||
fetchAsPullRequest(repository.owner, repository.name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
|
fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
|
||||||
|
|
||||||
// record activity
|
// record activity
|
||||||
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
|
recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
|
||||||
|
|
||||||
// call web hook
|
// call web hook
|
||||||
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
|
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||||
|
|
||||||
|
getIssue(owner, name, issueId.toString) foreach { issue =>
|
||||||
|
// extract references and create refer comment
|
||||||
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
getIssue(repository.owner, repository.name, issueId.toString) foreach { issue =>
|
|
||||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||||
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO Same method exists in IssueController. Should it moved to IssueService?
|
||||||
|
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
||||||
|
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||||
|
val content = fromIssue.issueId + ":" + fromIssue.title
|
||||||
|
if(getIssue(owner, repository, issueId).isDefined){
|
||||||
|
// Not add if refer comment already exist.
|
||||||
|
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
||||||
|
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||||
*
|
*
|
||||||
@@ -459,7 +515,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
"pulls",
|
"pulls",
|
||||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||||
page,
|
page,
|
||||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
|
||||||
|
(getCollaborators(owner, repoName) :+ owner).sorted
|
||||||
|
} else {
|
||||||
|
getCollaborators(owner, repoName)
|
||||||
|
},
|
||||||
getMilestones(owner, repoName),
|
getMilestones(owner, repoName),
|
||||||
getLabels(owner, repoName),
|
getLabels(owner, repoName),
|
||||||
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
|
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import org.apache.commons.io.FileUtils
|
|||||||
import org.scalatra.i18n.Messages
|
import org.scalatra.i18n.Messages
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.lib.Constants
|
import org.eclipse.jgit.lib.Constants
|
||||||
|
import scala.util.{Success, Failure}
|
||||||
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
|
|
||||||
|
|
||||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||||
@@ -42,10 +44,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
)(CollaboratorForm.apply)
|
)(CollaboratorForm.apply)
|
||||||
|
|
||||||
// for web hook url addition
|
// for web hook url addition
|
||||||
case class WebHookForm(url: String)
|
case class WebHookForm(url: String, events: Set[WebHook.Event])
|
||||||
|
|
||||||
val webHookForm = mapping(
|
def webHookForm(update:Boolean) = mapping(
|
||||||
"url" -> trim(label("url", text(required, webHook)))
|
"url" -> trim(label("url", text(required, webHook(update)))),
|
||||||
|
"events" -> webhookEvents
|
||||||
)(WebHookForm.apply)
|
)(WebHookForm.apply)
|
||||||
|
|
||||||
// for transfer ownership
|
// for transfer ownership
|
||||||
@@ -138,14 +141,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Display the web hook page.
|
* Display the web hook page.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
|
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
|
||||||
html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
|
html.hooks(getWebHooks(repository.owner, repository.name), repository, flash.get("info"))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the web hook edit page.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
||||||
|
val webhook = WebHook(repository.owner, repository.name, "")
|
||||||
|
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the web hook URL.
|
* Add the web hook URL.
|
||||||
*/
|
*/
|
||||||
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
|
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
|
||||||
addWebHookURL(repository.owner, repository.name, form.url)
|
addWebHook(repository.owner, repository.name, form.url, form.events)
|
||||||
|
flash += "info" -> s"Webhook ${form.url} created"
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -153,30 +165,77 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Delete the web hook URL.
|
* Delete the web hook URL.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
|
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
|
||||||
deleteWebHookURL(repository.owner, repository.name, params("url"))
|
deleteWebHook(repository.owner, repository.name, params("url"))
|
||||||
|
flash += "info" -> s"Webhook ${params("url")} deleted"
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the test request to registered web hook URLs.
|
* Send the test request to registered web hook URLs.
|
||||||
*/
|
*/
|
||||||
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
|
ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
|
||||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import scala.concurrent._
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
import org.apache.http.util.EntityUtils
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
|
||||||
|
val url = params("url")
|
||||||
|
val dummyPayload = {
|
||||||
|
val ownerAccount = getAccountByUserName(repository.owner).get
|
||||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||||
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
||||||
.setMaxCount(3)
|
.setMaxCount(4)
|
||||||
.call.iterator.asScala.map(new CommitInfo(_))
|
.call.iterator.asScala.map(new CommitInfo(_)).toList
|
||||||
|
val pushedCommit = commits.drop(1)
|
||||||
|
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, pushedCommit, ownerAccount,
|
||||||
|
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
|
||||||
|
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()))
|
||||||
|
}
|
||||||
|
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url)
|
||||||
|
|
||||||
getAccountByUserName(repository.owner).foreach { ownerAccount =>
|
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
|
||||||
callWebHook("push",
|
|
||||||
List(WebHook(repository.owner, repository.name, form.url)),
|
def headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map{ h => Array(h.getName, h.getValue) }
|
||||||
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
|
val toErrorMap:PartialFunction[Throwable, Map[String,String]] = {
|
||||||
)
|
case e:java.net.UnknownHostException => Map("error"-> ("Unknown host "+ e.getMessage))
|
||||||
|
case e:java.lang.IllegalArgumentException => Map("error"-> ("invalid url"))
|
||||||
|
case e:org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url"))
|
||||||
|
case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage))
|
||||||
}
|
}
|
||||||
flash += "url" -> form.url
|
contentType = formats("json")
|
||||||
flash += "info" -> "Test payload deployed!"
|
var result = Map(
|
||||||
|
"url" -> url,
|
||||||
|
"request" -> Await.result(reqFuture.map(req => Map(
|
||||||
|
"headers" -> headers(req.getAllHeaders),
|
||||||
|
"payload" -> json
|
||||||
|
)).recover(toErrorMap), 20 seconds),
|
||||||
|
"responce" -> Await.result(resFuture.map(res => Map(
|
||||||
|
"status" -> res.getStatusLine(),
|
||||||
|
"body" -> EntityUtils.toString(res.getEntity()),
|
||||||
|
"headers" -> headers(res.getAllHeaders())
|
||||||
|
)).recover(toErrorMap), 20 seconds))
|
||||||
|
org.json4s.jackson.Serialization.write(result)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the web hook edit page.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/hooks/edit/:url")(ownerOnly { repository =>
|
||||||
|
getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
|
||||||
|
html.edithooks(webhook, events, repository, flash.get("info"), false)
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update web hook settings.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/hooks/edit/:url", webHookForm(true))(ownerOnly { (form, repository) =>
|
||||||
|
updateWebHook(repository.owner, repository.name, form.url, form.events)
|
||||||
|
flash += "info" -> s"webhook ${form.url} updated"
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -226,9 +285,30 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
/**
|
/**
|
||||||
* Provides duplication check for web hook url.
|
* Provides duplication check for web hook url.
|
||||||
*/
|
*/
|
||||||
private def webHook: Constraint = new Constraint(){
|
private def webHook(needExists: Boolean): Constraint = new Constraint(){
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
|
if(getWebHook(params("owner"), params("repository"), value).isDefined != needExists){
|
||||||
|
Some(if(needExists){
|
||||||
|
"URL had not been registered yet."
|
||||||
|
} else {
|
||||||
|
"URL had been registered already."
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def webhookEvents = new ValueType[Set[WebHook.Event]]{
|
||||||
|
def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = {
|
||||||
|
WebHook.Event.values.flatMap { t =>
|
||||||
|
params.get(name + "." + t.name).map(_ => t)
|
||||||
|
}.toSet
|
||||||
|
}
|
||||||
|
def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
|
||||||
|
Seq(name -> messages("error.required").format(name))
|
||||||
|
} else {
|
||||||
|
Nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import gitbucket.core.util.StringUtil._
|
|||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import gitbucket.core.model.{Account, CommitState}
|
import gitbucket.core.model.{Account, CommitState, WebHook}
|
||||||
import gitbucket.core.service.CommitStatusService
|
import gitbucket.core.service.CommitStatusService
|
||||||
import gitbucket.core.service.WebHookService._
|
import gitbucket.core.service.WebHookService._
|
||||||
import gitbucket.core.view
|
import gitbucket.core.view
|
||||||
@@ -22,6 +22,7 @@ import org.apache.commons.io.FileUtils
|
|||||||
import org.eclipse.jgit.api.{ArchiveCommand, Git}
|
import org.eclipse.jgit.api.{ArchiveCommand, Git}
|
||||||
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
|
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
|
||||||
import org.eclipse.jgit.dircache.DirCache
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.eclipse.jgit.errors.MissingObjectException
|
||||||
import org.eclipse.jgit.lib._
|
import org.eclipse.jgit.lib._
|
||||||
import org.eclipse.jgit.revwalk.RevCommit
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
import org.eclipse.jgit.treewalk._
|
import org.eclipse.jgit.treewalk._
|
||||||
@@ -31,7 +32,7 @@ import org.scalatra._
|
|||||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||||
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||||
with WebHookPullRequestService
|
with WebHookPullRequestService with WebHookPullRequestReviewCommentService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The repository viewer.
|
* The repository viewer.
|
||||||
@@ -39,7 +40,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
|
|||||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||||
with WebHookPullRequestService =>
|
with WebHookPullRequestService with WebHookPullRequestReviewCommentService =>
|
||||||
|
|
||||||
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||||
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||||
@@ -249,7 +250,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
)
|
)
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||||
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
|
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
|
||||||
}")
|
}")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -270,7 +271,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
)
|
)
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||||
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
|
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
|
||||||
}")
|
}")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -292,8 +293,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||||
if(raw){
|
if(raw){
|
||||||
// Download
|
// Download
|
||||||
JGitUtil.getContentFromId(git, objectId, true).map { bytes =>
|
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||||
RawData("application/octet-stream", bytes)
|
//RawData("application/octet-stream", bytes)
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
response.setContentLength(loader.getSize.toInt)
|
||||||
|
loader.copyTo(response.getOutputStream)
|
||||||
|
()
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
} else {
|
} else {
|
||||||
html.blob(id, repository, path.split("/").toList,
|
html.blob(id, repository, path.split("/").toList,
|
||||||
@@ -344,9 +349,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
||||||
val id = params("id")
|
val id = params("id")
|
||||||
|
|
||||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
try {
|
||||||
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
|
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit =>
|
||||||
|
JGitUtil.getDiffs(git, id) match {
|
||||||
|
case (diffs, oldCommitId) =>
|
||||||
html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
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),
|
||||||
@@ -355,12 +362,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
case e:MissingObjectException => NotFound
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
val id = params("id")
|
val id = params("id")
|
||||||
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
|
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
|
||||||
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
|
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId)
|
||||||
form.issueId match {
|
form.issueId match {
|
||||||
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
||||||
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
||||||
@@ -385,13 +395,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
|
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
val id = params("id")
|
val id = params("id")
|
||||||
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
|
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
|
||||||
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
|
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId)
|
||||||
|
val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get
|
||||||
form.issueId match {
|
form.issueId match {
|
||||||
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
case Some(issueId) =>
|
||||||
|
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
||||||
|
callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||||
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
||||||
}
|
}
|
||||||
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
|
helper.html.commitcomment(comment, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
|
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
|
||||||
@@ -436,10 +448,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
* Displays branches.
|
* Displays branches.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
||||||
val branches = JGitUtil.getBranches(repository.owner, repository.name, repository.repository.defaultBranch)
|
val branches = JGitUtil.getBranches(
|
||||||
|
owner = repository.owner,
|
||||||
|
name = repository.name,
|
||||||
|
defaultBranch = repository.repository.defaultBranch,
|
||||||
|
origin = repository.repository.originUserName.isEmpty
|
||||||
|
)
|
||||||
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
|
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
|
||||||
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
|
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
|
||||||
.reverse
|
.reverse
|
||||||
|
|
||||||
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -511,10 +529,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
/**
|
/**
|
||||||
* Displays the file find of branch.
|
* Displays the file find of branch.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/find/:ref")(referrersOnly { repository =>
|
get("/:owner/:repository/find/*")(referrersOnly { repository =>
|
||||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
JGitUtil.getTreeId(git, params("ref")).map{ treeId =>
|
val ref = multiParams("splat").head
|
||||||
html.find(params("ref"),
|
JGitUtil.getTreeId(git, ref).map{ treeId =>
|
||||||
|
html.find(ref,
|
||||||
treeId,
|
treeId,
|
||||||
repository,
|
repository,
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
@@ -650,9 +669,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
// call web hook
|
// call web hook
|
||||||
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
|
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
|
||||||
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
||||||
callWebHookOf(repository.owner, repository.name, "push") {
|
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
|
||||||
getAccountByUserName(repository.owner).map{ ownerAccount =>
|
getAccountByUserName(repository.owner).map{ ownerAccount =>
|
||||||
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)
|
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
|
||||||
|
oldId = headTip, newId = commitId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
||||||
"ssh" -> trim(label("SSH access", boolean())),
|
"ssh" -> trim(label("SSH access", boolean())),
|
||||||
"sshPort" -> trim(label("SSH port", optional(number()))),
|
"sshPort" -> trim(label("SSH port", optional(number()))),
|
||||||
"smtp" -> optionalIfNotChecked("notification", mapping(
|
"useSMTP" -> trim(label("SMTP", boolean())),
|
||||||
|
"smtp" -> optionalIfNotChecked("useSMTP", mapping(
|
||||||
"host" -> trim(label("SMTP Host", text(required))),
|
"host" -> trim(label("SMTP Host", text(required))),
|
||||||
"port" -> trim(label("SMTP Port", optional(number()))),
|
"port" -> trim(label("SMTP Port", optional(number()))),
|
||||||
"user" -> trim(label("SMTP User", optional(text()))),
|
"user" -> trim(label("SMTP User", optional(text()))),
|
||||||
|
|||||||
@@ -100,12 +100,12 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
|||||||
|
|
||||||
if(form.isRemoved){
|
if(form.isRemoved){
|
||||||
// Remove repositories
|
// Remove repositories
|
||||||
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||||
deleteRepository(userName, repositoryName)
|
// deleteRepository(userName, repositoryName)
|
||||||
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||||
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||||
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||||
}
|
// }
|
||||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||||
removeUserRelatedData(userName)
|
removeUserRelatedData(userName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ trait CommitCommentComponent extends TemplateComponent { self: Profile =>
|
|||||||
val newLine = column[Option[Int]]("NEW_LINE_NUMBER")
|
val newLine = column[Option[Int]]("NEW_LINE_NUMBER")
|
||||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
val pullRequest = column[Boolean]("PULL_REQUEST")
|
val issueId = column[Option[Int]]("ISSUE_ID")
|
||||||
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply)
|
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, issueId) <> (CommitComment.tupled, CommitComment.unapply)
|
||||||
|
|
||||||
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
||||||
}
|
}
|
||||||
@@ -74,5 +74,5 @@ case class CommitComment(
|
|||||||
newLine: Option[Int],
|
newLine: Option[Int],
|
||||||
registeredDate: java.util.Date,
|
registeredDate: java.util.Date,
|
||||||
updatedDate: java.util.Date,
|
updatedDate: java.util.Date,
|
||||||
pullRequest: Boolean
|
issueId: Option[Int]
|
||||||
) extends Comment
|
) extends Comment
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ trait CoreProfile extends ProfileProvider with Profile
|
|||||||
with RepositoryComponent
|
with RepositoryComponent
|
||||||
with SshKeyComponent
|
with SshKeyComponent
|
||||||
with WebHookComponent
|
with WebHookComponent
|
||||||
|
with WebHookEventComponent
|
||||||
with PluginComponent
|
with PluginComponent
|
||||||
|
|
||||||
object Profile extends CoreProfile
|
object Profile extends CoreProfile
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
|
|||||||
|
|
||||||
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||||
val url = column[String]("URL")
|
val url = column[String]("URL")
|
||||||
def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply)
|
def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply)
|
||||||
|
|
||||||
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
||||||
}
|
}
|
||||||
@@ -18,3 +18,32 @@ case class WebHook(
|
|||||||
repositoryName: String,
|
repositoryName: String,
|
||||||
url: String
|
url: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object WebHook {
|
||||||
|
sealed class Event(var name: String)
|
||||||
|
case object CommitComment extends Event("commit_comment")
|
||||||
|
case object Create extends Event("create")
|
||||||
|
case object Delete extends Event("delete")
|
||||||
|
case object Deployment extends Event("deployment")
|
||||||
|
case object DeploymentStatus extends Event("deployment_status")
|
||||||
|
case object Fork extends Event("fork")
|
||||||
|
case object Gollum extends Event("gollum")
|
||||||
|
case object IssueComment extends Event("issue_comment")
|
||||||
|
case object Issues extends Event("issues")
|
||||||
|
case object Member extends Event("member")
|
||||||
|
case object PageBuild extends Event("page_build")
|
||||||
|
case object Public extends Event("public")
|
||||||
|
case object PullRequest extends Event("pull_request")
|
||||||
|
case object PullRequestReviewComment extends Event("pull_request_review_comment")
|
||||||
|
case object Push extends Event("push")
|
||||||
|
case object Release extends Event("release")
|
||||||
|
case object Status extends Event("status")
|
||||||
|
case object TeamAdd extends Event("team_add")
|
||||||
|
case object Watch extends Event("watch")
|
||||||
|
object Event{
|
||||||
|
val values = List(CommitComment,Create,Delete,Deployment,DeploymentStatus,Fork,Gollum,IssueComment,Issues,Member,PageBuild,Public,PullRequest,PullRequestReviewComment,Push,Release,Status,TeamAdd,Watch)
|
||||||
|
private val map:Map[String,Event] = values.map(e => e.name -> e).toMap
|
||||||
|
def valueOf(name: String): Event = map(name)
|
||||||
|
def valueOpt(name: String): Option[Event] = map.get(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
30
src/main/scala/gitbucket/core/model/WebHookEvent.scala
Normal file
30
src/main/scala/gitbucket/core/model/WebHookEvent.scala
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package gitbucket.core.model
|
||||||
|
|
||||||
|
trait WebHookEventComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import gitbucket.core.model.Profile.WebHooks
|
||||||
|
|
||||||
|
lazy val WebHookEvents = TableQuery[WebHookEvents]
|
||||||
|
|
||||||
|
implicit val typedType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_))
|
||||||
|
|
||||||
|
class WebHookEvents(tag: Tag) extends Table[WebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate {
|
||||||
|
val url = column[String]("URL")
|
||||||
|
val event = column[WebHook.Event]("EVENT")
|
||||||
|
def * = (userName, repositoryName, url, event) <> ((WebHookEvent.apply _).tupled, WebHookEvent.unapply)
|
||||||
|
|
||||||
|
def byWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
||||||
|
def byWebHook(owner: Column[String], repository: Column[String], url: Column[String]) =
|
||||||
|
byRepository(userName, repositoryName) && (this.url === url)
|
||||||
|
def byWebHook(webhook: WebHooks) =
|
||||||
|
byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url)
|
||||||
|
def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byWebHook(owner, repository, url) && (this.event === event.bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class WebHookEvent(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
url: String,
|
||||||
|
event: WebHook.Event
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package gitbucket.core.plugin
|
||||||
|
|
||||||
|
import gitbucket.core.model.Session
|
||||||
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the Git repository routing.
|
||||||
|
*
|
||||||
|
* @param urlPattern the regular expression which matches the repository path (e.g. "gist/(.+?)/(.+?)\\.git")
|
||||||
|
* @param localPath the string to assemble local file path of repository (e.g. "gist/$1/$2")
|
||||||
|
* @param filter the filter for request to the Git repository which is defined by this routing
|
||||||
|
*/
|
||||||
|
case class GitRepositoryRouting(urlPattern: String, localPath: String, filter: GitRepositoryFilter){
|
||||||
|
|
||||||
|
def this(urlPattern: String, localPath: String) = {
|
||||||
|
this(urlPattern, localPath, new GitRepositoryFilter(){
|
||||||
|
def filter(repositoryName: String, userName: Option[String], settings: SystemSettings, isUpdating: Boolean)
|
||||||
|
(implicit session: Session): Boolean = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters request to plug-in served repository. This is used to provide authentication mainly.
|
||||||
|
*/
|
||||||
|
trait GitRepositoryFilter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters request to Git repository. If this method returns true then request is accepted.
|
||||||
|
*
|
||||||
|
* @param path the repository path which starts with '/'
|
||||||
|
* @param userName the authenticated user name or None
|
||||||
|
* @param settings the system settings
|
||||||
|
* @param isUpdating true if update request, otherwise false
|
||||||
|
* @param session the database session
|
||||||
|
* @return true if allow accessing to repository, otherwise false.
|
||||||
|
*/
|
||||||
|
def filter(path: String, userName: Option[String], settings: SystemSettings, isUpdating: Boolean)
|
||||||
|
(implicit session: Session): Boolean
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,38 +22,71 @@ trait Plugin {
|
|||||||
*/
|
*/
|
||||||
val images: Seq[(String, Array[Byte])] = Nil
|
val images: Seq[(String, Array[Byte])] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to declare this plug-in provides images.
|
||||||
|
*/
|
||||||
|
def images(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, Array[Byte])] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override to declare this plug-in provides controllers.
|
* Override to declare this plug-in provides controllers.
|
||||||
*/
|
*/
|
||||||
val controllers: Seq[(String, ControllerBase)] = Nil
|
val controllers: Seq[(String, ControllerBase)] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to declare this plug-in provides controllers.
|
||||||
|
*/
|
||||||
|
def controllers(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, ControllerBase)] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override to declare this plug-in provides JavaScript.
|
* Override to declare this plug-in provides JavaScript.
|
||||||
*/
|
*/
|
||||||
val javaScripts: Seq[(String, String)] = Nil
|
val javaScripts: Seq[(String, String)] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to declare this plug-in provides JavaScript.
|
||||||
|
*/
|
||||||
|
def javaScripts(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, String)] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override to declare this plug-in provides renderers.
|
* Override to declare this plug-in provides renderers.
|
||||||
*/
|
*/
|
||||||
val renderers: Seq[(String, Renderer)] = Nil
|
val renderers: Seq[(String, Renderer)] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to declare this plug-in provides renderers.
|
||||||
|
*/
|
||||||
|
def renderers(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, Renderer)] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add git repository routings.
|
||||||
|
*/
|
||||||
|
val repositoryRoutings: Seq[GitRepositoryRouting] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add git repository routings.
|
||||||
|
*/
|
||||||
|
def repositoryRoutings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[GitRepositoryRouting] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is invoked in initialization of plugin system.
|
* This method is invoked in initialization of plugin system.
|
||||||
* Register plugin functionality to PluginRegistry.
|
* Register plugin functionality to PluginRegistry.
|
||||||
*/
|
*/
|
||||||
def initialize(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {
|
def initialize(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {
|
||||||
images.foreach { case (id, in) =>
|
(images ++ images(registry, context, settings)).foreach { case (id, in) =>
|
||||||
registry.addImage(id, in)
|
registry.addImage(id, in)
|
||||||
}
|
}
|
||||||
controllers.foreach { case (path, controller) =>
|
(controllers ++ controllers(registry, context, settings)).foreach { case (path, controller) =>
|
||||||
registry.addController(path, controller)
|
registry.addController(path, controller)
|
||||||
}
|
}
|
||||||
javaScripts.foreach { case (path, script) =>
|
(javaScripts ++ javaScripts(registry, context, settings)).foreach { case (path, script) =>
|
||||||
registry.addJavaScript(path, script)
|
registry.addJavaScript(path, script)
|
||||||
}
|
}
|
||||||
renderers.foreach { case (extension, renderer) =>
|
(renderers ++ renderers(registry, context, settings)).foreach { case (extension, renderer) =>
|
||||||
registry.addRenderer(extension, renderer)
|
registry.addRenderer(extension, renderer)
|
||||||
}
|
}
|
||||||
|
(repositoryRoutings ++ repositoryRoutings(registry, context, settings)).foreach { routing =>
|
||||||
|
registry.addRepositoryRouting(routing)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class PluginRegistry {
|
|||||||
renderers ++= Seq(
|
renderers ++= Seq(
|
||||||
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
|
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
|
||||||
)
|
)
|
||||||
|
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
|
||||||
|
|
||||||
def addPlugin(pluginInfo: PluginInfo): Unit = {
|
def addPlugin(pluginInfo: PluginInfo): Unit = {
|
||||||
plugins += pluginInfo
|
plugins += pluginInfo
|
||||||
@@ -61,7 +62,7 @@ class PluginRegistry {
|
|||||||
addController(path, controller)
|
addController(path, controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
def getControllers(): List[(ControllerBase, String)] = controllers.toList
|
def getControllers(): Seq[(ControllerBase, String)] = controllers.toSeq
|
||||||
|
|
||||||
def addJavaScript(path: String, script: String): Unit = {
|
def addJavaScript(path: String, script: String): Unit = {
|
||||||
javaScripts += ((path, script))
|
javaScripts += ((path, script))
|
||||||
@@ -81,6 +82,22 @@ class PluginRegistry {
|
|||||||
|
|
||||||
def renderableExtensions: Seq[String] = renderers.keys.toSeq
|
def renderableExtensions: Seq[String] = renderers.keys.toSeq
|
||||||
|
|
||||||
|
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = {
|
||||||
|
repositoryRoutings += routing
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = {
|
||||||
|
repositoryRoutings.toSeq
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
|
||||||
|
PluginRegistry().getRepositoryRoutings().find {
|
||||||
|
case GitRepositoryRouting(urlPath, _, _) => {
|
||||||
|
repositoryPath.matches("/" + urlPath + "(/.*)?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private case class GlobalAction(
|
private case class GlobalAction(
|
||||||
method: String,
|
method: String,
|
||||||
path: String,
|
path: String,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ trait Renderer {
|
|||||||
object MarkdownRenderer extends Renderer {
|
object MarkdownRenderer extends Renderer {
|
||||||
override def render(request: RenderRequest): Html = {
|
override def render(request: RenderRequest): Html = {
|
||||||
import request._
|
import request._
|
||||||
Html(Markdown.toHtml(fileContent, repository, enableWikiLink, enableRefsLink)(context))
|
Html(Markdown.toHtml(fileContent, repository, enableWikiLink, enableRefsLink, enableAnchor)(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,4 +41,5 @@ case class RenderRequest(filePath: List[String],
|
|||||||
repository: RepositoryService.RepositoryInfo,
|
repository: RepositoryService.RepositoryInfo,
|
||||||
enableWikiLink: Boolean,
|
enableWikiLink: Boolean,
|
||||||
enableRefsLink: Boolean,
|
enableRefsLink: Boolean,
|
||||||
|
enableAnchor: Boolean,
|
||||||
context: Context)
|
context: Context)
|
||||||
@@ -13,9 +13,9 @@ import StringUtil._
|
|||||||
|
|
||||||
trait CommitsService {
|
trait CommitsService {
|
||||||
|
|
||||||
def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) =
|
def getCommitComments(owner: String, repository: String, commitId: String, includePullRequest: Boolean)(implicit s: Session) =
|
||||||
CommitComments filter {
|
CommitComments filter {
|
||||||
t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest)
|
t => t.byCommit(owner, repository, commitId) && (t.issueId.isEmpty || includePullRequest)
|
||||||
} list
|
} list
|
||||||
|
|
||||||
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||||
@@ -27,7 +27,8 @@ trait CommitsService {
|
|||||||
None
|
None
|
||||||
|
|
||||||
def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String,
|
def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String,
|
||||||
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], pullRequest: Boolean)(implicit s: Session): Int =
|
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int],
|
||||||
|
issueId: Option[Int])(implicit s: Session): Int =
|
||||||
CommitComments.autoInc insert CommitComment(
|
CommitComments.autoInc insert CommitComment(
|
||||||
userName = owner,
|
userName = owner,
|
||||||
repositoryName = repository,
|
repositoryName = repository,
|
||||||
@@ -39,7 +40,7 @@ trait CommitsService {
|
|||||||
newLine = newLine,
|
newLine = newLine,
|
||||||
registeredDate = currentDate,
|
registeredDate = currentDate,
|
||||||
updatedDate = currentDate,
|
updatedDate = currentDate,
|
||||||
pullRequest = pullRequest)
|
issueId = issueId)
|
||||||
|
|
||||||
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
|
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
|
||||||
CommitComments
|
CommitComments
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ trait IssuesService {
|
|||||||
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||||
IssueComments filter (_.byIssue(owner, repository, issueId)) list
|
IssueComments filter (_.byIssue(owner, repository, issueId)) list
|
||||||
|
|
||||||
/** @return IssueComment and commentedUser */
|
/** @return IssueComment and commentedUser and Issue */
|
||||||
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account)] =
|
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account, Issue)] =
|
||||||
IssueComments.filter(_.byIssue(owner, repository, issueId))
|
IssueComments.filter(_.byIssue(owner, repository, issueId))
|
||||||
.filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment"))
|
.filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment"))
|
||||||
.innerJoin(Accounts).on( (t1, t2) => t1.commentedUserName === t2.userName )
|
.innerJoin(Accounts).on( (t1, t2) => t1.commentedUserName === t2.userName )
|
||||||
|
.innerJoin(Issues).on{ case ((t1, t2), t3) => t3.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
|
||||||
|
.map{ case ((t1, t2), t3) => (t1, t2, t3) }
|
||||||
.list
|
.list
|
||||||
|
|
||||||
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||||
@@ -90,7 +92,7 @@ trait IssuesService {
|
|||||||
def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={
|
def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={
|
||||||
if(issueList.isEmpty){
|
if(issueList.isEmpty){
|
||||||
Map.empty
|
Map.empty
|
||||||
}else{
|
} else {
|
||||||
import scala.slick.jdbc._
|
import scala.slick.jdbc._
|
||||||
val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ")
|
val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ")
|
||||||
implicit val qset = SetParameter[Seq[(String, String, Int)]] {
|
implicit val qset = SetParameter[Seq[(String, String, Int)]] {
|
||||||
@@ -474,9 +476,11 @@ object IssuesService {
|
|||||||
* Restores IssueSearchCondition instance from filter query.
|
* Restores IssueSearchCondition instance from filter query.
|
||||||
*/
|
*/
|
||||||
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
|
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
|
||||||
val conditions = filter.split("[ \t]+").map { x =>
|
val conditions = filter.split("[ \t]+").flatMap { x =>
|
||||||
val dim = x.split(":")
|
x.split(":") match {
|
||||||
dim(0) -> dim(1)
|
case Array(key, value) => Some((key, value))
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
}.groupBy(_._1).map { case (key, values) =>
|
}.groupBy(_._1).map { case (key, values) =>
|
||||||
key -> values.map(_._2).toSeq
|
key -> values.map(_._2).toSeq
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
|
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
|
||||||
|
|
||||||
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
@@ -66,10 +67,6 @@ trait RepositoryService { self: AccountService =>
|
|||||||
(t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
|
(t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
|
||||||
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
|
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
|
||||||
|
|
||||||
PullRequests.filter { t =>
|
|
||||||
t.requestRepositoryName === oldRepositoryName.bind
|
|
||||||
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
|
||||||
|
|
||||||
// Updates activity fk before deleting repository because activity is sorted by activityId
|
// Updates activity fk before deleting repository because activity is sorted by activityId
|
||||||
// and it can't be changed by deleting-and-inserting record.
|
// and it can't be changed by deleting-and-inserting record.
|
||||||
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
|
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
|
||||||
@@ -80,7 +77,8 @@ trait RepositoryService { self: AccountService =>
|
|||||||
deleteRepository(oldUserName, oldRepositoryName)
|
deleteRepository(oldUserName, oldRepositoryName)
|
||||||
|
|
||||||
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
WebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
||||||
|
|
||||||
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
|
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
|
||||||
@@ -98,6 +96,11 @@ trait RepositoryService { self: AccountService =>
|
|||||||
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
|
||||||
|
// Update source repository of pull requests
|
||||||
|
PullRequests.filter { t =>
|
||||||
|
(t.requestUserName === oldUserName.bind) && (t.requestRepositoryName === oldRepositoryName.bind)
|
||||||
|
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
||||||
|
|
||||||
// Convert labelId
|
// Convert labelId
|
||||||
val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap
|
val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap
|
||||||
val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap
|
val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap
|
||||||
@@ -145,6 +148,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
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
|
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
|
||||||
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
|
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
|
||||||
@@ -383,6 +387,12 @@ object RepositoryService {
|
|||||||
|
|
||||||
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
|
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
|
||||||
|
|
||||||
|
def sshOpenRepoUrl(platform: String, port: Int, userName: String) = openRepoUrl(platform, sshUrl(port, userName))
|
||||||
|
|
||||||
|
def httpOpenRepoUrl(platform: String) = openRepoUrl(platform, httpUrl)
|
||||||
|
|
||||||
|
def openRepoUrl(platform: String, openUrl: String) = s"github-${platform}://openRepo/${openUrl}"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates instance with issue count and pull request count.
|
* Creates instance with issue count and pull request count.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ trait SystemSettingsService {
|
|||||||
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
|
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
|
||||||
props.setProperty(Ssh, settings.ssh.toString)
|
props.setProperty(Ssh, settings.ssh.toString)
|
||||||
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
||||||
if(settings.notification) {
|
props.setProperty(UseSMTP, settings.useSMTP.toString)
|
||||||
|
if(settings.useSMTP) {
|
||||||
settings.smtp.foreach { smtp =>
|
settings.smtp.foreach { smtp =>
|
||||||
props.setProperty(SmtpHost, smtp.host)
|
props.setProperty(SmtpHost, smtp.host)
|
||||||
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
||||||
@@ -75,7 +76,8 @@ trait SystemSettingsService {
|
|||||||
getOptionValue[Int](props, ActivityLogLimit, None),
|
getOptionValue[Int](props, ActivityLogLimit, None),
|
||||||
getValue(props, Ssh, false),
|
getValue(props, Ssh, false),
|
||||||
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
||||||
if(getValue(props, Notification, false)){
|
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP
|
||||||
|
if(getValue(props, UseSMTP, getValue(props, Notification, false))){
|
||||||
Some(Smtp(
|
Some(Smtp(
|
||||||
getValue(props, SmtpHost, ""),
|
getValue(props, SmtpHost, ""),
|
||||||
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
||||||
@@ -125,6 +127,7 @@ object SystemSettingsService {
|
|||||||
activityLogLimit: Option[Int],
|
activityLogLimit: Option[Int],
|
||||||
ssh: Boolean,
|
ssh: Boolean,
|
||||||
sshPort: Option[Int],
|
sshPort: Option[Int],
|
||||||
|
useSMTP: Boolean,
|
||||||
smtp: Option[Smtp],
|
smtp: Option[Smtp],
|
||||||
ldapAuthentication: Boolean,
|
ldapAuthentication: Boolean,
|
||||||
ldap: Option[Ldap]){
|
ldap: Option[Ldap]){
|
||||||
@@ -172,6 +175,7 @@ object SystemSettingsService {
|
|||||||
private val ActivityLogLimit = "activity_log_limit"
|
private val ActivityLogLimit = "activity_log_limit"
|
||||||
private val Ssh = "ssh"
|
private val Ssh = "ssh"
|
||||||
private val SshPort = "ssh.port"
|
private val SshPort = "ssh.port"
|
||||||
|
private val UseSMTP = "useSMTP"
|
||||||
private val SmtpHost = "smtp.host"
|
private val SmtpHost = "smtp.host"
|
||||||
private val SmtpPort = "smtp.port"
|
private val SmtpPort = "smtp.port"
|
||||||
private val SmtpUser = "smtp.user"
|
private val SmtpUser = "smtp.user"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
import gitbucket.core.api._
|
import gitbucket.core.api._
|
||||||
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment}
|
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
|
||||||
import gitbucket.core.model.Profile._
|
import gitbucket.core.model.Profile._
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
@@ -12,7 +12,11 @@ import org.apache.http.NameValuePair
|
|||||||
import org.apache.http.client.entity.UrlEncodedFormEntity
|
import org.apache.http.client.entity.UrlEncodedFormEntity
|
||||||
import org.apache.http.message.BasicNameValuePair
|
import org.apache.http.message.BasicNameValuePair
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import scala.concurrent._
|
||||||
|
import org.apache.http.HttpRequest
|
||||||
|
import org.apache.http.HttpResponse
|
||||||
|
|
||||||
|
|
||||||
trait WebHookService {
|
trait WebHookService {
|
||||||
@@ -20,46 +24,91 @@ trait WebHookService {
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
|
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
|
||||||
|
|
||||||
def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] =
|
/** get All WebHook informations of repository */
|
||||||
WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list
|
def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(WebHook, Set[WebHook.Event])] =
|
||||||
|
WebHooks.filter(_.byRepository(owner, repository))
|
||||||
|
.innerJoin(WebHookEvents).on { (w, t) => t.byWebHook(w) }
|
||||||
|
.map{ case (w,t) => w -> t.event }
|
||||||
|
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
|
||||||
|
|
||||||
def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
|
/** get All WebHook informations of repository event */
|
||||||
|
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
|
||||||
|
WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind)
|
||||||
|
.list.map(t => WebHook(t.userName, t.repositoryName, t.url))
|
||||||
|
|
||||||
|
/** get All WebHook information from repository to url */
|
||||||
|
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
|
||||||
|
WebHooks
|
||||||
|
.filter(_.byPrimaryKey(owner, repository, url))
|
||||||
|
.innerJoin(WebHookEvents).on { (w, t) => t.byWebHook(w) }
|
||||||
|
.map{ case (w,t) => w -> t.event }
|
||||||
|
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
|
||||||
|
|
||||||
|
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
||||||
WebHooks insert WebHook(owner, repository, url)
|
WebHooks insert WebHook(owner, repository, url)
|
||||||
|
events.toSet.map{ event: WebHook.Event =>
|
||||||
|
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
|
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
||||||
|
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
|
||||||
|
events.toSet.map{ event: WebHook.Event =>
|
||||||
|
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteWebHook(owner: String, repository: String, url :String)(implicit s: Session): Unit =
|
||||||
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
|
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
|
||||||
|
|
||||||
def callWebHookOf(owner: String, repository: String, eventName: String)(makePayload: => Option[WebHookPayload])(implicit s: Session, c: JsonFormat.Context): Unit = {
|
def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload])(implicit s: Session, c: JsonFormat.Context): Unit = {
|
||||||
val webHookURLs = getWebHookURLs(owner, repository)
|
val webHooks = getWebHooksByEvent(owner, repository, event)
|
||||||
if(webHookURLs.nonEmpty){
|
if(webHooks.nonEmpty){
|
||||||
makePayload.map(callWebHook(eventName, webHookURLs, _))
|
makePayload.map(callWebHook(event, webHooks, _))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def callWebHook(eventName: String, webHookURLs: List[WebHook], payload: WebHookPayload)(implicit c: JsonFormat.Context): Unit = {
|
def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload)(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
|
||||||
import org.apache.http.client.methods.HttpPost
|
|
||||||
import org.apache.http.impl.client.HttpClientBuilder
|
import org.apache.http.impl.client.HttpClientBuilder
|
||||||
import scala.concurrent._
|
|
||||||
import ExecutionContext.Implicits.global
|
import ExecutionContext.Implicits.global
|
||||||
|
import org.apache.http.protocol.HttpContext
|
||||||
|
import org.apache.http.client.methods.HttpPost
|
||||||
|
|
||||||
if(webHookURLs.nonEmpty){
|
if(webHookURLs.nonEmpty){
|
||||||
val json = JsonFormat(payload)
|
val json = JsonFormat(payload)
|
||||||
val httpClient = HttpClientBuilder.create.build
|
|
||||||
|
|
||||||
webHookURLs.foreach { webHookUrl =>
|
webHookURLs.map { webHookUrl =>
|
||||||
|
val reqPromise = Promise[HttpRequest]
|
||||||
val f = Future {
|
val f = Future {
|
||||||
logger.debug(s"start web hook invocation for ${webHookUrl}")
|
val itcp = new org.apache.http.HttpRequestInterceptor{
|
||||||
|
def process(res: HttpRequest, ctx: HttpContext): Unit = {
|
||||||
|
reqPromise.success(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try{
|
||||||
|
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
|
||||||
|
logger.debug(s"start web hook invocation for ${webHookUrl.url}")
|
||||||
val httpPost = new HttpPost(webHookUrl.url)
|
val httpPost = new HttpPost(webHookUrl.url)
|
||||||
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||||
httpPost.addHeader("X-Github-Event", eventName)
|
httpPost.addHeader("X-Github-Event", event.name)
|
||||||
|
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
|
||||||
|
|
||||||
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||||
params.add(new BasicNameValuePair("payload", json))
|
params.add(new BasicNameValuePair("payload", json))
|
||||||
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
||||||
|
|
||||||
httpClient.execute(httpPost)
|
val res = httpClient.execute(httpPost)
|
||||||
httpPost.releaseConnection()
|
httpPost.releaseConnection()
|
||||||
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
||||||
|
res
|
||||||
|
}catch{
|
||||||
|
case e:Throwable => {
|
||||||
|
if(!reqPromise.isCompleted){
|
||||||
|
reqPromise.failure(e)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
f.onSuccess {
|
f.onSuccess {
|
||||||
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
|
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
|
||||||
@@ -67,9 +116,12 @@ trait WebHookService {
|
|||||||
f.onFailure {
|
f.onFailure {
|
||||||
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
|
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
|
||||||
}
|
}
|
||||||
|
(webHookUrl, json, reqPromise.future, f)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Nil
|
||||||
}
|
}
|
||||||
logger.debug("end callWebHook")
|
// logger.debug("end callWebHook")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +132,7 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
import WebHookService._
|
import WebHookService._
|
||||||
// https://developer.github.com/v3/activity/events/types/#issuesevent
|
// https://developer.github.com/v3/activity/events/types/#issuesevent
|
||||||
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
||||||
callWebHookOf(repository.owner, repository.name, "issues"){
|
callWebHookOf(repository.owner, repository.name, WebHook.Issues){
|
||||||
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
|
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
|
||||||
for{
|
for{
|
||||||
repoOwner <- users.get(repository.owner)
|
repoOwner <- users.get(repository.owner)
|
||||||
@@ -98,7 +150,7 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
|
|
||||||
def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
||||||
import WebHookService._
|
import WebHookService._
|
||||||
callWebHookOf(repository.owner, repository.name, "pull_request"){
|
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){
|
||||||
for{
|
for{
|
||||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||||
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
|
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
|
||||||
@@ -134,6 +186,7 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
ru <- Accounts if ru.userName === pr.requestUserName
|
ru <- Accounts if ru.userName === pr.requestUserName
|
||||||
iu <- Accounts if iu.userName === is.openedUserName
|
iu <- Accounts if iu.userName === is.openedUserName
|
||||||
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
|
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
|
||||||
|
wht <- WebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byWebHook(wh)
|
||||||
} yield {
|
} yield {
|
||||||
((is, iu, pr, bu, ru), wh)
|
((is, iu, pr, bu, ru), wh)
|
||||||
}).list.groupBy(_._1).mapValues(_.map(_._2))
|
}).list.groupBy(_._1).mapValues(_.map(_._2))
|
||||||
@@ -154,7 +207,36 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
baseRepository = baseRepo,
|
baseRepository = baseRepo,
|
||||||
baseOwner = baseOwner,
|
baseOwner = baseOwner,
|
||||||
sender = sender)
|
sender = sender)
|
||||||
callWebHook("pull_request", webHooks, payload)
|
callWebHook(WebHook.PullRequest, webHooks, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait WebHookPullRequestReviewCommentService extends WebHookService {
|
||||||
|
self: AccountService with RepositoryService with PullRequestService with IssuesService with CommitsService =>
|
||||||
|
def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
||||||
|
import WebHookService._
|
||||||
|
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){
|
||||||
|
for{
|
||||||
|
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||||
|
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
|
||||||
|
baseOwner <- users.get(repository.owner)
|
||||||
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
|
issueUser <- users.get(issue.openedUserName)
|
||||||
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
||||||
|
} yield {
|
||||||
|
WebHookPullRequestReviewCommentPayload(
|
||||||
|
action = action,
|
||||||
|
comment = comment,
|
||||||
|
issue = issue,
|
||||||
|
issueUser = issueUser,
|
||||||
|
pullRequest = pullRequest,
|
||||||
|
headRepository = headRepo,
|
||||||
|
headOwner = headOwner,
|
||||||
|
baseRepository = repository,
|
||||||
|
baseOwner = baseOwner,
|
||||||
|
sender = sender)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,7 +246,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
|
|||||||
|
|
||||||
import WebHookService._
|
import WebHookService._
|
||||||
def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
||||||
callWebHookOf(repository.owner, repository.name, "issue_comment"){
|
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment){
|
||||||
for{
|
for{
|
||||||
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
|
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
|
||||||
users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender))
|
users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender))
|
||||||
@@ -192,21 +274,33 @@ object WebHookService {
|
|||||||
case class WebHookPushPayload(
|
case class WebHookPushPayload(
|
||||||
pusher: ApiUser,
|
pusher: ApiUser,
|
||||||
ref: String,
|
ref: String,
|
||||||
|
before: String,
|
||||||
|
after: String,
|
||||||
commits: List[ApiCommit],
|
commits: List[ApiCommit],
|
||||||
repository: ApiRepository
|
repository: ApiRepository
|
||||||
) extends WebHookPayload
|
) extends FieldSerializable with WebHookPayload {
|
||||||
|
val compare = commits.size match {
|
||||||
|
case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initalied repository
|
||||||
|
case 1 => ApiPath(s"/${repository.full_name}/commit/${after}")
|
||||||
|
case _ if before.filterNot(_=='0').isEmpty => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
|
||||||
|
case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}")
|
||||||
|
}
|
||||||
|
val head_commit = commits.lastOption
|
||||||
|
}
|
||||||
|
|
||||||
object WebHookPushPayload {
|
object WebHookPushPayload {
|
||||||
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
|
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
|
||||||
commits: List[CommitInfo], repositoryOwner: Account): WebHookPushPayload =
|
commits: List[CommitInfo], repositoryOwner: Account,
|
||||||
|
newId: ObjectId, oldId: ObjectId): WebHookPushPayload =
|
||||||
WebHookPushPayload(
|
WebHookPushPayload(
|
||||||
ApiUser(pusher),
|
pusher = ApiUser(pusher),
|
||||||
refName,
|
ref = refName,
|
||||||
commits.map{ commit => ApiCommit(git, RepositoryName(repositoryInfo), commit) },
|
before = ObjectId.toString(oldId),
|
||||||
ApiRepository(
|
after = ObjectId.toString(newId),
|
||||||
|
commits = commits.map{ commit => ApiCommit.forPushPayload(git, RepositoryName(repositoryInfo), commit) },
|
||||||
|
repository = ApiRepository.forPushPayload(
|
||||||
repositoryInfo,
|
repositoryInfo,
|
||||||
owner= ApiUser(repositoryOwner)
|
owner= ApiUser(repositoryOwner))
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +367,41 @@ object WebHookService {
|
|||||||
action = "created",
|
action = "created",
|
||||||
repository = ApiRepository(repository, repositoryUser),
|
repository = ApiRepository(repository, repositoryUser),
|
||||||
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
|
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
|
||||||
comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser)),
|
comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser), issue.isPullRequest),
|
||||||
sender = ApiUser(sender))
|
sender = ApiUser(sender))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
|
||||||
|
case class WebHookPullRequestReviewCommentPayload(
|
||||||
|
action: String,
|
||||||
|
comment: ApiPullRequestReviewComment,
|
||||||
|
pull_request: ApiPullRequest,
|
||||||
|
repository: ApiRepository,
|
||||||
|
sender: ApiUser
|
||||||
|
) extends WebHookPayload
|
||||||
|
|
||||||
|
object WebHookPullRequestReviewCommentPayload{
|
||||||
|
def apply(
|
||||||
|
action: String,
|
||||||
|
comment: CommitComment,
|
||||||
|
issue: Issue,
|
||||||
|
issueUser: Account,
|
||||||
|
pullRequest: PullRequest,
|
||||||
|
headRepository: RepositoryInfo,
|
||||||
|
headOwner: Account,
|
||||||
|
baseRepository: RepositoryInfo,
|
||||||
|
baseOwner: Account,
|
||||||
|
sender: Account
|
||||||
|
) : WebHookPullRequestReviewCommentPayload = {
|
||||||
|
val headRepoPayload = ApiRepository(headRepository, headOwner)
|
||||||
|
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
|
||||||
|
val senderPayload = ApiUser(sender)
|
||||||
|
WebHookPullRequestReviewCommentPayload(
|
||||||
|
action = action,
|
||||||
|
comment = ApiPullRequestReviewComment(comment, senderPayload, RepositoryName(baseRepository), issue.issueId),
|
||||||
|
pull_request = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, ApiUser(issueUser)),
|
||||||
|
repository = baseRepoPayload,
|
||||||
|
sender = senderPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class AccessTokenAuthenticationFilter extends Filter with AccessTokenService {
|
|||||||
case None => chain.doFilter(req, res)
|
case None => chain.doFilter(req, res)
|
||||||
case Some(Left(_)) => {
|
case Some(Left(_)) => {
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
|
||||||
response.setContentType("Content-Type: application/json; charset=utf-8")
|
response.setContentType("application/json; charset=utf-8")
|
||||||
val w = response.getWriter()
|
val w = response.getWriter()
|
||||||
w.print("""{ "message": "Bad credentials" }""")
|
w.print("""{ "message": "Bad credentials" }""")
|
||||||
w.close()
|
w.close()
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ object AutoUpdate {
|
|||||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||||
*/
|
*/
|
||||||
val versions = Seq(
|
val versions = Seq(
|
||||||
|
new Version(3, 9),
|
||||||
|
new Version(3, 8),
|
||||||
|
new Version(3, 7) with SystemSettingsService {
|
||||||
|
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||||
|
super.update(conn, cl)
|
||||||
|
val settings = loadSystemSettings()
|
||||||
|
if(settings.notification){
|
||||||
|
saveSystemSettings(settings.copy(useSMTP = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Version(3, 6),
|
||||||
|
new Version(3, 5),
|
||||||
new Version(3, 4),
|
new Version(3, 4),
|
||||||
new Version(3, 3),
|
new Version(3, 3),
|
||||||
new Version(3, 2),
|
new Version(3, 2),
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package gitbucket.core.servlet
|
|||||||
|
|
||||||
import javax.servlet._
|
import javax.servlet._
|
||||||
import javax.servlet.http._
|
import javax.servlet.http._
|
||||||
|
import gitbucket.core.plugin.{GitRepositoryFilter, GitRepositoryRouting, PluginRegistry}
|
||||||
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
|
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
|
||||||
import gitbucket.core.util.{ControlUtil, Keys, Implicits}
|
import gitbucket.core.util.{Keys, Implicits}
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import Implicits._
|
import Implicits._
|
||||||
import ControlUtil._
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides BASIC Authentication for [[GitRepositoryServlet]].
|
* Provides BASIC Authentication for [[GitRepositoryServlet]].
|
||||||
@@ -20,7 +21,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
def destroy(): Unit = {}
|
def destroy(): Unit = {}
|
||||||
|
|
||||||
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
||||||
implicit val request = req.asInstanceOf[HttpServletRequest]
|
val request = req.asInstanceOf[HttpServletRequest]
|
||||||
val response = res.asInstanceOf[HttpServletResponse]
|
val response = res.asInstanceOf[HttpServletResponse]
|
||||||
|
|
||||||
val wrappedResponse = new HttpServletResponseWrapper(response){
|
val wrappedResponse = new HttpServletResponseWrapper(response){
|
||||||
@@ -31,36 +32,69 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
val settings = loadSystemSettings()
|
val settings = loadSystemSettings()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
defining(request.paths){
|
PluginRegistry().getRepositoryRouting(request.gitRepositoryPath).map { case GitRepositoryRouting(_, _, filter) =>
|
||||||
|
// served by plug-ins
|
||||||
|
pluginRepository(request, wrappedResponse, chain, settings, isUpdating, filter)
|
||||||
|
|
||||||
|
}.getOrElse {
|
||||||
|
// default repositories
|
||||||
|
defaultRepository(request, wrappedResponse, chain, settings, isUpdating)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case ex: Exception => {
|
||||||
|
logger.error("error", ex)
|
||||||
|
requireAuth(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def pluginRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
|
||||||
|
settings: SystemSettings, isUpdating: Boolean, filter: GitRepositoryFilter): Unit = {
|
||||||
|
implicit val r = request
|
||||||
|
|
||||||
|
val account = for {
|
||||||
|
auth <- Option(request.getHeader("Authorization"))
|
||||||
|
Array(username, password) = decodeAuthHeader(auth).split(":", 2)
|
||||||
|
account <- authenticate(settings, username, password)
|
||||||
|
} yield {
|
||||||
|
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||||
|
account
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)){
|
||||||
|
chain.doFilter(request, response)
|
||||||
|
} else {
|
||||||
|
requireAuth(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def defaultRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
|
||||||
|
settings: SystemSettings, isUpdating: Boolean): Unit = {
|
||||||
|
implicit val r = request
|
||||||
|
|
||||||
|
request.paths match {
|
||||||
case Array(_, repositoryOwner, repositoryName, _*) =>
|
case Array(_, repositoryOwner, repositoryName, _*) =>
|
||||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
|
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
|
||||||
case Some(repository) => {
|
case Some(repository) => {
|
||||||
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
|
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
|
||||||
chain.doFilter(req, wrappedResponse)
|
chain.doFilter(request, response)
|
||||||
} else {
|
} else {
|
||||||
request.getHeader("Authorization") match {
|
val passed = for {
|
||||||
case null => requireAuth(response)
|
auth <- Option(request.getHeader("Authorization"))
|
||||||
case auth => decodeAuthHeader(auth).split(":", 2) match {
|
Array(username, password) = decodeAuthHeader(auth).split(":", 2)
|
||||||
case Array(username, password) => {
|
account <- authenticate(settings, username, password)
|
||||||
authenticate(settings, username, password) match {
|
} yield if(isUpdating || repository.repository.isPrivate){
|
||||||
case Some(account) => {
|
|
||||||
if (isUpdating || repository.repository.isPrivate) {
|
|
||||||
if(hasWritePermission(repository.owner, repository.name, Some(account))){
|
if(hasWritePermission(repository.owner, repository.name, Some(account))){
|
||||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||||
chain.doFilter(req, wrappedResponse)
|
true
|
||||||
|
} else false
|
||||||
|
} else true
|
||||||
|
|
||||||
|
if(passed.getOrElse(false)){
|
||||||
|
chain.doFilter(request, response)
|
||||||
} else {
|
} else {
|
||||||
requireAuth(response)
|
requireAuth(response)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
chain.doFilter(req, wrappedResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case _ => requireAuth(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case _ => requireAuth(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case None => {
|
case None => {
|
||||||
@@ -73,12 +107,6 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
case ex: Exception => {
|
|
||||||
logger.error("error", ex)
|
|
||||||
requireAuth(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def requireAuth(response: HttpServletResponse): Unit = {
|
private def requireAuth(response: HttpServletResponse): Unit = {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package gitbucket.core.servlet
|
package gitbucket.core.servlet
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
import gitbucket.core.api
|
import gitbucket.core.api
|
||||||
import gitbucket.core.model.Session
|
import gitbucket.core.model.{Session, WebHook}
|
||||||
|
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
|
||||||
import gitbucket.core.service.IssuesService.IssueSearchCondition
|
import gitbucket.core.service.IssuesService.IssueSearchCondition
|
||||||
import gitbucket.core.service.WebHookService._
|
import gitbucket.core.service.WebHookService._
|
||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
@@ -18,7 +21,6 @@ import org.eclipse.jgit.transport.resolver._
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
import javax.servlet.ServletConfig
|
import javax.servlet.ServletConfig
|
||||||
import javax.servlet.ServletContext
|
|
||||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||||
|
|
||||||
|
|
||||||
@@ -35,20 +37,8 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
|||||||
override def init(config: ServletConfig): Unit = {
|
override def init(config: ServletConfig): Unit = {
|
||||||
setReceivePackFactory(new GitBucketReceivePackFactory())
|
setReceivePackFactory(new GitBucketReceivePackFactory())
|
||||||
|
|
||||||
// TODO are there any other ways...?
|
val root: File = new File(Directory.RepositoryHome)
|
||||||
super.init(new ServletConfig(){
|
setRepositoryResolver(new GitBucketRepositoryResolver(new FileResolver[HttpServletRequest](root, true)))
|
||||||
def getInitParameter(name: String): String = name match {
|
|
||||||
case "base-path" => Directory.RepositoryHome
|
|
||||||
case "export-all" => "true"
|
|
||||||
case name => config.getInitParameter(name)
|
|
||||||
}
|
|
||||||
def getInitParameterNames(): java.util.Enumeration[String] = {
|
|
||||||
config.getInitParameterNames
|
|
||||||
}
|
|
||||||
|
|
||||||
def getServletContext(): ServletContext = config.getServletContext
|
|
||||||
def getServletName(): String = config.getServletName
|
|
||||||
})
|
|
||||||
|
|
||||||
super.init(config)
|
super.init(config)
|
||||||
}
|
}
|
||||||
@@ -67,12 +57,30 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GitBucketRepositoryResolver(parent: FileResolver[HttpServletRequest]) extends RepositoryResolver[HttpServletRequest] {
|
||||||
|
|
||||||
|
private val resolver = new FileResolver[HttpServletRequest](new File(Directory.GitBucketHome), true)
|
||||||
|
|
||||||
|
override def open(req: HttpServletRequest, name: String): Repository = {
|
||||||
|
// Rewrite repository path if routing is marched
|
||||||
|
PluginRegistry().getRepositoryRouting("/" + name).map { case GitRepositoryRouting(urlPattern, localPath, _) =>
|
||||||
|
val path = urlPattern.r.replaceFirstIn(name, localPath)
|
||||||
|
resolver.open(req, path)
|
||||||
|
}.getOrElse {
|
||||||
|
parent.open(req, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
|
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
|
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
|
||||||
|
|
||||||
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
||||||
val receivePack = new ReceivePack(db)
|
val receivePack = new ReceivePack(db)
|
||||||
|
|
||||||
|
if(PluginRegistry().getRepositoryRouting(request.gitRepositoryPath).isEmpty){
|
||||||
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
||||||
|
|
||||||
logger.debug("requestURI: " + request.getRequestURI)
|
logger.debug("requestURI: " + request.getRequestURI)
|
||||||
@@ -91,9 +99,11 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
|||||||
receivePack.setPostReceiveHook(hook)
|
receivePack.setPostReceiveHook(hook)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
receivePack
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
receivePack
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
@@ -190,10 +200,11 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// call web hook
|
// call web hook
|
||||||
callWebHookOf(owner, repository, "push"){
|
callWebHookOf(owner, repository, WebHook.Push){
|
||||||
for(pusherAccount <- getAccountByUserName(pusher);
|
for(pusherAccount <- getAccountByUserName(pusher);
|
||||||
ownerAccount <- getAccountByUserName(owner)) yield {
|
ownerAccount <- getAccountByUserName(owner)) yield {
|
||||||
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)
|
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
|
||||||
|
newId = command.getNewId(), oldId = command.getOldId())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package gitbucket.core.ssh
|
package gitbucket.core.ssh
|
||||||
|
|
||||||
import gitbucket.core.model.Session
|
import gitbucket.core.model.Session
|
||||||
|
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
|
||||||
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
|
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
|
||||||
import gitbucket.core.servlet.{Database, CommitLogHook}
|
import gitbucket.core.servlet.{Database, CommitLogHook}
|
||||||
import gitbucket.core.util.{Directory, ControlUtil}
|
import gitbucket.core.util.{Directory, ControlUtil}
|
||||||
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
|
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.{InputStream, OutputStream}
|
import java.io.{File, InputStream, OutputStream}
|
||||||
import ControlUtil._
|
import ControlUtil._
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import Directory._
|
import Directory._
|
||||||
@@ -15,11 +16,11 @@ import org.apache.sshd.server.command.UnknownCommand
|
|||||||
import org.eclipse.jgit.errors.RepositoryNotFoundException
|
import org.eclipse.jgit.errors.RepositoryNotFoundException
|
||||||
|
|
||||||
object GitCommand {
|
object GitCommand {
|
||||||
val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
|
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
|
||||||
|
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GitCommand(val owner: String, val repoName: String) extends Command {
|
abstract class GitCommand() extends Command {
|
||||||
self: RepositoryService with AccountService =>
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
|
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
|
||||||
protected var err: OutputStream = null
|
protected var err: OutputStream = null
|
||||||
@@ -71,6 +72,11 @@ abstract class GitCommand(val owner: String, val repoName: String) extends Comma
|
|||||||
this.in = in
|
this.in = in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class DefaultGitCommand(val owner: String, val repoName: String) extends GitCommand {
|
||||||
|
self: RepositoryService with AccountService =>
|
||||||
|
|
||||||
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
|
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
|
||||||
(implicit session: Session): Boolean =
|
(implicit session: Session): Boolean =
|
||||||
getAccountByUserName(username) match {
|
getAccountByUserName(username) match {
|
||||||
@@ -80,7 +86,8 @@ abstract class GitCommand(val owner: String, val repoName: String) extends Comma
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GitUploadPack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName)
|
|
||||||
|
class DefaultGitUploadPack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
|
||||||
with RepositoryService with AccountService {
|
with RepositoryService with AccountService {
|
||||||
|
|
||||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||||
@@ -94,11 +101,10 @@ class GitUploadPack(owner: String, repoName: String, baseUrl: String) extends Gi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GitReceivePack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName)
|
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
|
||||||
with SystemSettingsService with RepositoryService with AccountService {
|
with RepositoryService with AccountService {
|
||||||
|
|
||||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||||
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
|
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
|
||||||
@@ -116,18 +122,56 @@ class GitReceivePack(owner: String, repoName: String, baseUrl: String) extends G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand
|
||||||
|
with SystemSettingsService {
|
||||||
|
|
||||||
|
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||||
|
if(routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), false)){
|
||||||
|
val path = routing.urlPattern.r.replaceFirstIn(repoName, routing.localPath)
|
||||||
|
using(Git.open(new File(Directory.GitBucketHome, path))){ git =>
|
||||||
|
val repository = git.getRepository
|
||||||
|
val upload = new UploadPack(repository)
|
||||||
|
upload.upload(in, out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginGitReceivePack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand
|
||||||
|
with SystemSettingsService {
|
||||||
|
|
||||||
|
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||||
|
if(routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), true)){
|
||||||
|
val path = routing.urlPattern.r.replaceFirstIn(repoName, routing.localPath)
|
||||||
|
using(Git.open(new File(Directory.GitBucketHome, path))){ git =>
|
||||||
|
val repository = git.getRepository
|
||||||
|
val receive = new ReceivePack(repository)
|
||||||
|
receive.receive(in, out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GitCommandFactory(baseUrl: String) extends CommandFactory {
|
class GitCommandFactory(baseUrl: String) extends CommandFactory {
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
|
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
|
||||||
|
|
||||||
override def createCommand(command: String): Command = {
|
override def createCommand(command: String): Command = {
|
||||||
|
import GitCommand._
|
||||||
logger.debug(s"command: $command")
|
logger.debug(s"command: $command")
|
||||||
|
|
||||||
command match {
|
command match {
|
||||||
case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(owner, repoName, baseUrl)
|
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, baseUrl, routing(repoName))
|
||||||
case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(owner, repoName, baseUrl)
|
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, baseUrl, routing(repoName))
|
||||||
|
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName, baseUrl)
|
||||||
|
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
|
||||||
case _ => new UnknownCommand(command)
|
case _ => new UnknownCommand(command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def pluginRepository(repoName: String): Boolean = PluginRegistry().getRepositoryRouting("/" + repoName).isDefined
|
||||||
|
private def routing(repoName: String): GitRepositoryRouting = PluginRegistry().getRepositoryRouting("/" + repoName).get
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ object SshServer {
|
|||||||
|
|
||||||
private def configure(port: Int, baseUrl: String) = {
|
private def configure(port: Int, baseUrl: String) = {
|
||||||
server.setPort(port)
|
server.setPort(port)
|
||||||
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser"))
|
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser", "RSA"))
|
||||||
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
|
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
|
||||||
server.setCommandFactory(new GitCommandFactory(baseUrl))
|
server.setCommandFactory(new GitCommandFactory(baseUrl))
|
||||||
server.setShellFactory(new NoShell)
|
server.setShellFactory(new NoShell)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package gitbucket.core.util
|
package gitbucket.core.util
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import java.net.URLConnection
|
import org.apache.tika.Tika
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import ControlUtil._
|
import ControlUtil._
|
||||||
import scala.util.Random
|
import scala.util.Random
|
||||||
@@ -9,8 +9,8 @@ import scala.util.Random
|
|||||||
object FileUtil {
|
object FileUtil {
|
||||||
|
|
||||||
def getMimeType(name: String): String =
|
def getMimeType(name: String): String =
|
||||||
defining(URLConnection.getFileNameMap()){ fileNameMap =>
|
defining(new Tika()){ tika =>
|
||||||
fileNameMap.getContentTypeFor(name) match {
|
tika.detect(name) match {
|
||||||
case null => "application/octet-stream"
|
case null => "application/octet-stream"
|
||||||
case mimeType => mimeType
|
case mimeType => mimeType
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,8 @@ object FileUtil {
|
|||||||
|
|
||||||
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
|
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
|
||||||
|
|
||||||
|
def isUploadableType(name: String): Boolean = mimeTypeWhiteList contains getMimeType(name)
|
||||||
|
|
||||||
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)
|
||||||
@@ -50,4 +52,14 @@ object FileUtil {
|
|||||||
FileUtils.deleteDirectory(dir)
|
FileUtils.deleteDirectory(dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mimeTypeWhiteList: Array[String] = Array(
|
||||||
|
"application/pdf",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"image/gif",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"text/plain")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ object Implicits {
|
|||||||
|
|
||||||
def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{
|
def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{
|
||||||
case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
|
case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
|
||||||
|
case path if path.startsWith("api/v3/orgs/") => path.substring(12/* "/api/v3/orgs".length */)
|
||||||
case path => path
|
case path => path
|
||||||
}).split("/")
|
}).split("/")
|
||||||
|
|
||||||
@@ -72,6 +73,8 @@ object Implicits {
|
|||||||
|
|
||||||
def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
|
def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
|
||||||
|
|
||||||
|
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit class RichSession(session: HttpSession){
|
implicit class RichSession(session: HttpSession){
|
||||||
|
|||||||
@@ -100,8 +100,18 @@ object JGitUtil {
|
|||||||
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
|
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String],
|
case class DiffInfo(
|
||||||
oldIsImage: Boolean, newIsImage: Boolean, oldObjectId: Option[String], newObjectId: Option[String])
|
changeType: ChangeType,
|
||||||
|
oldPath: String,
|
||||||
|
newPath: String,
|
||||||
|
oldContent: Option[String],
|
||||||
|
newContent: Option[String],
|
||||||
|
oldIsImage: Boolean,
|
||||||
|
newIsImage: Boolean,
|
||||||
|
oldObjectId: Option[String],
|
||||||
|
newObjectId: Option[String],
|
||||||
|
tooLarge: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file content data for the file content view of the repository viewer.
|
* The file content data for the file content view of the repository viewer.
|
||||||
@@ -495,11 +505,31 @@ object JGitUtil {
|
|||||||
while(treeWalk.next){
|
while(treeWalk.next){
|
||||||
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
|
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
|
||||||
buffer.append((if(!fetchContent){
|
buffer.append((if(!fetchContent){
|
||||||
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None, false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
|
DiffInfo(
|
||||||
|
changeType = ChangeType.ADD,
|
||||||
|
oldPath = null,
|
||||||
|
newPath = treeWalk.getPathString,
|
||||||
|
oldContent = None,
|
||||||
|
newContent = None,
|
||||||
|
oldIsImage = false,
|
||||||
|
newIsImage = newIsImage,
|
||||||
|
oldObjectId = None,
|
||||||
|
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
|
||||||
|
tooLarge = false
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
|
DiffInfo(
|
||||||
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
|
changeType = ChangeType.ADD,
|
||||||
false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
|
oldPath = null,
|
||||||
|
newPath = treeWalk.getPathString,
|
||||||
|
oldContent = None,
|
||||||
|
newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||||
|
oldIsImage = false,
|
||||||
|
newIsImage = newIsImage,
|
||||||
|
oldObjectId = None,
|
||||||
|
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
|
||||||
|
tooLarge = false
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
(buffer.toList, None)
|
(buffer.toList, None)
|
||||||
@@ -518,16 +548,52 @@ object JGitUtil {
|
|||||||
|
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
|
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
|
||||||
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
|
|
||||||
|
val diffs = git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala
|
||||||
|
diffs.map { diff =>
|
||||||
|
if(diffs.size > 100){
|
||||||
|
DiffInfo(
|
||||||
|
changeType = diff.getChangeType,
|
||||||
|
oldPath = diff.getOldPath,
|
||||||
|
newPath = diff.getNewPath,
|
||||||
|
oldContent = None,
|
||||||
|
newContent = None,
|
||||||
|
oldIsImage = false,
|
||||||
|
newIsImage = false,
|
||||||
|
oldObjectId = Option(diff.getOldId).map(_.name),
|
||||||
|
newObjectId = Option(diff.getNewId).map(_.name),
|
||||||
|
tooLarge = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
||||||
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
||||||
if(!fetchContent || oldIsImage || newIsImage){
|
if(!fetchContent || oldIsImage || newIsImage){
|
||||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None, oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
|
DiffInfo(
|
||||||
|
changeType = diff.getChangeType,
|
||||||
|
oldPath = diff.getOldPath,
|
||||||
|
newPath = diff.getNewPath,
|
||||||
|
oldContent = None,
|
||||||
|
newContent = None,
|
||||||
|
oldIsImage = oldIsImage,
|
||||||
|
newIsImage = newIsImage,
|
||||||
|
oldObjectId = Option(diff.getOldId).map(_.name),
|
||||||
|
newObjectId = Option(diff.getNewId).map(_.name),
|
||||||
|
tooLarge = false
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
|
DiffInfo(
|
||||||
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
changeType = diff.getChangeType,
|
||||||
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
oldPath = diff.getOldPath,
|
||||||
oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
|
newPath = diff.getNewPath,
|
||||||
|
oldContent = JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||||
|
newContent = JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||||
|
oldIsImage = oldIsImage,
|
||||||
|
newIsImage = newIsImage,
|
||||||
|
oldObjectId = Option(diff.getOldId).map(_.name),
|
||||||
|
newObjectId = Option(diff.getNewId).map(_.name),
|
||||||
|
tooLarge = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.toList
|
}.toList
|
||||||
}
|
}
|
||||||
@@ -713,7 +779,7 @@ object JGitUtil {
|
|||||||
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
|
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
|
||||||
using(git.getRepository.getObjectDatabase){ db =>
|
using(git.getRepository.getObjectDatabase){ db =>
|
||||||
val loader = db.open(id)
|
val loader = db.open(id)
|
||||||
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
|
if(loader.isLarge || (fetchLargeFile == false && FileUtil.isLarge(loader.getSize))){
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(loader.getBytes)
|
Some(loader.getBytes)
|
||||||
@@ -723,6 +789,22 @@ object JGitUtil {
|
|||||||
case e: MissingObjectException => None
|
case e: MissingObjectException => None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get objectLoader of the given object id from the Git repository.
|
||||||
|
*
|
||||||
|
* @param git the Git object
|
||||||
|
* @param id the object id
|
||||||
|
* @param f the function process ObjectLoader
|
||||||
|
* @return None if object does not exist
|
||||||
|
*/
|
||||||
|
def getObjectLoaderFromId[A](git: Git, id: ObjectId)(f: ObjectLoader => A):Option[A] = try {
|
||||||
|
using(git.getRepository.getObjectDatabase){ db =>
|
||||||
|
Some(f(db.open(id)))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case e: MissingObjectException => None
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all commit id in the specified repository.
|
* Returns all commit id in the specified repository.
|
||||||
*/
|
*/
|
||||||
@@ -791,7 +873,7 @@ object JGitUtil {
|
|||||||
return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
|
return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
|
||||||
}
|
}
|
||||||
|
|
||||||
def getBranches(owner: String, name: String, defaultBranch: String): Seq[BranchInfo] = {
|
def getBranches(owner: String, name: String, defaultBranch: String, origin: Boolean): Seq[BranchInfo] = {
|
||||||
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||||
val repo = git.getRepository
|
val repo = git.getRepository
|
||||||
val defaultObject = if (repo.getAllRefs.keySet().contains(defaultBranch)) {
|
val defaultObject = if (repo.getAllRefs.keySet().contains(defaultBranch)) {
|
||||||
@@ -802,20 +884,20 @@ object JGitUtil {
|
|||||||
|
|
||||||
git.branchList.call.asScala.map { ref =>
|
git.branchList.call.asScala.map { ref =>
|
||||||
val walk = new RevWalk(repo)
|
val walk = new RevWalk(repo)
|
||||||
try{
|
try {
|
||||||
val defaultCommit = walk.parseCommit(defaultObject)
|
val defaultCommit = walk.parseCommit(defaultObject)
|
||||||
val branchName = ref.getName.stripPrefix("refs/heads/")
|
val branchName = ref.getName.stripPrefix("refs/heads/")
|
||||||
val branchCommit = if(branchName == defaultBranch){
|
val branchCommit = if(branchName == defaultBranch){
|
||||||
defaultCommit
|
defaultCommit
|
||||||
}else{
|
} else {
|
||||||
walk.parseCommit(ref.getObjectId)
|
walk.parseCommit(ref.getObjectId)
|
||||||
}
|
}
|
||||||
val when = branchCommit.getCommitterIdent.getWhen
|
val when = branchCommit.getCommitterIdent.getWhen
|
||||||
val committer = branchCommit.getCommitterIdent.getName
|
val committer = branchCommit.getCommitterIdent.getName
|
||||||
val committerEmail = branchCommit.getCommitterIdent.getEmailAddress
|
val committerEmail = branchCommit.getCommitterIdent.getEmailAddress
|
||||||
val mergeInfo = if(branchName==defaultBranch){
|
val mergeInfo = if(origin && branchName == defaultBranch){
|
||||||
None
|
None
|
||||||
}else{
|
} else {
|
||||||
walk.reset()
|
walk.reset()
|
||||||
walk.setRevFilter( RevFilter.MERGE_BASE )
|
walk.setRevFilter( RevFilter.MERGE_BASE )
|
||||||
walk.markStart(branchCommit)
|
walk.markStart(branchCommit)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
|
|||||||
object Notifier {
|
object Notifier {
|
||||||
// TODO We want to be able to switch to mock.
|
// TODO We want to be able to switch to mock.
|
||||||
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
|
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
|
||||||
case settings if settings.notification => new Mailer(settings.smtp.get)
|
case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
|
||||||
case _ => new MockMailer
|
case _ => new MockMailer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
|||||||
database withSession { implicit session =>
|
database withSession { implicit session =>
|
||||||
defining(
|
defining(
|
||||||
s"[${r.name}] ${issue.title} (#${issue.issueId})" ->
|
s"[${r.name}] ${issue.title} (#${issue.issueId})" ->
|
||||||
msg(Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
|
msg(Markdown.toHtml(content, r, false, true, false))) { case (subject, msg) =>
|
||||||
recipients(issue) { to =>
|
recipients(issue) { to =>
|
||||||
val email = new HtmlEmail
|
val email = new HtmlEmail
|
||||||
email.setHostName(smtp.host)
|
email.setHostName(smtp.host)
|
||||||
@@ -87,7 +87,7 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
|||||||
email.setSSLOnConnect(ssl)
|
email.setSSLOnConnect(ssl)
|
||||||
}
|
}
|
||||||
smtp.fromAddress
|
smtp.fromAddress
|
||||||
.map (_ -> smtp.fromName.orNull)
|
.map (_ -> smtp.fromName.getOrElse(context.loginAccount.get.userName))
|
||||||
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
|
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
|
||||||
.foreach { case (address, name) =>
|
.foreach { case (address, name) =>
|
||||||
email.setFrom(address, name)
|
email.setFrom(address, name)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ object StringUtil {
|
|||||||
md.digest.map(b => "%02x".format(b)).mkString
|
md.digest.map(b => "%02x".format(b)).mkString
|
||||||
}
|
}
|
||||||
|
|
||||||
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
|
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8").replace("+", "%20")
|
||||||
|
|
||||||
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
|
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.scalatra.i18n.Messages
|
|||||||
trait Validations {
|
trait Validations {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constraint for the identifier such as user name, repository name or page name.
|
* Constraint for the identifier such as user name or page name.
|
||||||
*/
|
*/
|
||||||
def identifier: Constraint = new Constraint(){
|
def identifier: Constraint = new Constraint(){
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
@@ -19,6 +19,23 @@ trait Validations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constraint for the repository identifier.
|
||||||
|
*/
|
||||||
|
def repository: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
if(!value.matches("[a-zA-Z0-9\\-\\+_.]+")){
|
||||||
|
Some(s"${name} contains invalid character.")
|
||||||
|
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||||
|
Some(s"${name} starts with invalid character.")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constraint for the color pattern.
|
||||||
|
*/
|
||||||
def color = pattern("#[0-9a-fA-F]{6}")
|
def color = pattern("#[0-9a-fA-F]{6}")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,31 +7,92 @@ import gitbucket.core.util.Implicits.RichString
|
|||||||
trait LinkConverter { self: RequestCache =>
|
trait LinkConverter { self: RequestCache =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts issue id, username and commit id to link.
|
* Creates a link to the issue or the pull request from the issue id.
|
||||||
*/
|
*/
|
||||||
protected def convertRefsLinks(value: String, repository: RepositoryService.RepositoryInfo,
|
protected def createIssueLink(repository: RepositoryService.RepositoryInfo, issueId: Int)(implicit context: Context): String = {
|
||||||
|
val userName = repository.repository.userName
|
||||||
|
val repositoryName = repository.repository.repositoryName
|
||||||
|
|
||||||
|
getIssue(userName, repositoryName, issueId.toString) match {
|
||||||
|
case Some(issue) if (issue.isPullRequest) =>
|
||||||
|
s"""<a href="${context.path}/${userName}/${repositoryName}/pull/${issueId}">Pull #${issueId}</a>"""
|
||||||
|
case Some(_) =>
|
||||||
|
s"""<a href="${context.path}/${userName}/${repositoryName}/issues/${issueId}">Issue #${issueId}</a>"""
|
||||||
|
case None =>
|
||||||
|
s"Unknown #${issueId}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts issue id, username and commit id to link in the given text.
|
||||||
|
*/
|
||||||
|
protected def convertRefsLinks(text: String, repository: RepositoryService.RepositoryInfo,
|
||||||
issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = {
|
issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = {
|
||||||
|
|
||||||
// escape HTML tags
|
// escape HTML tags
|
||||||
val escaped = if(escapeHtml) value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) else value
|
val escaped = if(escapeHtml) text.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) else text
|
||||||
|
|
||||||
escaped
|
escaped
|
||||||
|
// convert username/project@SHA to link
|
||||||
|
.replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r){ m =>
|
||||||
|
getAccountByUserName(m.group(2)).map { _ =>
|
||||||
|
s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}</a>"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert username/project#Num to link
|
||||||
|
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
||||||
|
getIssue(m.group(2), m.group(3), m.group(4)) match {
|
||||||
|
case Some(issue) if (issue.isPullRequest) =>
|
||||||
|
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m.group(4)}">${m.group(2)}/${m.group(3)}#${m.group(4)}</a>""")
|
||||||
|
case Some(_) =>
|
||||||
|
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/issues/${m.group(4)}">${m.group(2)}/${m.group(3)}#${m.group(4)}</a>""")
|
||||||
|
case None =>
|
||||||
|
Some(s"""${m.group(2)}/${m.group(3)}#${m.group(4)}""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert username@SHA to link
|
||||||
|
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r ) { m =>
|
||||||
|
getAccountByUserName(m.group(2)).map { _ =>
|
||||||
|
s"""<a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m.group(3).substring(0, 7)}</a>"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert username#Num to link
|
||||||
|
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r ) { m =>
|
||||||
|
getIssue(m.group(2), repository.name, m.group(3)) match {
|
||||||
|
case Some(issue) if(issue.isPullRequest) =>
|
||||||
|
Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/pull/${m.group(3)}">${m.group(2)}#${m.group(3)}</a>""")
|
||||||
|
case Some(_) =>
|
||||||
|
Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/issues/${m.group(3)}">${m.group(2)}#${m.group(3)}</a>""")
|
||||||
|
case None =>
|
||||||
|
Some(s"""${m.group(2)}#${m.group(3)}""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// convert issue id to link
|
// convert issue id to link
|
||||||
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
.replaceBy(("(?<=(^|\\W))(GH-|" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r){ m =>
|
||||||
getIssue(repository.owner, repository.name, m.group(2)) match {
|
val prefix = if(m.group(2) == "issue:") "#" else m.group(2)
|
||||||
case Some(issue) if(issue.isPullRequest)
|
getIssue(repository.owner, repository.name, m.group(3)) match {
|
||||||
=> Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>""")
|
case Some(issue) if(issue.isPullRequest) =>
|
||||||
case Some(_) => Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>""")
|
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(3)}">${prefix}${m.group(3)}</a>""")
|
||||||
case None => Some(s"""#${m.group(2)}""")
|
case Some(_) =>
|
||||||
|
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(3)}">${prefix}${m.group(3)}</a>""")
|
||||||
|
case None =>
|
||||||
|
Some(s"""${m.group(2)}${m.group(3)}""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
|
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"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
|
.replaceAll("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
package gitbucket.core.view
|
package gitbucket.core.view
|
||||||
|
|
||||||
import java.text.Normalizer
|
import java.text.Normalizer
|
||||||
import java.util.Locale
|
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
import gitbucket.core.controller.Context
|
import gitbucket.core.controller.Context
|
||||||
import gitbucket.core.service.{RepositoryService, RequestCache, WikiService}
|
import gitbucket.core.service.{RepositoryService, RequestCache}
|
||||||
import gitbucket.core.util.StringUtil
|
import gitbucket.core.util.StringUtil
|
||||||
import org.parboiled.common.StringUtils
|
import io.github.gitbucket.markedj._
|
||||||
import org.pegdown.LinkRenderer.Rendering
|
import io.github.gitbucket.markedj.Utils._
|
||||||
import org.pegdown._
|
|
||||||
import org.pegdown.ast._
|
|
||||||
|
|
||||||
import scala.collection.JavaConverters._
|
|
||||||
|
|
||||||
object Markdown {
|
object Markdown {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts Markdown of Wiki pages to HTML.
|
* Converts Markdown of Wiki pages to HTML.
|
||||||
|
*
|
||||||
|
* @param repository the repository which contains the markdown
|
||||||
|
* @param enableWikiLink if true then wiki style link is available in markdown
|
||||||
|
* @param enableRefsLink if true then issue reference (e.g. #123) is rendered as link
|
||||||
|
* @param enableAnchor if true then anchor for headline is generated
|
||||||
|
* @param enableTaskList if true then task list syntax is available
|
||||||
|
* @param hasWritePermission true if user has writable to ths given repository
|
||||||
|
* @param pages the list of existing Wiki pages
|
||||||
*/
|
*/
|
||||||
def toHtml(markdown: String,
|
def toHtml(markdown: String,
|
||||||
repository: RepositoryService.RepositoryInfo,
|
repository: RepositoryService.RepositoryInfo,
|
||||||
enableWikiLink: Boolean,
|
enableWikiLink: Boolean,
|
||||||
enableRefsLink: Boolean,
|
enableRefsLink: Boolean,
|
||||||
|
enableAnchor: Boolean,
|
||||||
enableTaskList: Boolean = false,
|
enableTaskList: Boolean = false,
|
||||||
hasWritePermission: Boolean = false,
|
hasWritePermission: Boolean = false,
|
||||||
pages: List[String] = Nil)(implicit context: Context): String = {
|
pages: List[String] = Nil)(implicit context: Context): String = {
|
||||||
|
|
||||||
// escape issue id
|
// escape issue id
|
||||||
val s = if(enableRefsLink){
|
val s = if(enableRefsLink){
|
||||||
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
||||||
@@ -34,99 +38,113 @@ object Markdown {
|
|||||||
|
|
||||||
// escape task list
|
// escape task list
|
||||||
val source = if(enableTaskList){
|
val source = if(enableTaskList){
|
||||||
GitBucketHtmlSerializer.escapeTaskList(s)
|
escapeTaskList(s)
|
||||||
} else s
|
} else s
|
||||||
|
|
||||||
val rootNode = new PegDownProcessor(
|
val options = new Options()
|
||||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML
|
options.setSanitize(true)
|
||||||
).parseMarkdown(source.toCharArray)
|
options.setBreaks(true)
|
||||||
|
val renderer = new GitBucketMarkedRenderer(options, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages)
|
||||||
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages).toHtml(rootNode)
|
Marked.marked(source, options, renderer)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class GitBucketLinkRender(
|
/**
|
||||||
context: Context,
|
* Extends markedj Renderer for GitBucket
|
||||||
repository: RepositoryService.RepositoryInfo,
|
*/
|
||||||
enableWikiLink: Boolean,
|
class GitBucketMarkedRenderer(options: Options, repository: RepositoryService.RepositoryInfo,
|
||||||
pages: List[String]) extends LinkRenderer with WikiService {
|
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
|
||||||
|
pages: List[String])
|
||||||
|
(implicit val context: Context) extends Renderer(options) with LinkConverter with RequestCache {
|
||||||
|
|
||||||
override def render(node: WikiLinkNode): Rendering = {
|
override def heading(text: String, level: Int, raw: String): String = {
|
||||||
if(enableWikiLink){
|
val id = generateAnchorName(text)
|
||||||
try {
|
val out = new StringBuilder()
|
||||||
val text = node.getText
|
|
||||||
val (label, page) = if(text.contains('|')){
|
out.append("<h" + level + " id=\"" + options.getHeaderPrefix + id + "\" class=\"markdown-head\">")
|
||||||
val i = text.indexOf('|')
|
|
||||||
(text.substring(0, i), text.substring(i + 1))
|
if(enableAnchor){
|
||||||
|
out.append("<a class=\"markdown-anchor-link\" href=\"#" + id + "\"></a>")
|
||||||
|
out.append("<a class=\"markdown-anchor\" name=\"" + id + "\"></a>")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.append(text)
|
||||||
|
out.append("</h" + level + ">\n")
|
||||||
|
out.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override def code(code: String, lang: String, escaped: Boolean): String = {
|
||||||
|
"<pre class=\"prettyprint" + (if(lang != null) s" ${options.getLangPrefix}${lang}" else "" )+ "\">" +
|
||||||
|
(if(escaped) code else escape(code, true)) + "</pre>"
|
||||||
|
}
|
||||||
|
|
||||||
|
override def list(body: String, ordered: Boolean): String = {
|
||||||
|
var listType: String = null
|
||||||
|
if (ordered) {
|
||||||
|
listType = "ol"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listType = "ul"
|
||||||
|
}
|
||||||
|
if(body.contains("""class="task-list-item-checkbox"""")){
|
||||||
|
return "<" + listType + " class=\"task-list\">\n" + body + "</" + listType + ">\n"
|
||||||
} else {
|
} else {
|
||||||
(text, text)
|
return "<" + listType + ">\n" + body + "</" + listType + ">\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def listitem(text: String): String = {
|
||||||
|
if(text.contains("""class="task-list-item-checkbox" """)){
|
||||||
|
return "<li class=\"task-list-item\">" + text + "</li>\n"
|
||||||
|
} else {
|
||||||
|
return "<li>" + text + "</li>\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def text(text: String): String = {
|
||||||
|
// convert commit id and username to link.
|
||||||
|
val t1 = if(enableRefsLink) convertRefsLinks(text, repository, "issue:", false) else text
|
||||||
|
|
||||||
|
// convert task list to checkbox.
|
||||||
|
val t2 = if(enableTaskList) convertCheckBox(t1, hasWritePermission) else t1
|
||||||
|
|
||||||
|
t2
|
||||||
|
}
|
||||||
|
|
||||||
|
override def link(href: String, title: String, text: String): String = {
|
||||||
|
super.link(fixUrl(href, false), title, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def image(href: String, title: String, text: String): String = {
|
||||||
|
super.image(fixUrl(href, true), title, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def nolink(text: String): String = {
|
||||||
|
if(enableWikiLink && text.startsWith("[[") && text.endsWith("]]")){
|
||||||
|
val link = text.replaceAll("(^\\[\\[|\\]\\]$)", "")
|
||||||
|
|
||||||
|
val (label, page) = if(link.contains('|')){
|
||||||
|
val i = link.indexOf('|')
|
||||||
|
(link.substring(0, i), link.substring(i + 1))
|
||||||
|
} else {
|
||||||
|
(link, link)
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
|
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
|
||||||
|
|
||||||
if(pages.contains(page)){
|
if(pages.contains(page)){
|
||||||
new Rendering(url, label)
|
"<a href=\"" + url + "\">" + escape(label) + "</a>"
|
||||||
} else {
|
} else {
|
||||||
new Rendering(url, label).withAttribute("class", "absent")
|
"<a href=\"" + url + "\" class=\"absent\">" + escape(label) + "</a>"
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
super.render(node)
|
escape(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class GitBucketVerbatimSerializer extends VerbatimSerializer {
|
|
||||||
def serialize(node: VerbatimNode, printer: Printer): Unit = {
|
|
||||||
printer.println.print("<pre")
|
|
||||||
if (!StringUtils.isEmpty(node.getType)) {
|
|
||||||
printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"')
|
|
||||||
}
|
|
||||||
printer.print(">")
|
|
||||||
var text: String = node.getText
|
|
||||||
while (text.charAt(0) == '\n') {
|
|
||||||
printer.print("<br/>")
|
|
||||||
text = text.substring(1)
|
|
||||||
}
|
|
||||||
printer.printEncoded(text)
|
|
||||||
printer.print("</pre>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GitBucketHtmlSerializer(
|
|
||||||
markdown: String,
|
|
||||||
repository: RepositoryService.RepositoryInfo,
|
|
||||||
enableWikiLink: Boolean,
|
|
||||||
enableRefsLink: Boolean,
|
|
||||||
enableTaskList: Boolean,
|
|
||||||
hasWritePermission: Boolean,
|
|
||||||
pages: List[String]
|
|
||||||
)(implicit val context: Context) extends ToHtmlSerializer(
|
|
||||||
new GitBucketLinkRender(context, repository, enableWikiLink, pages),
|
|
||||||
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
|
|
||||||
) with LinkConverter with RequestCache {
|
|
||||||
|
|
||||||
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = {
|
|
||||||
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url, true)).print("\">")
|
|
||||||
.print("<img src=\"").print(fixUrl(url, true)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
|
|
||||||
}
|
|
||||||
|
|
||||||
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
|
|
||||||
printer.print('<').print('a')
|
|
||||||
printAttribute("href", fixUrl(rendering.href))
|
|
||||||
for (attr <- rendering.attributes.asScala) {
|
|
||||||
printAttribute(attr.name, attr.value)
|
|
||||||
}
|
|
||||||
printer.print('>').print(rendering.text).print("</a>")
|
|
||||||
}
|
|
||||||
|
|
||||||
private def fixUrl(url: String, isImage: Boolean = false): String = {
|
private def fixUrl(url: String, isImage: Boolean = false): String = {
|
||||||
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
|
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
|
||||||
url
|
url
|
||||||
} else if(url.startsWith("#")){
|
} else if(url.startsWith("#")){
|
||||||
("#" + GitBucketHtmlSerializer.generateAnchorName(url.substring(1)))
|
("#" + generateAnchorName(url.substring(1)))
|
||||||
} else if(!enableWikiLink){
|
} else if(!enableWikiLink){
|
||||||
if(context.currentPath.contains("/blob/")){
|
if(context.currentPath.contains("/blob/")){
|
||||||
url + (if(isImage) "?raw=true" else "")
|
url + (if(isImage) "?raw=true" else "")
|
||||||
@@ -144,118 +162,23 @@ class GitBucketHtmlSerializer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def printAttribute(name: String, value: String): Unit = {
|
|
||||||
printer.print(' ').print(name).print('=').print('"').print(value).print('"')
|
|
||||||
}
|
|
||||||
|
|
||||||
private def printHeaderTag(node: HeaderNode): Unit = {
|
|
||||||
val tag = s"h${node.getLevel}"
|
|
||||||
val headerTextString = printChildrenToString(node)
|
|
||||||
val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
|
|
||||||
printer.print(s"""<$tag class="markdown-head">""")
|
|
||||||
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
|
|
||||||
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
|
|
||||||
visitChildren(node)
|
|
||||||
printer.print(s"</$tag>")
|
|
||||||
}
|
|
||||||
|
|
||||||
override def visit(node: HeaderNode): Unit = {
|
|
||||||
printHeaderTag(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def visit(node: TextNode): Unit = {
|
|
||||||
// convert commit id and username to link.
|
|
||||||
val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
|
|
||||||
|
|
||||||
// convert task list to checkbox.
|
|
||||||
val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t
|
|
||||||
|
|
||||||
if (abbreviations.isEmpty) {
|
|
||||||
printer.print(text)
|
|
||||||
} else {
|
|
||||||
printWithAbbreviations(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def visit(node: VerbatimNode) {
|
|
||||||
val printer = new Printer()
|
|
||||||
val serializer = verbatimSerializers.get(VerbatimSerializer.DEFAULT)
|
|
||||||
serializer.serialize(node, printer)
|
|
||||||
val html = printer.getString
|
|
||||||
|
|
||||||
// convert commit id and username to link.
|
|
||||||
val t = if(enableRefsLink) convertRefsLinks(html, repository, "issue:", escapeHtml = false) else html
|
|
||||||
|
|
||||||
this.printer.print(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def visit(node: BulletListNode): Unit = {
|
|
||||||
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
|
|
||||||
printer.println().print("""<ul class="task-list">""").indent(+2)
|
|
||||||
visitChildren(node)
|
|
||||||
printer.indent(-2).println().print("</ul>")
|
|
||||||
} else {
|
|
||||||
printIndentedTag(node, "ul")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def visit(node: ListItemNode): Unit = {
|
|
||||||
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
|
|
||||||
printer.println()
|
|
||||||
printer.print("""<li class="task-list-item">""")
|
|
||||||
visitChildren(node)
|
|
||||||
printer.print("</li>")
|
|
||||||
} else {
|
|
||||||
printer.println()
|
|
||||||
printTag(node, "li")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def visit(node: ExpLinkNode) {
|
|
||||||
printLink(linkRenderer.render(node, printLinkChildrenToString(node)))
|
|
||||||
}
|
|
||||||
|
|
||||||
def printLinkChildrenToString(node: SuperNode) = {
|
|
||||||
val priorPrinter = printer
|
|
||||||
printer = new Printer()
|
|
||||||
visitLinkChildren(node)
|
|
||||||
val result = printer.getString()
|
|
||||||
printer = priorPrinter
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
def visitLinkChildren(node: SuperNode) {
|
|
||||||
import scala.collection.JavaConversions._
|
|
||||||
node.getChildren.foreach(child => child match {
|
|
||||||
case node: ExpImageNode => visitLinkChild(node)
|
|
||||||
case node: SuperNode => visitLinkChildren(node)
|
|
||||||
case _ => child.accept(this)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
def visitLinkChild(node: ExpImageNode) {
|
|
||||||
printer.print("<img src=\"").print(fixUrl(node.url, true)).print("\" alt=\"").printEncoded(printChildrenToString(node)).print("\"/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def escapeTaskList(text: String): String = {
|
def escapeTaskList(text: String): String = {
|
||||||
Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ")
|
Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def generateAnchorName(text: String): String = {
|
||||||
|
val normalized = Normalizer.normalize(text.replaceAll("<.*>", "").replaceAll("[\\s]", "-"), Normalizer.Form.NFD)
|
||||||
|
val encoded = StringUtil.urlEncode(normalized)
|
||||||
|
encoded.toLowerCase(Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
|
||||||
def convertCheckBox(text: String, hasWritePermission: Boolean): String = {
|
def convertCheckBox(text: String, hasWritePermission: Boolean): String = {
|
||||||
val disabled = if (hasWritePermission) "" else "disabled"
|
val disabled = if (hasWritePermission) "" else "disabled"
|
||||||
text.replaceAll("task:x:", """<input type="checkbox" class="task-list-item-checkbox" checked="checked" """ + disabled + "/>")
|
text.replaceAll("task:x:", """<input type="checkbox" class="task-list-item-checkbox" checked="checked" """ + disabled + "/>")
|
||||||
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
|
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import java.util.{Date, Locale, TimeZone}
|
|||||||
|
|
||||||
import gitbucket.core.controller.Context
|
import gitbucket.core.controller.Context
|
||||||
import gitbucket.core.model.CommitState
|
import gitbucket.core.model.CommitState
|
||||||
import gitbucket.core.plugin.{RenderRequest, PluginRegistry, Renderer}
|
import gitbucket.core.plugin.{RenderRequest, PluginRegistry}
|
||||||
import gitbucket.core.service.{RepositoryService, RequestCache}
|
import gitbucket.core.service.{RepositoryService, RequestCache}
|
||||||
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
|
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
|
||||||
|
|
||||||
@@ -91,22 +91,36 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
enableTaskList: Boolean = false,
|
enableTaskList: Boolean = false,
|
||||||
hasWritePermission: Boolean = false,
|
hasWritePermission: Boolean = false,
|
||||||
pages: List[String] = Nil)(implicit context: Context): Html =
|
pages: List[String] = Nil)(implicit context: Context): Html =
|
||||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages))
|
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, true, enableTaskList, hasWritePermission, pages))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the given source (only markdown is supported in default) as HTML.
|
||||||
|
* You can test if a file is renderable in this method by [[isRenderable()]].
|
||||||
|
*/
|
||||||
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
|
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
|
||||||
repository: RepositoryService.RepositoryInfo,
|
repository: RepositoryService.RepositoryInfo,
|
||||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: Context): Html = {
|
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = {
|
||||||
|
|
||||||
val fileName = filePath.reverse.head.toLowerCase
|
val fileName = filePath.reverse.head.toLowerCase
|
||||||
val extension = FileUtil.getExtension(fileName)
|
val extension = FileUtil.getExtension(fileName)
|
||||||
val renderer = PluginRegistry().getRenderer(extension)
|
val renderer = PluginRegistry().getRenderer(extension)
|
||||||
renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context))
|
renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the given file is renderable. It's tested by the file extension.
|
||||||
|
*/
|
||||||
def isRenderable(fileName: String): Boolean = {
|
def isRenderable(fileName: String): Boolean = {
|
||||||
PluginRegistry().renderableExtensions.exists(extension => fileName.toLowerCase.endsWith("." + extension))
|
PluginRegistry().renderableExtensions.exists(extension => fileName.toLowerCase.endsWith("." + extension))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a link to the issue or the pull request from the issue id.
|
||||||
|
*/
|
||||||
|
def issueLink(repository: RepositoryService.RepositoryInfo, issueId: Int)(implicit context: Context): Html = {
|
||||||
|
Html(createIssueLink(repository, issueId))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns <img> which displays the avatar icon for the given user name.
|
* Returns <img> which displays the avatar icon for the given user name.
|
||||||
* This method looks up Gravatar if avatar icon has not been configured in user settings.
|
* This method looks up Gravatar if avatar icon has not been configured in user settings.
|
||||||
@@ -155,6 +169,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
|
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove html tags from the given Html instance.
|
||||||
|
*/
|
||||||
|
def removeHtml(html: Html): Html = Html(html.body.replaceAll("<.+?>", ""))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL encode except '/'.
|
* URL encode except '/'.
|
||||||
*/
|
*/
|
||||||
@@ -270,4 +289,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
case CommitState.ERROR => "Failed"
|
case CommitState.ERROR => "Failed"
|
||||||
case CommitState.FAILURE => "Failed"
|
case CommitState.FAILURE => "Failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string)
|
||||||
|
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
|
||||||
|
|
||||||
|
def detectAndRenderLinks(text: String): Html = {
|
||||||
|
Html(detectAndRenderLinksRegex.replaceAllIn(text, m => s"""<a href="${m.group(0)}">${m.group(0)}</a>"""))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="span9">
|
<div class="span9">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="box-header">Personal access tokens</div>
|
<div class="box-header">Personal access tokens</div>
|
||||||
<div class="box-content">
|
<div class="box-content-bottom">
|
||||||
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
||||||
No tokens.
|
No tokens.
|
||||||
}else{
|
}else{
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
|
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="box-header">Generate new token</div>
|
<div class="box-header">Generate new token</div>
|
||||||
<div class="box-content">
|
<div class="box-content-bottom">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="note" class="strong">Token description</label>
|
<label for="note" class="strong">Token description</label>
|
||||||
<div><span id="error-note" class="error"></span></div>
|
<div><span id="error-note" class="error"></span></div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<form action="@url(account.userName)/_edit" method="POST" validate="true">
|
<form action="@url(account.userName)/_edit" method="POST" validate="true">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="box-header">Profile</div>
|
<div class="box-header">Profile</div>
|
||||||
<div class="box-content">
|
<div class="box-content-bottom">
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span6">
|
<div class="span6">
|
||||||
@if(account.password.nonEmpty){
|
@if(account.password.nonEmpty){
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
@if(account.url.isDefined){
|
@if(account.url.isDefined){
|
||||||
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
|
<div><i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a></div>
|
||||||
}
|
}
|
||||||
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
<div><i class="octicon octicon-clock"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
||||||
</div>
|
</div>
|
||||||
@if(groupNames.nonEmpty){
|
@if(groupNames.nonEmpty){
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
|||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="icon-ok"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
|
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="octicon octicon-check"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
|
||||||
@groupNames.map { groupName =>
|
@groupNames.map { groupName =>
|
||||||
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
|
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
|||||||
<fieldset class="margin">
|
<fieldset class="margin">
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}>
|
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}>
|
||||||
<span class="strong"><img src="@assets/common/images/repo_public.png"/> </i> Public</span><br>
|
<span class="strong"><i class="octicon octicon-repo"></i> </i> Public</span><br>
|
||||||
<div>
|
<div>
|
||||||
<span>All users and guests can read this repository.</span>
|
<span>All users and guests can read this repository.</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
|
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
|
||||||
<span class="strong"><img src="@assets/common/images/repo_private.png"/> </i> Private</span><br>
|
<span class="strong"><i class="octicon octicon-lock"></i> </i> Private</span><br>
|
||||||
<div>
|
<div>
|
||||||
<span>Only collaborators can read this repository.</span>
|
<span>Only collaborators can read this repository.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="block-header">
|
<div class="block-header">
|
||||||
<a href="@url(repository)">@repository.name</a>
|
<a href="@url(repository)">@repository.name</a>
|
||||||
@if(repository.repository.isPrivate){
|
@if(repository.repository.isPrivate){
|
||||||
<i class="icon-lock"></i>
|
<i class="octicon octicon-lock"></i>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if(repository.repository.originUserName.isDefined){
|
@if(repository.repository.originUserName.isDefined){
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="span9">
|
<div class="span9">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="box-header">SSH Keys</div>
|
<div class="box-header">SSH Keys</div>
|
||||||
<div class="box-content">
|
<div class="box-content-bottom">
|
||||||
@if(sshKeys.isEmpty){
|
@if(sshKeys.isEmpty){
|
||||||
No keys
|
No keys
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="box-header">Add an SSH Key</div>
|
<div class="box-header">Add an SSH Key</div>
|
||||||
<div class="box-content">
|
<div class="box-content-bottom">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="title" class="strong">Title</label>
|
<label for="title" class="strong">Title</label>
|
||||||
<div><span id="error-title" class="error"></span></div>
|
<div><span id="error-title" class="error"></span></div>
|
||||||
|
|||||||
@@ -3,20 +3,21 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span3">
|
<div class="span3">
|
||||||
<div class="box">
|
<ul class="nav nav-tabs nav-stacked side-menu" id="system-admin-menu-container">
|
||||||
<ul class="nav nav-tabs nav-stacked side-menu">
|
|
||||||
<li@if(active=="users"){ class="active"}>
|
<li@if(active=="users"){ class="active"}>
|
||||||
<a href="@path/admin/users">User Management</a>
|
<a href="@path/admin/users">User Management</a>
|
||||||
</li>
|
</li>
|
||||||
<li@if(active=="system"){ class="active"}>
|
<li@if(active=="system"){ class="active"}>
|
||||||
<a href="@path/admin/system">System Settings</a>
|
<a href="@path/admin/system">System Settings</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li@if(active=="plugins"){ class="active"}>
|
||||||
|
<a href="@path/admin/plugins">Plugins</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="@path/console/login.jsp">H2 Console</a>
|
<a href="@path/console/login.jsp">H2 Console</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="span9">
|
<div class="span9">
|
||||||
@body
|
@body
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
@(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context)
|
||||||
|
@import context._
|
||||||
|
@import gitbucket.core.view.helpers._
|
||||||
|
@html.main("Plugins"){
|
||||||
|
@admin.html.menu("plugins") {
|
||||||
|
<h1>Installed plugins</h1>
|
||||||
|
|
||||||
|
@if(plugins.size > 0) {
|
||||||
|
<ul>
|
||||||
|
@plugins.map {plugin =>
|
||||||
|
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.version</a></li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@plugins.map {plugin =>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-header">@plugin.pluginName</div>
|
||||||
|
<div class="box-content-bottom">
|
||||||
|
<p><span class="strong">Id: </span>@plugin.pluginId</p>
|
||||||
|
<p><span class="strong">Version: </span>@plugin.version</p>
|
||||||
|
<p><span class="strong">Name: </span>@plugin.pluginName</p>
|
||||||
|
<p class="muted">@plugin.description</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<p>No plugin detected on your gitbucket installation.</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<form action="@path/admin/system" method="POST" validate="true">
|
<form action="@path/admin/system" method="POST" validate="true">
|
||||||
<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-bottom">
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<!-- GITBUCKET_HOME -->
|
<!-- GITBUCKET_HOME -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
@@ -224,14 +224,25 @@
|
|||||||
<!-- Notification email -->
|
<!-- Notification email -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<hr>
|
<hr>
|
||||||
<label class="strong">Notification email</label>
|
<label class="strong">Notifications</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="checkbox">
|
<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>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="form-horizontal notification">
|
<!--====================================================================-->
|
||||||
|
<!-- Communication email -->
|
||||||
|
<!--====================================================================-->
|
||||||
|
<hr>
|
||||||
|
<label class="strong">Communication</label>
|
||||||
|
<fieldset>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" id="useSMTP" name="useSMTP" @if(settings.useSMTP){ checked}/>
|
||||||
|
SMTP
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-horizontal useSMTP">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label" for="smtpHost">SMTP Host</label>
|
<label class="control-label" for="smtpHost">SMTP Host</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@@ -277,12 +288,15 @@
|
|||||||
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
|
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="muted">
|
||||||
|
Enable notification not only SMTP configuration if you want to send notification email.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fieldset>
|
<div class="align-right" style="margin-top: 20px;">
|
||||||
<input type="submit" class="btn btn-success" value="Apply changes"/>
|
<input type="submit" class="btn btn-success" value="Apply changes"/>
|
||||||
</fieldset>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,8 +306,16 @@ $(function(){
|
|||||||
$('.ssh input').prop('disabled', !$(this).prop('checked'));
|
$('.ssh input').prop('disabled', !$(this).prop('checked'));
|
||||||
}).change();
|
}).change();
|
||||||
|
|
||||||
$('#notification').change(function(){
|
$('#useSMTP').change(function(){
|
||||||
$('.notification input').prop('disabled', !$(this).prop('checked'));
|
$('.useSMTP input').prop('disabled', !$(this).prop('checked'));
|
||||||
|
|
||||||
|
// With only SMTP in current version, notification cannot be enabled if no communication channel exists
|
||||||
|
$('#notification').prop('disabled', !$(this).prop('checked'));
|
||||||
|
|
||||||
|
if (!$(this).prop('checked')) {
|
||||||
|
// With only SMTP in current version, if SMTP is unchecked then we disable notification
|
||||||
|
$('#notification').prop('checked', false);
|
||||||
|
}
|
||||||
}).change();
|
}).change();
|
||||||
|
|
||||||
$('#ldapAuthentication').change(function(){
|
$('#ldapAuthentication').change(function(){
|
||||||
|
|||||||
@@ -43,10 +43,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<hr>
|
<hr>
|
||||||
@if(!account.isGroupAccount){
|
@if(!account.isGroupAccount){
|
||||||
<i class="icon-envelope"></i> @account.mailAddress
|
<i class="octicon octicon-mail"></i> @account.mailAddress
|
||||||
}
|
}
|
||||||
@account.url.map { url =>
|
@account.url.map { url =>
|
||||||
<i class="icon-home"></i> @url
|
<i class="octicon octicon-home"></i> @url
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
<span class="small">
|
<span class="small">
|
||||||
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
||||||
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
|
<i class="octicon octicon-issue-opened @(if(condition.state == "open"){"active"})"></i>
|
||||||
@openCount Open
|
@openCount Open
|
||||||
</a>
|
</a>
|
||||||
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
|
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
|
||||||
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
|
<i class="octicon octicon-check @(if(condition.state == "closed"){"active"})"></i>
|
||||||
@closedCount Closed
|
@closedCount Closed
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="padding-top: 15px; padding-bottom: 15px;">
|
<td style="padding-top: 15px; padding-bottom: 15px;">
|
||||||
@if(issue.isPullRequest){
|
@if(issue.isPullRequest){
|
||||||
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
|
<i class="octicon octicon-git-pull-request @(if(issue.closed) "closed" else "open")"></i>
|
||||||
} else {
|
} else {
|
||||||
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
|
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")"></i>
|
||||||
}
|
}
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
||||||
@if(issue.isPullRequest){
|
@if(issue.isPullRequest){
|
||||||
@@ -39,18 +39,19 @@
|
|||||||
}
|
}
|
||||||
@if(commentCount > 0){
|
@if(commentCount > 0){
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
|
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
|
||||||
<img src="@assets/common/images/comment-active.png"> @commentCount
|
<i class="octicon octicon-comment active"></i> @commentCount
|
||||||
</a>
|
</a>
|
||||||
} else {
|
} else {
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
|
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
|
||||||
<img src="@assets/common/images/comment.png"> @commentCount
|
<i class="octicon octicon-comment"></i> @commentCount
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<div class="small muted" style="margin-left: 20px; margin-top: 5px;">
|
<div class="small muted" style="margin-left: 20px; margin-top: 5px;">
|
||||||
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||||
@milestone.map { milestone =>
|
@milestone.map { milestone =>
|
||||||
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
|
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i>
|
||||||
|
@milestone</a></span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
<div class="dashboard-nav">
|
<div class="dashboard-nav">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a href="@path/" @if(active == ""){ class="active"}>
|
<a href="@path/" @if(active == ""){ class="active"}>
|
||||||
<img src="@assets/common/images/menu-feed.png">
|
<i class="octicon octicon-rss"></i>
|
||||||
News Feed
|
News Feed
|
||||||
</a>
|
</a>
|
||||||
@if(loginAccount.isDefined){
|
@if(loginAccount.isDefined){
|
||||||
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
|
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
|
||||||
<img src="@assets/common/images/menu-pulls.png">
|
<i class="octicon octicon-git-pull-request"></i>
|
||||||
Pull Requests
|
Pull Requests
|
||||||
</a>
|
</a>
|
||||||
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
|
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
|
||||||
<img src="@assets/common/images/menu-issues.png">
|
<i class="octicon octicon-issue-opened"></i>
|
||||||
Issues
|
Issues
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,8 @@ div.dashboard-nav {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.dashboard-nav a {
|
div.dashboard-nav a {
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
@@ -36,8 +38,17 @@ div.dashboard-nav a {
|
|||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.dashboard-nav a:hover {
|
|
||||||
|
div.dashboard-nav .octicon{
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dashboard-nav a:hover,div.dashboard-nav a:hover .octicon {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.dashboard-nav a.active {
|
div.dashboard-nav a.active {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@(id: String, width: Int)(implicit context: gitbucket.core.controller.Context)
|
@(id: String, width: Int)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
<input type="text" name="@id" id="@id" style="width: @{width}px; margin-bottom: 0px;"/>
|
<input type="text" name="@id" id="@id" autocomplete="off" style="width: @{width}px; margin-bottom: 0px;"/>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
$('#@id').typeahead({
|
$('#@id').typeahead({
|
||||||
|
|||||||
@@ -8,20 +8,20 @@
|
|||||||
@activities.map { activity =>
|
@activities.map { activity =>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
@(activity.activityType match {
|
@(activity.activityType match {
|
||||||
case "open_issue" => detailActivity(activity, "activity-issue.png")
|
case "open_issue" => detailActivity(activity, "issue-opened")
|
||||||
case "comment_issue" => detailActivity(activity, "activity-comment.png")
|
case "comment_issue" => detailActivity(activity, "comment-discussion")
|
||||||
case "comment_commit" => detailActivity(activity, "activity-comment.png")
|
case "comment_commit" => detailActivity(activity, "comment-discussion")
|
||||||
case "close_issue" => detailActivity(activity, "activity-issue-close.png")
|
case "close_issue" => detailActivity(activity, "issue-closed")
|
||||||
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
|
case "reopen_issue" => detailActivity(activity, "issue-reopened")
|
||||||
case "open_pullreq" => detailActivity(activity, "activity-merge.png")
|
case "open_pullreq" => detailActivity(activity, "git-pull-request")
|
||||||
case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
|
case "merge_pullreq" => detailActivity(activity, "git-merge")
|
||||||
case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
|
case "create_repository" => simpleActivity(activity, "repo")
|
||||||
case "create_branch" => simpleActivity(activity, "activity-branch.png")
|
case "create_branch" => simpleActivity(activity, "git-branch")
|
||||||
case "delete_branch" => simpleActivity(activity, "activity-delete.png")
|
case "delete_branch" => simpleActivity(activity, "circle-slash")
|
||||||
case "create_tag" => simpleActivity(activity, "activity-tag.png")
|
case "create_tag" => simpleActivity(activity, "tag")
|
||||||
case "delete_tag" => simpleActivity(activity, "activity-delete.png")
|
case "delete_tag" => simpleActivity(activity, "circle-slash")
|
||||||
case "fork" => simpleActivity(activity, "activity-fork.png")
|
case "fork" => simpleActivity(activity, "repo-forked")
|
||||||
case "push" => customActivity(activity, "activity-commit.png"){
|
case "push" => customActivity(activity, "git-commit"){
|
||||||
<div class="small activity-message">
|
<div class="small activity-message">
|
||||||
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
|
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
|
||||||
if(i == 3){
|
if(i == 3){
|
||||||
@@ -37,12 +37,12 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
case "create_wiki" => customActivity(activity, "activity-wiki.png"){
|
case "create_wiki" => customActivity(activity, "book"){
|
||||||
<div class="small activity-message">
|
<div class="small activity-message">
|
||||||
Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
|
Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
|
case "edit_wiki" => customActivity(activity, "book"){
|
||||||
activity.additionalInfo.get.split(":") match {
|
activity.additionalInfo.get.split(":") match {
|
||||||
case Array(pageName, commitId) =>
|
case Array(pageName, commitId) =>
|
||||||
<div class="small activity-message">
|
<div class="small activity-message">
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@detailActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
@detailActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
||||||
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||||
<div class="strong">
|
<div class="strong">
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customActivity(activity: gitbucket.core.model.Activity, image: String)(additionalInfo: Any) = {
|
@customActivity(activity: gitbucket.core.model.Activity, image: String)(additionalInfo: Any) = {
|
||||||
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||||
<div class="strong">
|
<div class="strong">
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
||||||
<div class="activity-icon-small"><img src="@assets/common/images/@image"/></div>
|
<div class="activity-icon-small"><i class="octicon octicon-@image"></i></div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<div>
|
<div>
|
||||||
@avatar(activity.activityUserName, 16)
|
@avatar(activity.activityUserName, 16)
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
@(owner: String, repository: String)(textarea: Html)(implicit context: gitbucket.core.controller.Context)
|
@(owner: String, repository: String)(textarea: Html)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
|
@import gitbucket.core.util.FileUtil
|
||||||
<div class="muted attachable">
|
<div class="muted attachable">
|
||||||
@textarea
|
@textarea
|
||||||
<div class="clickable">Attach images by dragging & dropping, or selecting them.</div>
|
<div class="clickable">Attach images or documents by dragging & dropping, or selecting them.</div>
|
||||||
</div>
|
</div>
|
||||||
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
|
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
try {
|
try {
|
||||||
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
|
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
|
||||||
url: '@path/upload/image/@owner/@repository',
|
url: '@path/upload/file/@owner/@repository',
|
||||||
maxFilesize: 10,
|
maxFilesize: 10,
|
||||||
acceptedFiles: 'image/*',
|
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
|
||||||
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.',
|
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
|
||||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||||
success: function(file, id) {
|
success: function(file, id) {
|
||||||
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
|
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +
|
||||||
$('#@textareaId').val($('#@textareaId').val() + images);
|
'](@baseUrl/@owner/@repository/_attached/' + id + ')';
|
||||||
|
$('#@textareaId').val($('#@textareaId').val() + attachFile);
|
||||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
@helper.html.dropdown(
|
@helper.html.dropdown(
|
||||||
value = if(branch.length == 40) branch.substring(0, 10) else branch,
|
value = if(branch.length == 40) branch.substring(0, 10) else branch,
|
||||||
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree",
|
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree",
|
||||||
mini = true
|
mini = false
|
||||||
) {
|
) {
|
||||||
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">×</button></div></li>
|
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">×</button></div></li>
|
||||||
<li><input id="branch-control-input" type="text" placeholder="Find or create branch ..."/></li>
|
<li><input id="branch-control-input" type="text" placeholder="Find or create branch ..."/></li>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@(condition: => Boolean)
|
@(condition: => Boolean)
|
||||||
@if(condition){
|
@if(condition){
|
||||||
<i class="icon-ok"></i>
|
<i class="octicon octicon-check"></i>
|
||||||
} else {
|
} else {
|
||||||
<i class="icon-white"></i>
|
<i class="icon-white"></i>
|
||||||
}
|
}
|
||||||
@@ -5,16 +5,20 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core._
|
@import gitbucket.core._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
<div class="@if(comment.fileName.isDefined && (!latestCommitId.isDefined || latestCommitId.get == comment.commitId)){inline-comment}" @if(comment.fileName.isDefined){filename=@comment.fileName.get} @if(comment.newLine.isDefined){newline=@comment.newLine.get} @if(comment.oldLine.isDefined){oldline=@comment.oldLine.get}>
|
<div class="@if(comment.fileName.isDefined && (!latestCommitId.isDefined || latestCommitId.get == comment.commitId)){inline-comment}"
|
||||||
|
id="discussion_r@comment.commentId"
|
||||||
|
@if(comment.fileName.isDefined){filename="@comment.fileName.get"}
|
||||||
|
@if(comment.newLine.isDefined){newline="@comment.newLine.get"}
|
||||||
|
@if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}>
|
||||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||||
<div class="box commit-comment-box commit-comment-@comment.commentId">
|
<div class="box commit-comment-box commit-comment-@comment.commentId">
|
||||||
<div class="box-header-small">
|
<div class="box-header">
|
||||||
@user(comment.commentedUserName, styleClass="username strong")
|
@user(comment.commentedUserName, styleClass="username strong")
|
||||||
<span class="muted">
|
<span class="muted">
|
||||||
commented
|
commented
|
||||||
@if(comment.pullRequest){
|
@if(comment.issueId.isDefined){
|
||||||
on this Pull Request
|
on this Pull Request
|
||||||
}else{
|
} else {
|
||||||
@if(comment.fileName.isDefined){
|
@if(comment.fileName.isDefined){
|
||||||
on @comment.fileName.get
|
on @comment.fileName.get
|
||||||
}
|
}
|
||||||
@@ -24,12 +28,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
@if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
|
@if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil"></i></a>
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x"></i></a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content commit-commentContent-@comment.commentId markdown-body">
|
<div class="box-content-bottom issue-content commit-commentContent-@comment.commentId markdown-body">
|
||||||
@markdown(comment.content, repository, false, true, true, hasWritePermission)
|
@markdown(comment.content, repository, false, true, true, hasWritePermission)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(id: String, value: String)(html: Html)
|
@(id: String, value: String, prepend: Boolean = false)(html: Html)
|
||||||
<div class="input-append" style="margin-bottom: 0px;">
|
<div class="input-append @if(prepend){input-prepend}" style="margin-bottom: 0px;">
|
||||||
@html
|
@html
|
||||||
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
|
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// copy to clipboard
|
// copy to clipboard
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@import gitbucket.core.view.helpers
|
@import gitbucket.core.view.helpers
|
||||||
<div id="@name" class="input-append date" data-date-format="yyyy-mm-dd" data-date="@value.map(helpers.date)">
|
<div id="@name" class="input-append date" data-date-format="yyyy-mm-dd" data-date="@value.map(helpers.date)">
|
||||||
<input class="span2" name="@name" type="text" readonly="" value="@value.map(helpers.date)" size="16"/>
|
<input class="span2" name="@name" type="text" readonly="" value="@value.map(helpers.date)" size="16"/>
|
||||||
<span class="add-on"><i class="icon-calendar"></i></span>
|
<span class="add-on"><i class="octicon octicon-calendar"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
|
|||||||
@@ -25,16 +25,16 @@
|
|||||||
<span class="pull-right diffstat" data-diff-id="@i"></span>
|
<span class="pull-right diffstat" data-diff-id="@i"></span>
|
||||||
<a href="#diff-@i">
|
<a href="#diff-@i">
|
||||||
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
||||||
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
|
<i class="octicon octicon-diff-renamed"></i> @diff.oldPath -> @diff.newPath
|
||||||
}
|
}
|
||||||
@if(diff.changeType == ChangeType.ADD){
|
@if(diff.changeType == ChangeType.ADD){
|
||||||
<img src="@assets/common/images/diff_add.png"/> @diff.newPath
|
<i class="octicon octicon-diff-added"></i> @diff.newPath
|
||||||
}
|
}
|
||||||
@if(diff.changeType == ChangeType.MODIFY){
|
@if(diff.changeType == ChangeType.MODIFY){
|
||||||
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath
|
<i class="octicon octicon-diff-modified"></i> @diff.newPath
|
||||||
}
|
}
|
||||||
@if(diff.changeType == ChangeType.DELETE){
|
@if(diff.changeType == ChangeType.DELETE){
|
||||||
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
|
<i class="octicon octicon-diff-removed"></i> @diff.oldPath
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<span class="diffstat">
|
<span class="diffstat">
|
||||||
<img src="@assets/common/images/diff_move.png"/>
|
<i class="octicon octicon-diff-renamed"></i>
|
||||||
</span> @diff.oldPath -> @diff.newPath
|
</span> @diff.oldPath -> @diff.newPath
|
||||||
}
|
}
|
||||||
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
|
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
|
||||||
@@ -68,9 +68,9 @@
|
|||||||
}
|
}
|
||||||
<span class="diffstat">
|
<span class="diffstat">
|
||||||
@if(diff.changeType == ChangeType.ADD){
|
@if(diff.changeType == ChangeType.ADD){
|
||||||
<img src="@assets/common/images/diff_add.png"/>
|
<i class="octicon octicon-diff-added"></i>
|
||||||
}else{
|
}else{
|
||||||
<img src="@assets/common/images/diff_edit.png"/>
|
<i class="octicon octicon-diff-modified"></i>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
@diff.newPath
|
@diff.newPath
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<span class="diffstat">
|
<span class="diffstat">
|
||||||
<img src="@assets/common/images/diff_delete.png"/>
|
<i class="octicon octicon-diff-removed"></i>
|
||||||
</span> @diff.oldPath
|
</span> @diff.oldPath
|
||||||
}
|
}
|
||||||
</th>
|
</th>
|
||||||
@@ -92,30 +92,38 @@
|
|||||||
<td style="padding: 0;">
|
<td style="padding: 0;">
|
||||||
@if(diff.oldObjectId == diff.newObjectId){
|
@if(diff.oldObjectId == diff.newObjectId){
|
||||||
<div class="diff-same">File renamed without changes</div>
|
<div class="diff-same">File renamed without changes</div>
|
||||||
} else { @if(diff.newContent != None || diff.oldContent != None){
|
} else {
|
||||||
|
@if(diff.newContent != None || diff.oldContent != None){
|
||||||
<div id="diffText-@i" class="diffText"></div>
|
<div id="diffText-@i" class="diffText"></div>
|
||||||
<textarea id="newText-@i" style="display: none;" data-file-name="@diff.oldPath">@diff.newContent.getOrElse("")</textarea>
|
<textarea id="newText-@i" style="display: none;" data-file-name="@diff.oldPath">@diff.newContent.getOrElse("")</textarea>
|
||||||
<textarea id="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
|
<textarea id="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
|
||||||
} else { @if(diff.newIsImage || diff.oldIsImage){
|
} else {
|
||||||
<div class="diff-image-render diff2up">
|
@if(diff.newIsImage || diff.oldIsImage){
|
||||||
|
<div class="diff-image-render diff2up">@diff.oldIsImage @diff.newIsImage
|
||||||
@if(oldCommitId.isDefined && diff.oldIsImage){
|
@if(oldCommitId.isDefined && diff.oldIsImage){
|
||||||
<div class="diff-image-frame diff-old"><img src="@url(repository)/blob/@oldCommitId.get/@diff.oldPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
<div class="diff-image-frame diff-old"><img src="@url(repository)/blob/@oldCommitId.get/@diff.oldPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
||||||
} else {
|
} else {
|
||||||
@if(diff.changeType != ChangeType.ADD){
|
@if(diff.changeType != ChangeType.ADD){
|
||||||
Not supported
|
<div style="padding: 12px;">Not supported</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if(newCommitId.isDefined && diff.newIsImage){
|
@if(newCommitId.isDefined && diff.newIsImage){
|
||||||
<div class="diff-image-frame diff-new"><img src="@url(repository)/blob/@newCommitId.get/@diff.newPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
<div class="diff-image-frame diff-new"><img src="@url(repository)/blob/@newCommitId.get/@diff.newPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
||||||
} else {
|
} else {
|
||||||
@if(diff.changeType != ChangeType.DELETE){
|
@if(diff.changeType != ChangeType.DELETE){
|
||||||
Not supported
|
<div style="padding: 12px;">Not supported</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
Not supported
|
@if(diff.tooLarge){
|
||||||
} } }
|
<div style="padding: 12px;">Too large</div>
|
||||||
|
} else {
|
||||||
|
<div style="padding: 12px;">Not supported</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -212,7 +220,6 @@ $(function(){
|
|||||||
dataType : 'html'
|
dataType : 'html'
|
||||||
},
|
},
|
||||||
function(responseContent) {
|
function(responseContent) {
|
||||||
$this.hide();
|
|
||||||
var tmp;
|
var tmp;
|
||||||
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
||||||
if (!isNaN(newLineNumber) && newLineNumber) {
|
if (!isNaN(newLineNumber) && newLineNumber) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
@if(flat){style="border: none; background-color: #eee;"}
|
@if(flat){style="border: none; background-color: #eee;"}
|
||||||
class="dropdown-toggle @if(!flat){btn} else {flat} @if(mini){btn-mini} else {btn-small}" data-toggle="dropdown">
|
class="dropdown-toggle @if(!flat){btn} else {flat} @if(mini){btn-mini} else {btn-small}" data-toggle="dropdown">
|
||||||
@if(value.isEmpty){
|
@if(value.isEmpty){
|
||||||
<i class="icon-cog"></i>
|
<i class="octicon octicon-gear"></i>
|
||||||
} else {
|
} else {
|
||||||
@if(prefix.nonEmpty){
|
@if(prefix.nonEmpty){
|
||||||
<span class="muted">@prefix:</span>
|
<span class="muted">@prefix:</span>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<published>@datetimeRFC3339(activity.activityDate)</published>
|
<published>@datetimeRFC3339(activity.activityDate)</published>
|
||||||
<updated>@datetimeRFC3339(activity.activityDate)</updated>
|
<updated>@datetimeRFC3339(activity.activityDate)</updated>
|
||||||
<link type="text/html" rel="alternate" href="@context.baseUrl/@activity.userName/@activity.repositoryName" />
|
<link type="text/html" rel="alternate" href="@context.baseUrl/@activity.userName/@activity.repositoryName" />
|
||||||
<title type="html">@activity.activityType</title>
|
<title type="html">@removeHtml(activityMessage(activity.message))</title>
|
||||||
<author>
|
<author>
|
||||||
<name>@activity.activityUserName</name>
|
<name>@activity.activityUserName</name>
|
||||||
<uri>@url(activity.activityUserName)</uri>
|
<uri>@url(activity.activityUserName)</uri>
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
enableTaskList: Boolean,
|
enableTaskList: Boolean,
|
||||||
hasWritePermission: Boolean,
|
hasWritePermission: Boolean,
|
||||||
style: String = "",
|
style: String = "",
|
||||||
|
styleClass: String = "",
|
||||||
placeholder: String = "Leave a comment",
|
placeholder: String = "Leave a comment",
|
||||||
elastic: Boolean = false,
|
elastic: Boolean = false,
|
||||||
|
tabIndex: Int = -2,
|
||||||
uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context)
|
uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core._
|
@import gitbucket.core._
|
||||||
@@ -20,7 +22,10 @@
|
|||||||
<div class="tab-pane active" id="tab@uid">
|
<div class="tab-pane active" id="tab@uid">
|
||||||
<span id="error-content" class="error"></span>
|
<span id="error-content" class="error"></span>
|
||||||
@textarea = {
|
@textarea = {
|
||||||
<textarea id="content@uid" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
|
<textarea id="content@uid" name="content" placeholder="@placeholder"
|
||||||
|
@if(tabIndex > -2){ tabindex="@tabIndex"}
|
||||||
|
@if(style.nonEmpty){ style="@style"}
|
||||||
|
@if(styleClass.nonEmpty){ class="@styleClass" }>@content</textarea>
|
||||||
}
|
}
|
||||||
@if(enableWikiLink){
|
@if(enableWikiLink){
|
||||||
@textarea
|
@textarea
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@if(repository.repository.isPrivate){
|
@if(repository.repository.isPrivate){
|
||||||
<img src="@assets/common/images/repo_private@{if(large){"_lg"}}.png"/>
|
<i class="@{if(large){"mega-"}}octicon octicon-lock"></i>
|
||||||
} else {
|
} else {
|
||||||
@if(repository.repository.originUserName.isDefined){
|
@if(repository.repository.originUserName.isDefined){
|
||||||
<img src="@assets/common/images/repo_fork@{if(large){"_lg"}}.png"/>
|
<i class="@{if(large){"mega-"}}octicon octicon-repo-forked"></i>
|
||||||
} else {
|
} else {
|
||||||
<img src="@assets/common/images/repo_public@{if(large){"_lg"}}.png"/>
|
<i class="@{if(large){"mega-"}}octicon octicon-repo"></i>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,57 +24,64 @@
|
|||||||
@if(loginAccount.isEmpty){
|
@if(loginAccount.isEmpty){
|
||||||
@signinform(settings)
|
@signinform(settings)
|
||||||
} else {
|
} else {
|
||||||
<table class="table table-bordered">
|
<div class="box-header">
|
||||||
<tr>
|
|
||||||
<th class="metal">
|
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
|
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
|
||||||
</div>
|
</div>
|
||||||
Your repositories (@userRepositories.size)
|
<span class="strong">Your repositories</span> <span class="label">@userRepositories.size</span>
|
||||||
</th>
|
</div>
|
||||||
</tr>
|
|
||||||
@if(userRepositories.isEmpty){
|
@if(userRepositories.isEmpty){
|
||||||
<tr>
|
<div class="box-content-bottom">
|
||||||
<td>No repositories</td>
|
No repositories
|
||||||
</tr>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
@userRepositories.map { repository =>
|
<div class="box-content-bottom" style="padding: 0px;">
|
||||||
<tr>
|
@defining(20){ max =>
|
||||||
<td>
|
@userRepositories.zipWithIndex.map { case (repository, i) =>
|
||||||
|
<div class="box-content-row repo-link" style="@if(i > max - 1){display:none;}">
|
||||||
@helper.html.repositoryicon(repository, false)
|
@helper.html.repositoryicon(repository, false)
|
||||||
@if(repository.owner == loginAccount.get.userName){
|
@if(repository.owner == loginAccount.get.userName){
|
||||||
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
||||||
} else {
|
} else {
|
||||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||||
}
|
}
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
}
|
||||||
|
@if(userRepositories.size > max){
|
||||||
|
<div class="box-content-row show-more">
|
||||||
|
<a href="javascript:void(0);" id="show-more-repos">Show more @{userRepositories.size - max} pages...</a>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</table>
|
</div>
|
||||||
}
|
}
|
||||||
<table class="table table-bordered">
|
}
|
||||||
<tr>
|
<div class="box-header">
|
||||||
<th class="metal">
|
<span class="strong">Recent updated repositories</span>
|
||||||
Recent updated repositories
|
</div>
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
@if(recentRepositories.isEmpty){
|
@if(recentRepositories.isEmpty){
|
||||||
<tr>
|
<div class="box-content-bottom">
|
||||||
<td>No repositories</td>
|
No repositories
|
||||||
</tr>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
|
<div class="box-content-bottom" style="padding: 0px;">
|
||||||
@recentRepositories.map { repository =>
|
@recentRepositories.map { repository =>
|
||||||
<tr>
|
<div class="box-content-row repo-link">
|
||||||
<td>
|
|
||||||
@helper.html.repositoryicon(repository, false)
|
@helper.html.repositoryicon(repository, false)
|
||||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#show-more-repos').click(function(e){
|
||||||
|
$(e.target).parents('div.box-content-bottom').find('div.repo-link').show();
|
||||||
|
$(e.target).parents('div.show-more').remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -8,17 +8,27 @@
|
|||||||
<hr/><br/>
|
<hr/><br/>
|
||||||
<form method="POST" validate="true">
|
<form method="POST" validate="true">
|
||||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||||
<div class="box issue-comment-box">
|
<div class="issue-comment-box">
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
|
@helper.html.preview(
|
||||||
</div>
|
repository = repository,
|
||||||
</div>
|
content = "",
|
||||||
<div class="pull-right">
|
enableWikiLink = false,
|
||||||
|
enableRefsLink = true,
|
||||||
|
enableTaskList = true,
|
||||||
|
hasWritePermission = hasWritePermission,
|
||||||
|
style = "",
|
||||||
|
elastic = true,
|
||||||
|
tabIndex = 1
|
||||||
|
)
|
||||||
|
<div style="text-align: right;">
|
||||||
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
||||||
<input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
|
|
||||||
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
|
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
|
||||||
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
|
<input type="submit" class="btn" tabindex="3" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
|
||||||
}
|
}
|
||||||
|
<input type="submit" class="btn btn-success" tabindex="2" formaction="@url(repository)/issue_comments/new" value="Comment"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,16 @@
|
|||||||
@import gitbucket.core.model.CommitComment
|
@import gitbucket.core.model.CommitComment
|
||||||
@if(issue.isDefined){
|
@if(issue.isDefined){
|
||||||
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
|
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
|
||||||
<div class="box issue-comment-box">
|
<div class="issue-comment-box">
|
||||||
<div class="box-header-small">
|
<div class="box-header">
|
||||||
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
|
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
|
@if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
|
||||||
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil" aria-label="Edit"></i></a>
|
<a href="#" data-issue-id="@issue.get.issueId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content issue-content markdown-body" id="issueContent">
|
<div class="box-content-bottom issue-content markdown-body" id="issueContent">
|
||||||
@markdown(issue.get.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
|
@markdown(issue.get.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
|
@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">
|
||||||
@user(comment.commentedUserName, styleClass="username strong")
|
@user(comment.commentedUserName, styleClass="username strong")
|
||||||
<span class="muted">
|
<span class="muted">
|
||||||
@if(comment.action == "comment"){
|
@if(comment.action == "comment"){
|
||||||
@@ -41,24 +41,24 @@
|
|||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
||||||
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil" aria-label="Edit"></i></a>
|
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle" aria-label="Remove"></i></a>
|
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
|
<div class="box-content-bottom issue-content markdown-body" id="commentContent-@comment.commentId">
|
||||||
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
|
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
|
||||||
@defining(comment.content.substring(comment.content.length - 40)){ id =>
|
@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>
|
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
|
||||||
<div class="markdown-body">@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission)</div>
|
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@if(comment.action == "refer"){
|
@if(comment.action == "refer"){
|
||||||
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
|
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
|
||||||
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
|
<strong>@issueLink(repository, issueId.toInt): @rest.mkString(":")</strong>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
<div class="markdown-body">@markdown(comment.content, repository, false, true, true, hasWritePermission)</div>
|
@markdown(comment.content, repository, false, true, true, hasWritePermission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -78,8 +78,8 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if(comment.action == "close" || comment.action == "close_comment"){
|
@if(comment.action == "close" || comment.action == "close_comment"){
|
||||||
<div class="small issue-comment-action">
|
<div class="issue-comment-action">
|
||||||
<span class="label label-important">Closed</span>
|
<i class="octicon octicon-circle-slash danger"></i>
|
||||||
@avatar(comment.commentedUserName, 20)
|
@avatar(comment.commentedUserName, 20)
|
||||||
@if(issue.isDefined && issue.get.isPullRequest){
|
@if(issue.isDefined && issue.get.isPullRequest){
|
||||||
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
|
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
|
||||||
@@ -89,14 +89,14 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
|
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
|
||||||
<div class="small issue-comment-action">
|
<div class="issue-comment-action issue-reopened">
|
||||||
<span class="label label-success">Reopened</span>
|
<i class="octicon octicon-primitive-dot"></i>
|
||||||
@avatar(comment.commentedUserName, 20)
|
@avatar(comment.commentedUserName, 20)
|
||||||
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
|
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if(comment.action == "delete_branch"){
|
@if(comment.action == "delete_branch"){
|
||||||
<div class="small issue-comment-action">
|
<div class="issue-comment-action">
|
||||||
<span class="label">Deleted</span>
|
<span class="label">Deleted</span>
|
||||||
@avatar(comment.commentedUserName, 20)
|
@avatar(comment.commentedUserName, 20)
|
||||||
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
|
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
@if(issue.isDefined){
|
@if(issue.isDefined){
|
||||||
$('.issue-comment-box i.icon-pencil').click(function(){
|
$('.issue-comment-box i.octicon-pencil').click(function(){
|
||||||
var id = $(this).closest('a').data('comment-id');
|
var id = $(this).closest('a').data('comment-id');
|
||||||
var url = '@url(repository)/issue_comments/_data/' + id;
|
var url = '@url(repository)/issue_comments/_data/' + id;
|
||||||
var $content = $('#commentContent-' + id);
|
var $content = $('#commentContent-' + id);
|
||||||
@@ -130,7 +130,7 @@ $(function(){
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
$('.issue-comment-box i.icon-remove-circle').click(function(){
|
$('.issue-comment-box i.octicon-x').click(function(){
|
||||||
if(confirm('Are you sure you want to delete this?')) {
|
if(confirm('Are you sure you want to delete this?')) {
|
||||||
var id = $(this).closest('a').data('comment-id');
|
var id = $(this).closest('a').data('comment-id');
|
||||||
$.post('@url(repository)/issue_comments/delete/' + id,
|
$.post('@url(repository)/issue_comments/delete/' + id,
|
||||||
@@ -144,7 +144,7 @@ $(function(){
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$(document).on('click', '.commit-comment-box i.icon-pencil', function(){
|
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
|
||||||
var id = $(this).closest('a').data('comment-id');
|
var id = $(this).closest('a').data('comment-id');
|
||||||
var url = '@url(repository)/commit_comments/_data/' + id;
|
var url = '@url(repository)/commit_comments/_data/' + id;
|
||||||
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
|
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
|
||||||
@@ -158,7 +158,7 @@ $(function(){
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
$(document).on('click', '.commit-comment-box i.icon-remove-circle', function(){
|
$(document).on('click', '.commit-comment-box i.octicon-x', function(){
|
||||||
if(confirm('Are you sure you want to delete this?')) {
|
if(confirm('Are you sure you want to delete this?')) {
|
||||||
var id = $(this).closest('a').data('comment-id');
|
var id = $(this).closest('a').data('comment-id');
|
||||||
$.post('@url(repository)/commit_comments/delete/' + id,
|
$.post('@url(repository)/commit_comments/delete/' + id,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user