mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 01:37:34 +02:00
Compare commits
222 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 | ||
|
|
5b658ef6ff | ||
|
|
e9ff24d9a7 | ||
|
|
a92051a4c3 | ||
|
|
70c386a934 | ||
|
|
08eb21844a | ||
|
|
7b37d6b571 | ||
|
|
f52bd2bcc0 |
@@ -1,5 +1,6 @@
|
||||
language: scala
|
||||
sudo: false
|
||||
script:
|
||||
- . env.sh
|
||||
- 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.
|
||||
77
README.md
77
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
|
||||
--------
|
||||
@@ -14,29 +13,22 @@ The current version of GitBucket provides a basic features below:
|
||||
- Wiki
|
||||
- Issues
|
||||
- Fork / Pull request
|
||||
- Mail notification
|
||||
- Email notification
|
||||
- Activity timeline
|
||||
- User management (for Administrators)
|
||||
- Group (like Organization in Github)
|
||||
- LDAP integration
|
||||
- Simple user and group management with LDAP integration
|
||||
- Gravatar support
|
||||
- Plug-in system
|
||||
|
||||
Following features are not implemented, but we will make them in the future release!
|
||||
|
||||
- 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).
|
||||
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/gitbucket/gitbucket/wiki).
|
||||
|
||||
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.
|
||||
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**.
|
||||
|
||||
@@ -47,9 +39,9 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
|
||||
- --host=[HOSTNAME]
|
||||
- --gitbucket.home=[DATA_DIR]
|
||||
|
||||
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
|
||||
To upgrade GitBucket, only replace gitbucket.war 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
|
||||
#### Installing Via Homebrew
|
||||
@@ -72,20 +64,65 @@ Or, if you don't want/need launchctl, you can just run:
|
||||
```
|
||||
|
||||
#### 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
|
||||
|
||||
- start gitbucket: `launchctl load ~/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
|
||||
--------
|
||||
### 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/takezoe/gitbucket-gist-plugin)
|
||||
- A lot of updates for [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
|
||||
|
||||
### 3.4 - 27 Jun 2015
|
||||
- Declarative style plug-in definition
|
||||
|
||||
@@ -44,7 +44,7 @@ GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
|
||||
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
|
||||
|
||||
# GitBucket version to fetch when installing
|
||||
GITBUCKET_VERSION=2.1
|
||||
GITBUCKET_VERSION=3.5
|
||||
|
||||
#
|
||||
# End of configuration section. Ignore this part
|
||||
|
||||
@@ -38,7 +38,7 @@ createDir "$GITBUCKET_DIR"
|
||||
createDir "$GITBUCKET_LOG_DIR"
|
||||
|
||||
echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
|
||||
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
|
||||
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/gitbucket/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
|
||||
|
||||
sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Summary: GitHub clone written with Scala.
|
||||
Version: 2.6
|
||||
Release: 1%{?dist}
|
||||
License: Apache
|
||||
URL: https://github.com/takezoe/gitbucket
|
||||
URL: https://github.com/gitbucket/gitbucket
|
||||
Group: System/Servers
|
||||
Source0: %{name}.war
|
||||
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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
3
env.sh
3
env.sh
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
export GITBUCKET_VERSION=`cat project/build.scala | grep 'val Version' | cut -d \" -f 2`
|
||||
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"
|
||||
@@ -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 {
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "3.5.0"
|
||||
val Version = "3.9.0"
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
|
||||
@@ -38,7 +38,8 @@ object MyBuild extends Build {
|
||||
scalaVersion := ScalaVersion,
|
||||
resolvers ++= Seq(
|
||||
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"),
|
||||
libraryDependencies ++= Seq(
|
||||
@@ -48,13 +49,14 @@ object MyBuild extends Build {
|
||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"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",
|
||||
"org.pegdown" % "pegdown" % "1.5.0",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.5",
|
||||
"org.apache.commons" % "commons-compress" % "1.9",
|
||||
"org.apache.commons" % "commons-email" % "1.3.3",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.3.6",
|
||||
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||
"org.apache.tika" % "tika-core" % "1.10",
|
||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.4.180",
|
||||
|
||||
@@ -55,7 +55,12 @@
|
||||
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
. ../env.sh
|
||||
. ./env.sh
|
||||
|
||||
cd ../
|
||||
./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"
|
||||
@@ -3,13 +3,13 @@ D="$(dirname "$0")"
|
||||
D="$(cd "${D}"; pwd)"
|
||||
DD="$(dirname "${D}")"
|
||||
(
|
||||
for f in "${DD}/env.sh" "${D}/build.xml"; do
|
||||
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}"
|
||||
. "${DD}/env.sh"
|
||||
ant -f "${D}/build.xml" all
|
||||
)
|
||||
|
||||
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
||||
set SCRIPT_DIR=%~dp0
|
||||
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -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" %*
|
||||
|
||||
2
sbt.sh
2
sbt.sh
@@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
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 "$@"
|
||||
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 "$@"
|
||||
|
||||
@@ -128,7 +128,7 @@ INSERT INTO ACCOUNT (
|
||||
'root@localhost',
|
||||
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
||||
true,
|
||||
'https://github.com/takezoe/gitbucket',
|
||||
'https://github.com/gitbucket/gitbucket',
|
||||
SYSDATE,
|
||||
SYSDATE,
|
||||
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 UserManagementController, "/*")
|
||||
context.mount(new SystemSettingsController, "/*")
|
||||
context.mount(new PluginsController, "/*")
|
||||
context.mount(new AccountController, "/*")
|
||||
context.mount(new RepositoryViewerController, "/*")
|
||||
context.mount(new WikiController, "/*")
|
||||
|
||||
@@ -14,16 +14,16 @@ case class ApiComment(
|
||||
user: ApiUser,
|
||||
body: String,
|
||||
created_at: Date,
|
||||
updated_at: Date)(repositoryName: RepositoryName, issueId: Int){
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${issueId}#comment-${id}")
|
||||
updated_at: Date)(repositoryName: RepositoryName, issueId: Int, isPullRequest: Boolean){
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${issueId}#comment-${id}")
|
||||
}
|
||||
|
||||
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(
|
||||
id = comment.commentId,
|
||||
user = user,
|
||||
body = comment.content,
|
||||
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],
|
||||
modified: List[String],
|
||||
author: ApiPersonIdent,
|
||||
committer: ApiPersonIdent)(repositoryName:RepositoryName){
|
||||
val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}")
|
||||
committer: ApiPersonIdent)(repositoryName:RepositoryName, urlIsHtmlUrl: Boolean) extends FieldSerializable{
|
||||
val url = if(urlIsHtmlUrl){
|
||||
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{
|
||||
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)
|
||||
ApiCommit(
|
||||
id = commit.id,
|
||||
@@ -43,6 +51,7 @@ object ApiCommit{
|
||||
},
|
||||
author = ApiPersonIdent.author(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,
|
||||
created_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 html_url = ApiPath(s"/${repositoryName.fullName}/issues/${number}")
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
|
||||
}
|
||||
|
||||
object ApiIssue{
|
||||
@@ -31,5 +31,5 @@ object ApiIssue{
|
||||
state = if(issue.closed){ "closed" }else{ "open" },
|
||||
body = issue.content.getOrElse(""),
|
||||
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,
|
||||
`private`: Boolean,
|
||||
default_branch: String,
|
||||
owner: ApiUser) {
|
||||
owner: ApiUser)(urlIsHtmlUrl: Boolean) {
|
||||
val forks_count = forks
|
||||
val watchers_coun = watchers
|
||||
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
||||
val watchers_count = watchers
|
||||
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 clone_url = ApiPath(s"/git/${full_name}.git")
|
||||
val html_url = ApiPath(s"/${full_name}")
|
||||
@@ -27,7 +31,8 @@ object ApiRepository{
|
||||
repository: Repository,
|
||||
owner: ApiUser,
|
||||
forkedCount: Int =0,
|
||||
watchers: Int = 0): ApiRepository =
|
||||
watchers: Int = 0,
|
||||
urlIsHtmlUrl: Boolean = false): ApiRepository =
|
||||
ApiRepository(
|
||||
name = repository.repositoryName,
|
||||
full_name = s"${repository.userName}/${repository.repositoryName}",
|
||||
@@ -37,7 +42,7 @@ object ApiRepository{
|
||||
`private` = repository.isPrivate,
|
||||
default_branch = repository.defaultBranch,
|
||||
owner = owner
|
||||
)
|
||||
)(urlIsHtmlUrl)
|
||||
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
|
||||
@@ -45,4 +50,7 @@ object ApiRepository{
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
|
||||
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[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]()
|
||||
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
// removeUserRelatedData(userName)
|
||||
|
||||
removeUserRelatedData(userName)
|
||||
updateAccount(account.copy(isRemoved = true))
|
||||
}
|
||||
|
||||
@@ -366,56 +367,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||
LockUtil.lock(s"${form.owner}/${form.name}"){
|
||||
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
|
||||
val ownerAccount = getAccountByUserName(form.owner).get
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
|
||||
// Insert to the database at first
|
||||
createRepository(form.name, form.owner, form.description, form.isPrivate)
|
||||
|
||||
// Add collaborators for group repository
|
||||
if(ownerAccount.isGroupAccount){
|
||||
getGroupMembers(form.owner).foreach { member =>
|
||||
addCollaborator(form.owner, form.name, member.userName)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(form.owner, form.name)
|
||||
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(form.owner, form.name)
|
||||
JGitUtil.initRepository(gitdir)
|
||||
|
||||
if(form.createReadme){
|
||||
using(Git.open(gitdir)){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
val content = if(form.description.nonEmpty){
|
||||
form.name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
form.description.get
|
||||
} else {
|
||||
form.name + "\n" +
|
||||
"===============\n"
|
||||
}
|
||||
|
||||
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||
}
|
||||
}
|
||||
|
||||
// Create Wiki repository
|
||||
createWikiRepository(loginAccount, form.owner, form.name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
|
||||
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||
}
|
||||
|
||||
// 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 =>
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
@@ -496,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 = {
|
||||
createLabel(userName, repositoryName, "bug", "fc2929")
|
||||
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 baseUrl = settings.baseUrl(request)
|
||||
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.
|
||||
|
||||
@@ -17,22 +17,22 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||
|
||||
post("/image"){
|
||||
execute { (file, fileId) =>
|
||||
execute({ (file, fileId) =>
|
||||
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
|
||||
session += Keys.Session.Upload(fileId) -> file.name
|
||||
}
|
||||
}, FileUtil.isImage)
|
||||
}
|
||||
|
||||
post("/image/:owner/:repository"){
|
||||
execute { (file, fileId) =>
|
||||
post("/file/:owner/:repository"){
|
||||
execute({ (file, fileId) =>
|
||||
FileUtils.writeByteArrayToFile(new java.io.File(
|
||||
getAttachedDir(params("owner"), params("repository")),
|
||||
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
|
||||
}
|
||||
}, FileUtil.isUploadableType)
|
||||
}
|
||||
|
||||
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
|
||||
case Some(file) if(FileUtil.isImage(file.name)) =>
|
||||
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
|
||||
case Some(file) if(mimeTypeChcker(file.name)) =>
|
||||
defining(FileUtil.generateFileId){ 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 {
|
||||
getAccountByUserName(params("userName")).isDefined
|
||||
|
||||
@@ -86,7 +86,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
issueId <- params("id").toIntOpt
|
||||
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
||||
} 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)
|
||||
})
|
||||
|
||||
@@ -190,7 +190,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
||||
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
||||
} 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
|
||||
})
|
||||
|
||||
@@ -233,7 +233,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("title" -> x.title,
|
||||
"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
|
||||
@@ -257,6 +257,12 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
} 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 =>
|
||||
defining(params("id").toInt){ issueId =>
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||
@@ -326,6 +332,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
||||
case dir if(dir.exists && dir.isDirectory) =>
|
||||
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
||||
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
|
||||
RawData(FileUtil.getMimeType(file.getName), file)
|
||||
}
|
||||
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) = {
|
||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
||||
@@ -459,7 +467,11 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
"issues",
|
||||
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||
page,
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted
|
||||
} else {
|
||||
getCollaborators(owner, repoName)
|
||||
},
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(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))),
|
||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||
"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)
|
||||
|
||||
val mergeForm = mapping(
|
||||
@@ -62,7 +65,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
requestRepositoryName: String,
|
||||
requestBranch: String,
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String)
|
||||
commitIdTo: String,
|
||||
assignedUserName: Option[String],
|
||||
milestoneId: Option[Int],
|
||||
labelNames: Option[String]
|
||||
)
|
||||
|
||||
case class MergeForm(message: String)
|
||||
|
||||
@@ -176,7 +183,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
pullreq,
|
||||
statuses,
|
||||
repository,
|
||||
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
@@ -232,6 +239,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}
|
||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||
}
|
||||
|
||||
updatePullRequests(owner, name, pullreq.branch)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
@@ -284,6 +294,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
originRepositoryName <- if(originOwner == forkedOwner) {
|
||||
// Self repository
|
||||
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){
|
||||
// Original repository
|
||||
forkedRepository.repository.originRepositoryName
|
||||
@@ -307,32 +320,44 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
originRepository.owner, originRepository.name, originId,
|
||||
forkedRepository.owner, forkedRepository.name, forkedId)
|
||||
|
||||
(oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId))
|
||||
(Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||
} else {
|
||||
// Commit id
|
||||
(oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId))
|
||||
(Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||
}
|
||||
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner, originRepository.name, oldId.getName,
|
||||
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||
(oldId, newId) match {
|
||||
case (Some(oldId), Some(newId)) => {
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner, originRepository.name, oldId.getName,
|
||||
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||
|
||||
html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
},
|
||||
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||
originId,
|
||||
forkedId,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
|
||||
html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
},
|
||||
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||
originId,
|
||||
forkedId,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
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
|
||||
})
|
||||
@@ -368,47 +393,78 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||
val loginUserName = context.loginAccount.get.userName
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
val writable = hasWritePermission(owner, name, context.loginAccount)
|
||||
val loginUserName = context.loginAccount.get.userName
|
||||
|
||||
val issueId = createIssue(
|
||||
owner = repository.owner,
|
||||
repository = repository.name,
|
||||
loginUser = loginUserName,
|
||||
title = form.title,
|
||||
content = form.content,
|
||||
assignedUserName = None,
|
||||
milestoneId = None,
|
||||
isPullRequest = true)
|
||||
val issueId = createIssue(
|
||||
owner = repository.owner,
|
||||
repository = repository.name,
|
||||
loginUser = loginUserName,
|
||||
title = form.title,
|
||||
content = form.content,
|
||||
assignedUserName = if(writable) form.assignedUserName else None,
|
||||
milestoneId = if(writable) form.milestoneId else None,
|
||||
isPullRequest = true)
|
||||
|
||||
createPullRequest(
|
||||
originUserName = repository.owner,
|
||||
originRepositoryName = repository.name,
|
||||
issueId = issueId,
|
||||
originBranch = form.targetBranch,
|
||||
requestUserName = form.requestUserName,
|
||||
requestRepositoryName = form.requestRepositoryName,
|
||||
requestBranch = form.requestBranch,
|
||||
commitIdFrom = form.commitIdFrom,
|
||||
commitIdTo = form.commitIdTo)
|
||||
createPullRequest(
|
||||
originUserName = repository.owner,
|
||||
originRepositoryName = repository.name,
|
||||
issueId = issueId,
|
||||
originBranch = form.targetBranch,
|
||||
requestUserName = form.requestUserName,
|
||||
requestRepositoryName = form.requestRepositoryName,
|
||||
requestBranch = form.requestBranch,
|
||||
commitIdFrom = form.commitIdFrom,
|
||||
commitIdTo = form.commitIdTo)
|
||||
|
||||
// fetch requested branch
|
||||
fetchAsPullRequest(repository.owner, repository.name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
|
||||
// fetch requested branch
|
||||
fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
// record activity
|
||||
recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
|
||||
|
||||
// notifications
|
||||
getIssue(repository.owner, repository.name, issueId.toString) foreach { issue =>
|
||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
// call web hook
|
||||
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
|
||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||
@@ -459,7 +515,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
"pulls",
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
page,
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted
|
||||
} else {
|
||||
getCollaborators(owner, repoName)
|
||||
},
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(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.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Constants
|
||||
import scala.util.{Success, Failure}
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
|
||||
|
||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
@@ -42,10 +44,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
)(CollaboratorForm.apply)
|
||||
|
||||
// for web hook url addition
|
||||
case class WebHookForm(url: String)
|
||||
case class WebHookForm(url: String, events: Set[WebHook.Event])
|
||||
|
||||
val webHookForm = mapping(
|
||||
"url" -> trim(label("url", text(required, webHook)))
|
||||
def webHookForm(update:Boolean) = mapping(
|
||||
"url" -> trim(label("url", text(required, webHook(update)))),
|
||||
"events" -> webhookEvents
|
||||
)(WebHookForm.apply)
|
||||
|
||||
// for transfer ownership
|
||||
@@ -138,14 +141,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the web hook page.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
|
||||
addWebHookURL(repository.owner, repository.name, form.url)
|
||||
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
|
||||
addWebHook(repository.owner, repository.name, form.url, form.events)
|
||||
flash += "info" -> s"Webhook ${form.url} created"
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
|
||||
@@ -153,30 +165,77 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Delete the web hook URL.
|
||||
*/
|
||||
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")
|
||||
})
|
||||
|
||||
/**
|
||||
* 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 =>
|
||||
import scala.collection.JavaConverters._
|
||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
||||
.setMaxCount(3)
|
||||
.call.iterator.asScala.map(new CommitInfo(_))
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent._
|
||||
import scala.util.control.NonFatal
|
||||
import org.apache.http.util.EntityUtils
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
getAccountByUserName(repository.owner).foreach { ownerAccount =>
|
||||
callWebHook("push",
|
||||
List(WebHook(repository.owner, repository.name, form.url)),
|
||||
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
|
||||
)
|
||||
val url = params("url")
|
||||
val dummyPayload = {
|
||||
val ownerAccount = getAccountByUserName(repository.owner).get
|
||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
||||
.setMaxCount(4)
|
||||
.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()))
|
||||
}
|
||||
flash += "url" -> form.url
|
||||
flash += "info" -> "Test payload deployed!"
|
||||
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url)
|
||||
|
||||
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
|
||||
|
||||
def headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map{ h => Array(h.getName, h.getValue) }
|
||||
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))
|
||||
}
|
||||
contentType = formats("json")
|
||||
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")
|
||||
})
|
||||
|
||||
@@ -226,9 +285,30 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
/**
|
||||
* 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] =
|
||||
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.Implicits._
|
||||
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.WebHookService._
|
||||
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.archive.{TgzFormat, ZipFormat}
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import org.eclipse.jgit.errors.MissingObjectException
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import org.eclipse.jgit.treewalk._
|
||||
@@ -31,7 +32,7 @@ import org.scalatra._
|
||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||
with WebHookPullRequestService
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService
|
||||
|
||||
/**
|
||||
* The repository viewer.
|
||||
@@ -39,7 +40,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||
with WebHookPullRequestService =>
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService =>
|
||||
|
||||
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||
@@ -249,7 +250,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
)
|
||||
|
||||
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}/${
|
||||
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 =>
|
||||
if(raw){
|
||||
// Download
|
||||
JGitUtil.getContentFromId(git, objectId, true).map { bytes =>
|
||||
RawData("application/octet-stream", bytes)
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
//RawData("application/octet-stream", bytes)
|
||||
contentType = "application/octet-stream"
|
||||
response.setContentLength(loader.getSize.toInt)
|
||||
loader.copyTo(response.getOutputStream)
|
||||
()
|
||||
} getOrElse NotFound
|
||||
} else {
|
||||
html.blob(id, repository, path.split("/").toList,
|
||||
@@ -344,23 +349,28 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
||||
val id = params("id")
|
||||
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
|
||||
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
|
||||
html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||
getCommitComments(repository.owner, repository.name, id, false),
|
||||
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
try {
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit =>
|
||||
JGitUtil.getDiffs(git, id) match {
|
||||
case (diffs, oldCommitId) =>
|
||||
html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||
getCommitComments(repository.owner, repository.name, id, false),
|
||||
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e:MissingObjectException => NotFound
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
val id = params("id")
|
||||
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 {
|
||||
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)
|
||||
@@ -385,13 +395,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
val id = params("id")
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||
helper.html.commitcomment(comment, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
|
||||
@@ -517,10 +529,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
/**
|
||||
* 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 =>
|
||||
JGitUtil.getTreeId(git, params("ref")).map{ treeId =>
|
||||
html.find(params("ref"),
|
||||
val ref = multiParams("splat").head
|
||||
JGitUtil.getTreeId(git, ref).map{ treeId =>
|
||||
html.find(ref,
|
||||
treeId,
|
||||
repository,
|
||||
context.loginAccount match {
|
||||
@@ -656,9 +669,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
// call web hook
|
||||
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
|
||||
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 =>
|
||||
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()))),
|
||||
"ssh" -> trim(label("SSH access", boolean())),
|
||||
"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))),
|
||||
"port" -> trim(label("SMTP Port", optional(number()))),
|
||||
"user" -> trim(label("SMTP User", optional(text()))),
|
||||
|
||||
@@ -100,12 +100,12 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
deleteRepository(userName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
}
|
||||
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
// deleteRepository(userName, repositoryName)
|
||||
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
// }
|
||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
removeUserRelatedData(userName)
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ trait CommitCommentComponent extends TemplateComponent { self: Profile =>
|
||||
val newLine = column[Option[Int]]("NEW_LINE_NUMBER")
|
||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
val pullRequest = column[Boolean]("PULL_REQUEST")
|
||||
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply)
|
||||
val issueId = column[Option[Int]]("ISSUE_ID")
|
||||
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
|
||||
}
|
||||
@@ -74,5 +74,5 @@ case class CommitComment(
|
||||
newLine: Option[Int],
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date,
|
||||
pullRequest: Boolean
|
||||
issueId: Option[Int]
|
||||
) extends Comment
|
||||
|
||||
@@ -48,6 +48,7 @@ trait CoreProfile extends ProfileProvider with Profile
|
||||
with RepositoryComponent
|
||||
with SshKeyComponent
|
||||
with WebHookComponent
|
||||
with WebHookEventComponent
|
||||
with PluginComponent
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
@@ -18,3 +18,32 @@ case class WebHook(
|
||||
repositoryName: 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
|
||||
)
|
||||
@@ -13,9 +13,9 @@ import StringUtil._
|
||||
|
||||
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 {
|
||||
t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest)
|
||||
t => t.byCommit(owner, repository, commitId) && (t.issueId.isEmpty || includePullRequest)
|
||||
} list
|
||||
|
||||
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||
@@ -27,7 +27,8 @@ trait CommitsService {
|
||||
None
|
||||
|
||||
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(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
@@ -39,7 +40,7 @@ trait CommitsService {
|
||||
newLine = newLine,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
pullRequest = pullRequest)
|
||||
issueId = issueId)
|
||||
|
||||
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
|
||||
CommitComments
|
||||
|
||||
@@ -22,11 +22,13 @@ trait IssuesService {
|
||||
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||
IssueComments filter (_.byIssue(owner, repository, issueId)) list
|
||||
|
||||
/** @return IssueComment and commentedUser */
|
||||
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account)] =
|
||||
/** @return IssueComment and commentedUser and Issue */
|
||||
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account, Issue)] =
|
||||
IssueComments.filter(_.byIssue(owner, repository, issueId))
|
||||
.filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment"))
|
||||
.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
|
||||
|
||||
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] ={
|
||||
if(issueList.isEmpty){
|
||||
Map.empty
|
||||
}else{
|
||||
} else {
|
||||
import scala.slick.jdbc._
|
||||
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)]] {
|
||||
@@ -474,9 +476,11 @@ object IssuesService {
|
||||
* Restores IssueSearchCondition instance from filter query.
|
||||
*/
|
||||
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
|
||||
val conditions = filter.split("[ \t]+").map { x =>
|
||||
val dim = x.split(":")
|
||||
dim(0) -> dim(1)
|
||||
val conditions = filter.split("[ \t]+").flatMap { x =>
|
||||
x.split(":") match {
|
||||
case Array(key, value) => Some((key, value))
|
||||
case _ => None
|
||||
}
|
||||
}.groupBy(_._1).map { case (key, values) =>
|
||||
key -> values.map(_._2).toSeq
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ trait RepositoryService { self: AccountService =>
|
||||
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
|
||||
|
||||
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 issueId = IssueId .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)
|
||||
}.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
|
||||
// and it can't be changed by deleting-and-inserting record.
|
||||
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
|
||||
@@ -79,9 +76,10 @@ trait RepositoryService { self: AccountService =>
|
||||
|
||||
deleteRepository(oldUserName, oldRepositoryName)
|
||||
|
||||
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
||||
WebHooks .insertAll(webHooks .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)) :_*)
|
||||
|
||||
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
|
||||
Issues.insertAll(issues.map { x => x.copy(
|
||||
@@ -98,6 +96,11 @@ trait RepositoryService { self: AccountService =>
|
||||
CommitComments.insertAll(commitComments.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
|
||||
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
|
||||
@@ -145,6 +148,7 @@ trait RepositoryService { self: AccountService =>
|
||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||
WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||
|
||||
// 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 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.
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,8 @@ trait SystemSettingsService {
|
||||
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
|
||||
props.setProperty(Ssh, settings.ssh.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 =>
|
||||
props.setProperty(SmtpHost, smtp.host)
|
||||
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
||||
@@ -75,7 +76,8 @@ trait SystemSettingsService {
|
||||
getOptionValue[Int](props, ActivityLogLimit, None),
|
||||
getValue(props, Ssh, false),
|
||||
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(
|
||||
getValue(props, SmtpHost, ""),
|
||||
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
||||
@@ -125,6 +127,7 @@ object SystemSettingsService {
|
||||
activityLogLimit: Option[Int],
|
||||
ssh: Boolean,
|
||||
sshPort: Option[Int],
|
||||
useSMTP: Boolean,
|
||||
smtp: Option[Smtp],
|
||||
ldapAuthentication: Boolean,
|
||||
ldap: Option[Ldap]){
|
||||
@@ -172,6 +175,7 @@ object SystemSettingsService {
|
||||
private val ActivityLogLimit = "activity_log_limit"
|
||||
private val Ssh = "ssh"
|
||||
private val SshPort = "ssh.port"
|
||||
private val UseSMTP = "useSMTP"
|
||||
private val SmtpHost = "smtp.host"
|
||||
private val SmtpPort = "smtp.port"
|
||||
private val SmtpUser = "smtp.user"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
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 profile.simple._
|
||||
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.message.BasicNameValuePair
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import org.slf4j.LoggerFactory
|
||||
import scala.concurrent._
|
||||
import org.apache.http.HttpRequest
|
||||
import org.apache.http.HttpResponse
|
||||
|
||||
|
||||
trait WebHookService {
|
||||
@@ -20,46 +24,91 @@ trait WebHookService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
|
||||
|
||||
def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] =
|
||||
WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list
|
||||
/** get All WebHook informations of repository */
|
||||
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)
|
||||
|
||||
def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
|
||||
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 = {
|
||||
val webHookURLs = getWebHookURLs(owner, repository)
|
||||
if(webHookURLs.nonEmpty){
|
||||
makePayload.map(callWebHook(eventName, webHookURLs, _))
|
||||
events.toSet.map{ event: WebHook.Event =>
|
||||
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||
}
|
||||
}
|
||||
|
||||
def callWebHook(eventName: String, webHookURLs: List[WebHook], payload: WebHookPayload)(implicit c: JsonFormat.Context): Unit = {
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
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
|
||||
|
||||
def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload])(implicit s: Session, c: JsonFormat.Context): Unit = {
|
||||
val webHooks = getWebHooksByEvent(owner, repository, event)
|
||||
if(webHooks.nonEmpty){
|
||||
makePayload.map(callWebHook(event, webHooks, _))
|
||||
}
|
||||
}
|
||||
|
||||
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.impl.client.HttpClientBuilder
|
||||
import scala.concurrent._
|
||||
import ExecutionContext.Implicits.global
|
||||
import org.apache.http.protocol.HttpContext
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
|
||||
if(webHookURLs.nonEmpty){
|
||||
val json = JsonFormat(payload)
|
||||
val httpClient = HttpClientBuilder.create.build
|
||||
|
||||
webHookURLs.foreach { webHookUrl =>
|
||||
webHookURLs.map { webHookUrl =>
|
||||
val reqPromise = Promise[HttpRequest]
|
||||
val f = Future {
|
||||
logger.debug(s"start web hook invocation for ${webHookUrl}")
|
||||
val httpPost = new HttpPost(webHookUrl.url)
|
||||
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
httpPost.addHeader("X-Github-Event", eventName)
|
||||
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)
|
||||
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
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()
|
||||
params.add(new BasicNameValuePair("payload", json))
|
||||
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
||||
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||
params.add(new BasicNameValuePair("payload", json))
|
||||
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
||||
|
||||
httpClient.execute(httpPost)
|
||||
httpPost.releaseConnection()
|
||||
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
||||
val res = httpClient.execute(httpPost)
|
||||
httpPost.releaseConnection()
|
||||
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
||||
res
|
||||
}catch{
|
||||
case e:Throwable => {
|
||||
if(!reqPromise.isCompleted){
|
||||
reqPromise.failure(e)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
f.onSuccess {
|
||||
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
|
||||
@@ -67,9 +116,12 @@ trait WebHookService {
|
||||
f.onFailure {
|
||||
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._
|
||||
// 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 = {
|
||||
callWebHookOf(repository.owner, repository.name, "issues"){
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.Issues){
|
||||
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
|
||||
for{
|
||||
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 = {
|
||||
import WebHookService._
|
||||
callWebHookOf(repository.owner, repository.name, "pull_request"){
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){
|
||||
for{
|
||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||
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
|
||||
iu <- Accounts if iu.userName === is.openedUserName
|
||||
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
|
||||
wht <- WebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byWebHook(wh)
|
||||
} yield {
|
||||
((is, iu, pr, bu, ru), wh)
|
||||
}).list.groupBy(_._1).mapValues(_.map(_._2))
|
||||
@@ -154,7 +207,36 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
baseRepository = baseRepo,
|
||||
baseOwner = baseOwner,
|
||||
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._
|
||||
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{
|
||||
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
|
||||
users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender))
|
||||
@@ -192,21 +274,33 @@ object WebHookService {
|
||||
case class WebHookPushPayload(
|
||||
pusher: ApiUser,
|
||||
ref: String,
|
||||
before: String,
|
||||
after: String,
|
||||
commits: List[ApiCommit],
|
||||
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 {
|
||||
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(
|
||||
ApiUser(pusher),
|
||||
refName,
|
||||
commits.map{ commit => ApiCommit(git, RepositoryName(repositoryInfo), commit) },
|
||||
ApiRepository(
|
||||
pusher = ApiUser(pusher),
|
||||
ref = refName,
|
||||
before = ObjectId.toString(oldId),
|
||||
after = ObjectId.toString(newId),
|
||||
commits = commits.map{ commit => ApiCommit.forPushPayload(git, RepositoryName(repositoryInfo), commit) },
|
||||
repository = ApiRepository.forPushPayload(
|
||||
repositoryInfo,
|
||||
owner= ApiUser(repositoryOwner)
|
||||
)
|
||||
owner= ApiUser(repositoryOwner))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -273,7 +367,41 @@ object WebHookService {
|
||||
action = "created",
|
||||
repository = ApiRepository(repository, repositoryUser),
|
||||
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))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,18 @@ object AutoUpdate {
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
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, 3),
|
||||
|
||||
@@ -3,7 +3,7 @@ package gitbucket.core.servlet
|
||||
import java.io.File
|
||||
|
||||
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.WebHookService._
|
||||
@@ -200,10 +200,11 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
}
|
||||
|
||||
// call web hook
|
||||
callWebHookOf(owner, repository, "push"){
|
||||
callWebHookOf(owner, repository, WebHook.Push){
|
||||
for(pusherAccount <- getAccountByUserName(pusher);
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ object SshServer {
|
||||
|
||||
private def configure(port: Int, baseUrl: String) = {
|
||||
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.setCommandFactory(new GitCommandFactory(baseUrl))
|
||||
server.setShellFactory(new NoShell)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.net.URLConnection
|
||||
import org.apache.tika.Tika
|
||||
import java.io.File
|
||||
import ControlUtil._
|
||||
import scala.util.Random
|
||||
@@ -9,8 +9,8 @@ import scala.util.Random
|
||||
object FileUtil {
|
||||
|
||||
def getMimeType(name: String): String =
|
||||
defining(URLConnection.getFileNameMap()){ fileNameMap =>
|
||||
fileNameMap.getContentTypeFor(name) match {
|
||||
defining(new Tika()){ tika =>
|
||||
tika.detect(name) match {
|
||||
case null => "application/octet-stream"
|
||||
case mimeType => mimeType
|
||||
}
|
||||
@@ -28,6 +28,8 @@ object FileUtil {
|
||||
|
||||
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 isText(content: Array[Byte]): Boolean = !content.contains(0)
|
||||
@@ -50,4 +52,14 @@ object FileUtil {
|
||||
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{
|
||||
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
|
||||
}).split("/")
|
||||
|
||||
|
||||
@@ -100,8 +100,18 @@ object JGitUtil {
|
||||
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
|
||||
}
|
||||
|
||||
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String],
|
||||
oldIsImage: Boolean, newIsImage: Boolean, oldObjectId: Option[String], newObjectId: Option[String])
|
||||
case class DiffInfo(
|
||||
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.
|
||||
@@ -495,11 +505,31 @@ object JGitUtil {
|
||||
while(treeWalk.next){
|
||||
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
|
||||
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 {
|
||||
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
|
||||
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
|
||||
DiffInfo(
|
||||
changeType = ChangeType.ADD,
|
||||
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)
|
||||
@@ -518,16 +548,52 @@ object JGitUtil {
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
|
||||
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
|
||||
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
||||
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
||||
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))
|
||||
|
||||
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 {
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
|
||||
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
|
||||
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
||||
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
||||
if(!fetchContent || oldIsImage || newIsImage){
|
||||
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 {
|
||||
DiffInfo(
|
||||
changeType = diff.getChangeType,
|
||||
oldPath = diff.getOldPath,
|
||||
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
|
||||
}
|
||||
@@ -713,7 +779,7 @@ object JGitUtil {
|
||||
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
|
||||
using(git.getRepository.getObjectDatabase){ db =>
|
||||
val loader = db.open(id)
|
||||
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
|
||||
if(loader.isLarge || (fetchLargeFile == false && FileUtil.isLarge(loader.getSize))){
|
||||
None
|
||||
} else {
|
||||
Some(loader.getBytes)
|
||||
@@ -723,6 +789,22 @@ object JGitUtil {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
|
||||
object Notifier {
|
||||
// TODO We want to be able to switch to mock.
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ object StringUtil {
|
||||
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")
|
||||
|
||||
|
||||
@@ -7,31 +7,92 @@ import gitbucket.core.util.Implicits.RichString
|
||||
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,
|
||||
issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = {
|
||||
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 = {
|
||||
|
||||
// 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
|
||||
// convert issue id to link
|
||||
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
||||
getIssue(repository.owner, repository.name, m.group(2)) match {
|
||||
case Some(issue) if(issue.isPullRequest)
|
||||
=> Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>""")
|
||||
case Some(_) => Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>""")
|
||||
case None => Some(s"""#${m.group(2)}""")
|
||||
// convert username/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
|
||||
.replaceBy(("(?<=(^|\\W))(GH-|" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r){ m =>
|
||||
val prefix = if(m.group(2) == "issue:") "#" else m.group(2)
|
||||
getIssue(repository.owner, repository.name, m.group(3)) match {
|
||||
case Some(issue) if(issue.isPullRequest) =>
|
||||
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(3)}">${prefix}${m.group(3)}</a>""")
|
||||
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
|
||||
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m =>
|
||||
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_\\.]+)(?=(\\W|$))".r){ m =>
|
||||
getAccountByUserName(m.group(2)).map { _ =>
|
||||
s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
|
||||
}
|
||||
}
|
||||
|
||||
// convert commit id to link
|
||||
.replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""<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,18 +1,14 @@
|
||||
package gitbucket.core.view
|
||||
|
||||
import java.text.Normalizer
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
import java.util.Locale
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.service.{RepositoryService, RequestCache, WikiService}
|
||||
import gitbucket.core.service.{RepositoryService, RequestCache}
|
||||
import gitbucket.core.util.StringUtil
|
||||
import org.parboiled.common.StringUtils
|
||||
import org.pegdown.LinkRenderer.Rendering
|
||||
import org.pegdown._
|
||||
import org.pegdown.ast._
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import io.github.gitbucket.markedj._
|
||||
import io.github.gitbucket.markedj.Utils._
|
||||
|
||||
object Markdown {
|
||||
|
||||
@@ -24,7 +20,7 @@ object 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
|
||||
* @param hasWritePermission true if user has writable to ths given repository
|
||||
* @param pages the list of existing Wiki pages
|
||||
*/
|
||||
def toHtml(markdown: String,
|
||||
@@ -35,7 +31,6 @@ object Markdown {
|
||||
enableTaskList: Boolean = false,
|
||||
hasWritePermission: Boolean = false,
|
||||
pages: List[String] = Nil)(implicit context: Context): String = {
|
||||
|
||||
// escape issue id
|
||||
val s = if(enableRefsLink){
|
||||
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
||||
@@ -43,252 +38,147 @@ object Markdown {
|
||||
|
||||
// escape task list
|
||||
val source = if(enableTaskList){
|
||||
GitBucketHtmlSerializer.escapeTaskList(s)
|
||||
escapeTaskList(s)
|
||||
} else s
|
||||
|
||||
val rootNode = new PegDownProcessor(
|
||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS |
|
||||
Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML | Extensions.STRIKETHROUGH
|
||||
).parseMarkdown(source.toCharArray)
|
||||
|
||||
new GitBucketHtmlSerializer(
|
||||
markdown, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList,
|
||||
hasWritePermission, pages
|
||||
).toHtml(rootNode)
|
||||
val options = new Options()
|
||||
options.setSanitize(true)
|
||||
options.setBreaks(true)
|
||||
val renderer = new GitBucketMarkedRenderer(options, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages)
|
||||
Marked.marked(source, options, renderer)
|
||||
}
|
||||
}
|
||||
|
||||
class GitBucketLinkRender(
|
||||
context: Context,
|
||||
repository: RepositoryService.RepositoryInfo,
|
||||
enableWikiLink: Boolean,
|
||||
pages: List[String]) extends LinkRenderer with WikiService {
|
||||
/**
|
||||
* Extends markedj Renderer for GitBucket
|
||||
*/
|
||||
class GitBucketMarkedRenderer(options: Options, repository: RepositoryService.RepositoryInfo,
|
||||
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 = {
|
||||
if(enableWikiLink){
|
||||
try {
|
||||
val text = node.getText
|
||||
val (label, page) = if(text.contains('|')){
|
||||
val i = text.indexOf('|')
|
||||
(text.substring(0, i), text.substring(i + 1))
|
||||
override def heading(text: String, level: Int, raw: String): String = {
|
||||
val id = generateAnchorName(text)
|
||||
val out = new StringBuilder()
|
||||
|
||||
out.append("<h" + level + " id=\"" + options.getHeaderPrefix + id + "\" class=\"markdown-head\">")
|
||||
|
||||
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 {
|
||||
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 {
|
||||
(text, text)
|
||||
(link, link)
|
||||
}
|
||||
|
||||
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
|
||||
|
||||
if(pages.contains(page)){
|
||||
new Rendering(url, label)
|
||||
"<a href=\"" + url + "\">" + escape(label) + "</a>"
|
||||
} 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 {
|
||||
super.render(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
enableAnchor: 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(rendering: LinkRenderer.Rendering): Unit = {
|
||||
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(rendering.href, true)).print("\">")
|
||||
.print("<img src=\"").print(fixUrl(rendering.href, true)).print("\" alt=\"").printEncoded(rendering.text).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 = {
|
||||
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
|
||||
url
|
||||
} else if(url.startsWith("#")){
|
||||
("#" + GitBucketHtmlSerializer.generateAnchorName(url.substring(1)))
|
||||
} else if(!enableWikiLink){
|
||||
if(context.currentPath.contains("/blob/")){
|
||||
url + (if(isImage) "?raw=true" else "")
|
||||
} else if(context.currentPath.contains("/tree/")){
|
||||
val paths = context.currentPath.split("/")
|
||||
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
|
||||
} else {
|
||||
val paths = context.currentPath.split("/")
|
||||
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
|
||||
escape(text)
|
||||
}
|
||||
} else {
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
|
||||
}
|
||||
}
|
||||
|
||||
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 child = node.getChildren.asScala.headOption
|
||||
val anchorName = child match {
|
||||
case Some(x: AnchorLinkNode) => x.getName
|
||||
case Some(x: TextNode) => x.getText
|
||||
case _ => GitBucketHtmlSerializer.generateAnchorName(extractText(node)) // TODO
|
||||
}
|
||||
|
||||
printer.print(s"""<$tag class="markdown-head">""")
|
||||
if(enableAnchor){
|
||||
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
|
||||
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
|
||||
private def fixUrl(url: String, isImage: Boolean = false): String = {
|
||||
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
|
||||
url
|
||||
} else if(url.startsWith("#")){
|
||||
("#" + generateAnchorName(url.substring(1)))
|
||||
} else if(!enableWikiLink){
|
||||
if(context.currentPath.contains("/blob/")){
|
||||
url + (if(isImage) "?raw=true" else "")
|
||||
} else if(context.currentPath.contains("/tree/")){
|
||||
val paths = context.currentPath.split("/")
|
||||
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
|
||||
} else {
|
||||
val paths = context.currentPath.split("/")
|
||||
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
|
||||
}
|
||||
} else {
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
|
||||
}
|
||||
}
|
||||
child match {
|
||||
case Some(x: AnchorLinkNode) => printer.print(x.getText)
|
||||
case _ => visitChildren(node)
|
||||
}
|
||||
printer.print(s"</$tag>")
|
||||
}
|
||||
|
||||
private def extractText(node: Node): String = {
|
||||
val sb = new StringBuilder()
|
||||
node.getChildren.asScala.map {
|
||||
case x: TextNode => sb.append(x.getText)
|
||||
case x: Node => sb.append(extractText(x))
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
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 = {
|
||||
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 = {
|
||||
val disabled = if (hasWritePermission) "" else "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 + "/>")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -91,8 +91,12 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
enableTaskList: Boolean = false,
|
||||
hasWritePermission: Boolean = false,
|
||||
pages: List[String] = Nil)(implicit context: Context): Html =
|
||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, true, 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,
|
||||
repository: RepositoryService.RepositoryInfo,
|
||||
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = {
|
||||
@@ -103,10 +107,20 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
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 = {
|
||||
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.
|
||||
* This method looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
@@ -275,4 +289,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
case CommitState.ERROR => "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="box">
|
||||
<div class="box-header">Personal access tokens</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-bottom">
|
||||
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
||||
No tokens.
|
||||
}else{
|
||||
@@ -40,7 +40,7 @@
|
||||
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
|
||||
<div class="box">
|
||||
<div class="box-header">Generate new token</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-bottom">
|
||||
<fieldset>
|
||||
<label for="note" class="strong">Token description</label>
|
||||
<div><span id="error-note" class="error"></span></div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<form action="@url(account.userName)/_edit" method="POST" validate="true">
|
||||
<div class="box">
|
||||
<div class="box-header">Profile</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-bottom">
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
@if(account.password.nonEmpty){
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
</div>
|
||||
<div class="block">
|
||||
@if(account.url.isDefined){
|
||||
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
|
||||
<div><i class="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>
|
||||
@if(groupNames.nonEmpty){
|
||||
<div>
|
||||
|
||||
@@ -13,7 +13,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<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 =>
|
||||
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
|
||||
}
|
||||
@@ -40,7 +40,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
|
||||
<span class="strong"><i class="icon-lock"></i> </i> Private</span><br>
|
||||
<span class="strong"><i class="octicon octicon-lock"></i> </i> Private</span><br>
|
||||
<div>
|
||||
<span>Only collaborators can read this repository.</span>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="block-header">
|
||||
<a href="@url(repository)">@repository.name</a>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
<i class="octicon octicon-lock"></i>
|
||||
}
|
||||
</div>
|
||||
@if(repository.repository.originUserName.isDefined){
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="span9">
|
||||
<div class="box">
|
||||
<div class="box-header">SSH Keys</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-bottom">
|
||||
@if(sshKeys.isEmpty){
|
||||
No keys
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
||||
<div class="box">
|
||||
<div class="box-header">Add an SSH Key</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-bottom">
|
||||
<fieldset>
|
||||
<label for="title" class="strong">Title</label>
|
||||
<div><span id="error-title" class="error"></span></div>
|
||||
|
||||
@@ -3,19 +3,20 @@
|
||||
<div class="container">
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<div class="box">
|
||||
<ul class="nav nav-tabs nav-stacked side-menu">
|
||||
<li@if(active=="users"){ class="active"}>
|
||||
<a href="@path/admin/users">User Management</a>
|
||||
</li>
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul class="nav nav-tabs nav-stacked side-menu" id="system-admin-menu-container">
|
||||
<li@if(active=="users"){ class="active"}>
|
||||
<a href="@path/admin/users">User Management</a>
|
||||
</li>
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
<li@if(active=="plugins"){ class="active"}>
|
||||
<a href="@path/admin/plugins">Plugins</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="span9">
|
||||
@body
|
||||
|
||||
@@ -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">
|
||||
<div class="box">
|
||||
<div class="box-header">System Settings</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-bottom">
|
||||
<!--====================================================================-->
|
||||
<!-- GITBUCKET_HOME -->
|
||||
<!--====================================================================-->
|
||||
@@ -224,14 +224,25 @@
|
||||
<!-- Notification email -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Notification email</label>
|
||||
<label class="strong">Notifications</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
|
||||
Send notifications
|
||||
</label>
|
||||
</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">
|
||||
<label class="control-label" for="smtpHost">SMTP Host</label>
|
||||
<div class="controls">
|
||||
@@ -277,12 +288,15 @@
|
||||
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Enable notification not only SMTP configuration if you want to send notification email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset>
|
||||
<div class="align-right" style="margin-top: 20px;">
|
||||
<input type="submit" class="btn btn-success" value="Apply changes"/>
|
||||
</fieldset>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
@@ -292,8 +306,16 @@ $(function(){
|
||||
$('.ssh input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
|
||||
$('#notification').change(function(){
|
||||
$('.notification input').prop('disabled', !$(this).prop('checked'));
|
||||
$('#useSMTP').change(function(){
|
||||
$('.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();
|
||||
|
||||
$('#ldapAuthentication').change(function(){
|
||||
|
||||
@@ -43,10 +43,10 @@
|
||||
<div>
|
||||
<hr>
|
||||
@if(!account.isGroupAccount){
|
||||
<i class="icon-envelope"></i> @account.mailAddress
|
||||
<i class="octicon octicon-mail"></i> @account.mailAddress
|
||||
}
|
||||
@account.url.map { url =>
|
||||
<i class="icon-home"></i> @url
|
||||
<i class="octicon octicon-home"></i> @url
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@(id: String, width: Int)(implicit context: gitbucket.core.controller.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>
|
||||
$(function(){
|
||||
$('#@id').typeahead({
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
@(owner: String, repository: String)(textarea: Html)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core.util.FileUtil
|
||||
<div class="muted attachable">
|
||||
@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>
|
||||
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
|
||||
<script>
|
||||
$(function(){
|
||||
try {
|
||||
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
|
||||
url: '@path/upload/image/@owner/@repository',
|
||||
url: '@path/upload/file/@owner/@repository',
|
||||
maxFilesize: 10,
|
||||
acceptedFiles: 'image/*',
|
||||
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.',
|
||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
|
||||
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 files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
success: function(file, id) {
|
||||
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
|
||||
$('#@textareaId').val($('#@textareaId').val() + images);
|
||||
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +
|
||||
'](@baseUrl/@owner/@repository/_attached/' + id + ')';
|
||||
$('#@textareaId').val($('#@textareaId').val() + attachFile);
|
||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@helper.html.dropdown(
|
||||
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",
|
||||
mini = true
|
||||
mini = false
|
||||
) {
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@(condition: => Boolean)
|
||||
@if(condition){
|
||||
<i class="icon-ok"></i>
|
||||
<i class="octicon octicon-check"></i>
|
||||
} else {
|
||||
<i class="icon-white"></i>
|
||||
}
|
||||
@@ -5,16 +5,20 @@
|
||||
@import context._
|
||||
@import gitbucket.core._
|
||||
@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="box commit-comment-box commit-comment-@comment.commentId">
|
||||
<div class="box-header-small">
|
||||
<div class="box-header">
|
||||
@user(comment.commentedUserName, styleClass="username strong")
|
||||
<span class="muted">
|
||||
commented
|
||||
@if(comment.pullRequest){
|
||||
@if(comment.issueId.isDefined){
|
||||
on this Pull Request
|
||||
}else{
|
||||
} else {
|
||||
@if(comment.fileName.isDefined){
|
||||
on @comment.fileName.get
|
||||
}
|
||||
@@ -24,12 +28,12 @@
|
||||
</span>
|
||||
<span class="pull-right">
|
||||
@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="icon-remove-circle"></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="octicon octicon-x"></i></a>
|
||||
}
|
||||
</span>
|
||||
</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)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@(id: String, value: String)(html: Html)
|
||||
<div class="input-append" style="margin-bottom: 0px;">
|
||||
@(id: String, value: String, prepend: Boolean = false)(html: Html)
|
||||
<div class="input-append @if(prepend){input-prepend}" style="margin-bottom: 0px;">
|
||||
@html
|
||||
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
|
||||
<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>
|
||||
<script>
|
||||
// copy to clipboard
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import gitbucket.core.view.helpers
|
||||
<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"/>
|
||||
<span class="add-on"><i class="icon-calendar"></i></span>
|
||||
<span class="add-on"><i class="octicon octicon-calendar"></i></span>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
@@ -92,30 +92,38 @@
|
||||
<td style="padding: 0;">
|
||||
@if(diff.oldObjectId == diff.newObjectId){
|
||||
<div class="diff-same">File renamed without changes</div>
|
||||
} else { @if(diff.newContent != None || diff.oldContent != None){
|
||||
<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="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
|
||||
} else { @if(diff.newIsImage || diff.oldIsImage){
|
||||
<div class="diff-image-render diff2up">
|
||||
@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>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.ADD){
|
||||
Not supported
|
||||
}
|
||||
}
|
||||
@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>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.DELETE){
|
||||
Not supported
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
Not supported
|
||||
} } }
|
||||
@if(diff.newContent != None || diff.oldContent != None){
|
||||
<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="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
|
||||
} else {
|
||||
@if(diff.newIsImage || diff.oldIsImage){
|
||||
<div class="diff-image-render diff2up">@diff.oldIsImage @diff.newIsImage
|
||||
@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>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.ADD){
|
||||
<div style="padding: 12px;">Not supported</div>
|
||||
}
|
||||
}
|
||||
@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>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.DELETE){
|
||||
<div style="padding: 12px;">Not supported</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
@if(diff.tooLarge){
|
||||
<div style="padding: 12px;">Too large</div>
|
||||
} else {
|
||||
<div style="padding: 12px;">Not supported</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -212,7 +220,6 @@ $(function(){
|
||||
dataType : 'html'
|
||||
},
|
||||
function(responseContent) {
|
||||
$this.hide();
|
||||
var tmp;
|
||||
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
||||
if (!isNaN(newLineNumber) && newLineNumber) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@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">
|
||||
@if(value.isEmpty){
|
||||
<i class="icon-cog"></i>
|
||||
<i class="octicon octicon-gear"></i>
|
||||
} else {
|
||||
@if(prefix.nonEmpty){
|
||||
<span class="muted">@prefix:</span>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
styleClass: String = "",
|
||||
placeholder: String = "Leave a comment",
|
||||
elastic: Boolean = false,
|
||||
tabIndex: Int = -2,
|
||||
uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core._
|
||||
@@ -22,6 +23,7 @@
|
||||
<span id="error-content" class="error"></span>
|
||||
@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>
|
||||
}
|
||||
|
||||
@@ -24,57 +24,64 @@
|
||||
@if(loginAccount.isEmpty){
|
||||
@signinform(settings)
|
||||
} else {
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th class="metal">
|
||||
<div class="pull-right">
|
||||
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
|
||||
</div>
|
||||
Your repositories (@userRepositories.size)
|
||||
</th>
|
||||
</tr>
|
||||
<div class="box-header">
|
||||
<div class="pull-right">
|
||||
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
|
||||
</div>
|
||||
<span class="strong">Your repositories</span> <span class="label">@userRepositories.size</span>
|
||||
</div>
|
||||
@if(userRepositories.isEmpty){
|
||||
<tr>
|
||||
<td>No repositories</td>
|
||||
</tr>
|
||||
<div class="box-content-bottom">
|
||||
No repositories
|
||||
</div>
|
||||
} else {
|
||||
@userRepositories.map { repository =>
|
||||
<tr>
|
||||
<td>
|
||||
@helper.html.repositoryicon(repository, false)
|
||||
@if(repository.owner == loginAccount.get.userName){
|
||||
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
||||
} else {
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<div class="box-content-bottom" style="padding: 0px;">
|
||||
@defining(20){ max =>
|
||||
@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)
|
||||
@if(repository.owner == loginAccount.get.userName){
|
||||
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
||||
} else {
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@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>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th class="metal">
|
||||
Recent updated repositories
|
||||
</th>
|
||||
</tr>
|
||||
@if(recentRepositories.isEmpty){
|
||||
<tr>
|
||||
<td>No repositories</td>
|
||||
</tr>
|
||||
} else {
|
||||
<div class="box-header">
|
||||
<span class="strong">Recent updated repositories</span>
|
||||
</div>
|
||||
@if(recentRepositories.isEmpty){
|
||||
<div class="box-content-bottom">
|
||||
No repositories
|
||||
</div>
|
||||
} else {
|
||||
<div class="box-content-bottom" style="padding: 0px;">
|
||||
@recentRepositories.map { repository =>
|
||||
<tr>
|
||||
<td>
|
||||
@helper.html.repositoryicon(repository, false)
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="box-content-row repo-link">
|
||||
@helper.html.repositoryicon(repository, false)
|
||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
</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,7 +8,7 @@
|
||||
<hr/><br/>
|
||||
<form method="POST" validate="true">
|
||||
<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">
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
@@ -17,18 +17,19 @@
|
||||
enableRefsLink = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "width: 635px; height: 100px; max-height: 150px;",
|
||||
elastic = true
|
||||
style = "",
|
||||
elastic = true,
|
||||
tabIndex = 1
|
||||
)
|
||||
<div style="text-align: right;">
|
||||
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
||||
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
|
||||
<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 class="pull-right">
|
||||
<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)){
|
||||
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
<script>
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
@import gitbucket.core.model.CommitComment
|
||||
@if(issue.isDefined){
|
||||
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
|
||||
<div class="box issue-comment-box">
|
||||
<div class="box-header-small">
|
||||
<div class="issue-comment-box">
|
||||
<div class="box-header">
|
||||
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
|
||||
<span class="pull-right">
|
||||
@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>
|
||||
</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)
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
|
||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
||||
<div class="box-header-small">
|
||||
<div class="box-header">
|
||||
@user(comment.commentedUserName, styleClass="username strong")
|
||||
<span class="muted">
|
||||
@if(comment.action == "comment"){
|
||||
@@ -41,24 +41,24 @@
|
||||
<span class="pull-right">
|
||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
||||
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil" 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-pencil" aria-label="Edit"></i></a>
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
|
||||
}
|
||||
</span>
|
||||
</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}")){
|
||||
@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="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 {
|
||||
@if(comment.action == "refer"){
|
||||
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
|
||||
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
|
||||
<strong>@issueLink(repository, issueId.toInt): @rest.mkString(":")</strong>
|
||||
}
|
||||
} else {
|
||||
<div class="markdown-body">@markdown(comment.content, repository, false, true, true, hasWritePermission)</div>
|
||||
@markdown(comment.content, repository, false, true, true, hasWritePermission)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -78,8 +78,8 @@
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "close" || comment.action == "close_comment"){
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label label-important">Closed</span>
|
||||
<div class="issue-comment-action">
|
||||
<i class="octicon octicon-circle-slash danger"></i>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
@if(issue.isDefined && issue.get.isPullRequest){
|
||||
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
|
||||
@@ -89,14 +89,14 @@
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label label-success">Reopened</span>
|
||||
<div class="issue-comment-action issue-reopened">
|
||||
<i class="octicon octicon-primitive-dot"></i>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "delete_branch"){
|
||||
<div class="small issue-comment-action">
|
||||
<div class="issue-comment-action">
|
||||
<span class="label">Deleted</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
|
||||
@@ -110,7 +110,7 @@
|
||||
<script>
|
||||
$(function(){
|
||||
@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 url = '@url(repository)/issue_comments/_data/' + id;
|
||||
var $content = $('#commentContent-' + id);
|
||||
@@ -130,7 +130,7 @@ $(function(){
|
||||
});
|
||||
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?')) {
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
$.post('@url(repository)/issue_comments/delete/' + id,
|
||||
@@ -144,7 +144,7 @@ $(function(){
|
||||
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 url = '@url(repository)/commit_comments/_data/' + id;
|
||||
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
|
||||
@@ -158,7 +158,7 @@ $(function(){
|
||||
});
|
||||
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?')) {
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
$.post('@url(repository)/commit_comments/delete/' + id,
|
||||
|
||||
@@ -10,148 +10,33 @@
|
||||
@navigation("issues", false, repository)
|
||||
<br/><br/><hr style="margin-bottom: 10px;">
|
||||
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span9">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-box">
|
||||
<div class="box-content">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;" autofocus/>
|
||||
<div>
|
||||
<span id="label-assigned">No one is assigned</span>
|
||||
@if(hasWritePermission){
|
||||
<input type="hidden" name="assignedUserName" value=""/>
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
<div class="pull-right">
|
||||
<span id="label-milestone">No milestone</span>
|
||||
@if(hasWritePermission){
|
||||
<input type="hidden" name="milestoneId" value=""/>
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
<i class="icon-while"></i> @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="milestone-alert">Due by @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="muted">Due by @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-box">
|
||||
<div class="box-content">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" name="title" value="" placeholder="Title" style="width: 690px;" autofocus/>
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "width: 690px; height: 200px; max-height: 250px;",
|
||||
elastic = true
|
||||
)
|
||||
<div class="align-right">
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "width: 565px; height: 200px; max-height: 250px;",
|
||||
elastic = true
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||
<div class="span2">
|
||||
@issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, hasWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
<div class="span3">
|
||||
@if(hasWritePermission){
|
||||
<span class="strong">Labels</span>
|
||||
<div>
|
||||
<div id="label-list">
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-label" data-label="@label.labelName" data-bgcolor="@label.color" data-fgcolor="@label.fontColor">
|
||||
<span style="background-color: #@label.color;" class="label-color"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<input type="hidden" name="labelNames" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('a.assign').click(function(){
|
||||
var userName = $(this).data('name');
|
||||
$('a.assign i.icon-ok').attr('class', 'icon-white');
|
||||
|
||||
if(userName == ''){
|
||||
$('#label-assigned').text('No one will be assigned');
|
||||
} else {
|
||||
$('#label-assigned').html($('<span>')
|
||||
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
|
||||
.append(' will be assigned'));
|
||||
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
$('input[name=assignedUserName]').val(userName);
|
||||
});
|
||||
|
||||
$('a.milestone').click(function(){
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
$('a.milestone i.icon-ok').attr('class', 'icon-white');
|
||||
|
||||
if(milestoneId == ''){
|
||||
$('#label-milestone').text('No milestone');
|
||||
} else {
|
||||
$('#label-milestone').html($('<span class="strong">').text(title));
|
||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
$('input[name=milestoneId]').val(milestoneId);
|
||||
});
|
||||
|
||||
$('a.toggle-label').click(function(){
|
||||
if($(this).data('selected') == true){
|
||||
$(this).css({
|
||||
'background-color': 'white',
|
||||
'color' : 'black',
|
||||
'font-weight' : 'normal'
|
||||
});
|
||||
$(this).data('selected', false);
|
||||
} else {
|
||||
$(this).css({
|
||||
'background-color': '#' + $(this).data('bgcolor'),
|
||||
'color' : '#' + $(this).data('fgcolor'),
|
||||
'font-weight' : 'bold'
|
||||
});
|
||||
$(this).data('selected', true);
|
||||
}
|
||||
|
||||
var labelNames = Array();
|
||||
$('a.toggle-label').each(function(i, e){
|
||||
if($(e).data('selected') == true){
|
||||
labelNames.push($(e).data('label'));
|
||||
}
|
||||
});
|
||||
$('input[name=labelNames]').val(labelNames.join(','));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import context._
|
||||
<span id="error-edit-content-@commentId" class="error"></span>
|
||||
@helper.html.attached(owner, repository){
|
||||
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||
<textarea id="edit-content-@commentId">@content</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger" value="Cancel"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@helper.html.attached(owner, repository){
|
||||
<textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
|
||||
<textarea id="edit-content">@content.getOrElse("")</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="cancel-issue" class="btn btn-small btn-danger" value="Cancel"/>
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
<div>
|
||||
<div class="show-title pull-right">
|
||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||
<a class="btn btn-small" href="#" id="edit">Edit</a>
|
||||
<a class="btn" href="#" id="edit">Edit</a>
|
||||
}
|
||||
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||
</div>
|
||||
<div class="edit-title pull-right" style="display: none;">
|
||||
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
||||
@@ -45,13 +45,14 @@
|
||||
</span>
|
||||
<br/><br/>
|
||||
<hr>
|
||||
<br/>
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
@commentlist(Some(issue), comments, hasWritePermission, repository)
|
||||
@commentform(issue, true, hasWritePermission, repository)
|
||||
</div>
|
||||
<div class="span2">
|
||||
@issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||
@issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@(issue: gitbucket.core.model.Issue,
|
||||
@(issue: Option[gitbucket.core.model.Issue],
|
||||
comments: List[gitbucket.core.model.Comment],
|
||||
issueLabels: List[gitbucket.core.model.Label],
|
||||
collaborators: List[String],
|
||||
@@ -6,6 +6,7 @@
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
hasWritePermission: Boolean,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="muted small strong">Labels</span>
|
||||
@@ -22,6 +23,9 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@if(issue.isEmpty){
|
||||
<input type="hidden" name="labelNames" value=""/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -34,11 +38,14 @@
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown(right = true) {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="octicon octicon-x"></i> Clear this milestone</a></li>
|
||||
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
|
||||
@issue.map { issue =>
|
||||
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId)
|
||||
}
|
||||
@milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
@@ -58,14 +65,14 @@
|
||||
}
|
||||
</div>
|
||||
<div id="milestone-progress-area">
|
||||
@issue.milestoneId.map { milestoneId =>
|
||||
@issue.flatMap(_.milestoneId).map { milestoneId =>
|
||||
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
|
||||
@issues.milestones.html.progress(openCount + closeCount, closeCount)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<span id="label-milestone">
|
||||
@issue.milestoneId.map { milestoneId =>
|
||||
@issue.flatMap(_.milestoneId).map { milestoneId =>
|
||||
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
|
||||
<span class="strong small">@milestone.title</span>
|
||||
}
|
||||
@@ -73,17 +80,20 @@
|
||||
<span class="muted small">No milestone</span>
|
||||
}
|
||||
</span>
|
||||
@if(issue.isEmpty){
|
||||
<input type="hidden" name="milestoneId" value=""/>
|
||||
}
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="muted small strong">Assignee</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown(right = true) {
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
|
||||
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
|
||||
@helper.html.checkicon(issue.exists(_.assignedUserName == collaborator))@avatar(collaborator, 20) @collaborator
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@@ -92,39 +102,37 @@
|
||||
}
|
||||
</div>
|
||||
<span id="label-assigned">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@issue.flatMap(_.assignedUserName).map { userName =>
|
||||
@avatar(userName, 20) @user(userName, styleClass="username strong small")
|
||||
}.getOrElse{
|
||||
<span class="muted small">No one</span>
|
||||
}
|
||||
</span>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
|
||||
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
|
||||
}
|
||||
</div>
|
||||
@if(issue.isEmpty){
|
||||
<input type="hidden" name="assignedUserName" value=""/>
|
||||
}
|
||||
@issue.map { issue =>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
|
||||
@participants.map { participant =>
|
||||
@avatarLink(participant, 20, tooltip = true)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
@issue.map { issue =>
|
||||
$('a.toggle-label').click(function(){
|
||||
var path, icon;
|
||||
var i = $(this).children('i');
|
||||
if(i.hasClass('icon-ok')){
|
||||
path = 'delete';
|
||||
icon = 'icon-white';
|
||||
} else {
|
||||
path = 'new';
|
||||
icon = 'icon-ok';
|
||||
}
|
||||
var path = switchLabel($(this));
|
||||
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
|
||||
{
|
||||
labelId : $(this).data('label-id')
|
||||
},
|
||||
function(data){
|
||||
i.removeClass().addClass(icon);
|
||||
$('ul.label-list').empty().html(data);
|
||||
});
|
||||
{ labelId : $(this).data('label-id') },
|
||||
function(data){
|
||||
$('ul.label-list').empty().html(data);
|
||||
}
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -132,42 +140,92 @@ $(function(){
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
$.post('@url(repository)/issues/@issue.issueId/milestone',
|
||||
{
|
||||
milestoneId: milestoneId
|
||||
},
|
||||
function(data){
|
||||
console.log(data);
|
||||
$('a.milestone i.icon-ok').attr('class', 'icon-white');
|
||||
if(milestoneId == ''){
|
||||
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
|
||||
$('#milestone-progress-area').empty();
|
||||
} else {
|
||||
$('#label-milestone').html($('<span class="strong small">').text(title));
|
||||
$('#milestone-progress-area').html(data);
|
||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||
{ milestoneId: milestoneId },
|
||||
function(data){
|
||||
displayMilestone(title, milestoneId, data);
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
$.post('@url(repository)/issues/@issue.issueId/assign',
|
||||
{
|
||||
assignedUserName: userName
|
||||
},
|
||||
function(){
|
||||
$('a.assign i.icon-ok').attr('class', 'icon-white');
|
||||
if(userName == ''){
|
||||
$('#label-assigned').html($('<span class="muted small">').text('No one'));
|
||||
} else {
|
||||
$('#label-assigned').empty()
|
||||
.append($this.find('img.avatar-mini').clone(false)).append(' ')
|
||||
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
|
||||
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
|
||||
{ assignedUserName: userName },
|
||||
function(){
|
||||
displayAssignee($this, userName);
|
||||
}
|
||||
);
|
||||
});
|
||||
}.getOrElse {
|
||||
$('a.toggle-label').click(function(){
|
||||
switchLabel($(this));
|
||||
var labelNames = Array();
|
||||
$('a.toggle-label').each(function(i, e){
|
||||
if($(e).children('i').hasClass('icon-ok') == true){
|
||||
labelNames.push($(e).text().trim());
|
||||
}
|
||||
});
|
||||
$('input[name=labelNames]').val(labelNames.join(','));
|
||||
|
||||
$.post('@url(repository)/issues/new/label',
|
||||
{ labelNames : labelNames.join(',') },
|
||||
function(data){
|
||||
$('ul.label-list').empty().html(data);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('a.milestone').click(function(){
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
displayMilestone(title, milestoneId);
|
||||
$('input[name=milestoneId]').val(milestoneId);
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
displayAssignee($this, userName);
|
||||
$('input[name=assignedUserName]').val(userName);
|
||||
});
|
||||
}
|
||||
|
||||
function switchLabel($this){
|
||||
var i = $this.children('i');
|
||||
if(i.hasClass('icon-ok')){
|
||||
i.removeClass().addClass('icon-white');
|
||||
return 'delete';
|
||||
} else {
|
||||
i.removeClass().addClass('icon-ok');
|
||||
return 'new';
|
||||
}
|
||||
}
|
||||
|
||||
function displayMilestone(title, milestoneId, progress){
|
||||
$('a.milestone i.icon-ok').attr('class', 'icon-white');
|
||||
if(milestoneId == ''){
|
||||
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
|
||||
$('#milestone-progress-area').empty();
|
||||
} else {
|
||||
$('#label-milestone').html($('<span class="strong small">').text(title));
|
||||
if(progress){
|
||||
$('#milestone-progress-area').html(progress);
|
||||
}
|
||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
}
|
||||
|
||||
function displayAssignee($this, userName){
|
||||
$('a.assign i.icon-ok').attr('class', 'icon-white');
|
||||
if(userName == ''){
|
||||
$('#label-assigned').html($('<span class="muted small">').text('No one'));
|
||||
} else {
|
||||
$('#label-assigned').empty()
|
||||
.append($this.find('img.avatar-mini').clone(false)).append(' ')
|
||||
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
|
||||
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Assignee", flat = true) {
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<form id="search" action="@path/search" method="POST">
|
||||
<div class="navbar">
|
||||
<div class="navbar-inner">
|
||||
<div class="container" style="width: 920px;">
|
||||
<div class="container" style="width: 980px;">
|
||||
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
@@ -66,15 +66,16 @@
|
||||
<div class="nav-collapse collapse pull-right header-menu">
|
||||
@if(loginAccount.isDefined){
|
||||
<div class="btn-group" style="margin-top: 0px;">
|
||||
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#"><i class="octicon octicon-plus" style="font-size: 20px; vertical-align: top; margin-top: 10px;"></i><span class="caret" style="vertical-align: middle;"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#"><i class="octicon octicon-plus" style="font-size: 20px; vertical-align: middle;height:20px !important;"></i><span class="caret" style="vertical-align: middle;"></span></a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li><a href="@path/new">New repository</a></li>
|
||||
<li><a href="@path/groups/new">New group</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group" style="margin-top: 0px;">
|
||||
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#" data-toggle="tooltip" data-placement="bottom" title="Signed is as @loginAccount.get.userName">@avatar(loginAccount.get.userName, 16)<span class="caret" style="vertical-align: middle;"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li><a href="@url(loginAccount.get.userName)">Your profile</a></li>
|
||||
<li><a href="@url(loginAccount.get.userName)/_edit">Account settings</a></li>
|
||||
@if(loginAccount.get.isAdmin){
|
||||
<li><a href="@path/admin/users">System administration</a></li>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
@sidemenu(path: String, name: String, icon: String, label: String, count: Int = 0) = {
|
||||
<li @if(active == name){class="active"} @if(!expand){data-toggle="tooltip" data-placement="left" data-original-title="@label"}>
|
||||
<div class="@if(active == name){margin} else {gradient} pull-left"></div>
|
||||
<a href="@url(repository)@path">
|
||||
<i class="menu-icon @if(active == name){menu-icon-active} octicon octicon-@{icon} "></i>
|
||||
@if(expand){ @label}
|
||||
@@ -40,7 +39,7 @@
|
||||
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
|
||||
</div>
|
||||
@if(loginAccount.isDefined && isNoGroup){
|
||||
<form id="fork-form" method="post" action="@path/@repository.owner/@repository.name/fork">
|
||||
<form id="fork-form" method="post" action="@path/@repository.owner/@repository.name/fork" style="display: none;">
|
||||
<input type="hidden" name="account" value="@loginAccount.get.userName"/>
|
||||
</form>
|
||||
}
|
||||
@@ -86,21 +85,28 @@
|
||||
</div>
|
||||
}
|
||||
@id.map { id =>
|
||||
@if(context.platform != "linux" && context.platform != null){
|
||||
<div style="margin-top: 10px;">
|
||||
<a href="@repository.httpOpenRepoUrl(context.platform)" id="repository-clone-url" class="btn btn-small" style="width: 147px;font-weight: bold;"><i class="octicon octicon-desktop-download"></i> Clone in Desktop</a>
|
||||
</div>
|
||||
}
|
||||
<div style="margin-top: 10px;">
|
||||
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-small" style="width: 147px;font-weight: bold;"><i class="octicon octicon-cloud-download"></i>Download ZIP</a>
|
||||
</div>
|
||||
@*
|
||||
<div style="margin-top: 10px;">
|
||||
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.tar.gz" class="btn btn-small" style="width: 147px;font-weight: bold;"><i class="octicon octicon-cloud-download"></i>Download TAR.GZ</a>
|
||||
</div>
|
||||
*@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div style="margin-right: @if(expand){180px} else {50px};">
|
||||
@if(expand){
|
||||
@repository.repository.description.map { description =>
|
||||
<p class="description">@description</p>
|
||||
<p class="description">@detectAndRenderLinks(description)</p>
|
||||
}
|
||||
<div style="border: 1px solid #eee; padding: 4px; margin-bottom: 10px;">
|
||||
<div style="margin-bottom: 10px;" class="box-content">
|
||||
<table class="fill-width">
|
||||
<tr>
|
||||
<td style="width: 33%; text-align: center;">
|
||||
@@ -171,12 +177,14 @@ $(function(){
|
||||
$('#repository-url-http').click(function(){
|
||||
$('#repository-url-proto').text('HTTP');
|
||||
$('#repository-url').val('@repository.httpUrl');
|
||||
$('#repository-clone-url').attr('href', '@repository.httpOpenRepoUrl(context.platform)')
|
||||
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
|
||||
});
|
||||
|
||||
$('#repository-url-ssh').click(function(){
|
||||
$('#repository-url-proto').text('SSH');
|
||||
$('#repository-url').val('@repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
|
||||
$('#repository-clone-url').attr('href', '@repository.sshOpenRepoUrl(context.platform, settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
|
||||
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,32 +4,42 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@import gitbucket.core.model._
|
||||
<div class="box">
|
||||
<table class="table table-file-list" style="border: 1px solid silver;">
|
||||
@commits.map { day =>
|
||||
<tr>
|
||||
<th colspan="4" class="box-header" style="font-weight: normal;">@date(day.head.commitTime)</th>
|
||||
</tr>
|
||||
@day.map { commit =>
|
||||
<tr>
|
||||
<td style="width: 20%;">
|
||||
@avatar(commit, 20)
|
||||
@user(commit.authorName, commit.authorEmailAddress, "username")
|
||||
</td>
|
||||
<td>@commit.shortMessage</td>
|
||||
<td style="width: 10%; text-align: right">
|
||||
<span class="badge" style="display: inline">@if(comments.isDefined){
|
||||
@comments.get.flatMap @{
|
||||
case comment: CommitComment => Some(comment)
|
||||
case other => None
|
||||
}.count(t => t.commitId == commit.id && !t.pullRequest)
|
||||
}</span>
|
||||
</td>
|
||||
<td style="width: 10%; text-align: right;">
|
||||
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<div class="commit-list">
|
||||
@commits.map { day =>
|
||||
<div class="muted" style="background-color: white;">
|
||||
<i class="octicon octicon-git-commit"></i> Commits on @date(day.head.commitTime)
|
||||
</div>
|
||||
<div class="box-commits">
|
||||
@day.map { commit =>
|
||||
<div class="box-content-row" style="padding: 8px;">
|
||||
<ul class="nav nav-pills-group pull-right" style="margin-top: 2px; margin-bottom: 0px; margin-right: 4px;">
|
||||
<li class="first"><a href="@url(repository)/commit/@commit.id" class="link monospace">@commit.id.substring(0, 7)</a></li>
|
||||
<li class="last"><a href="@url(repository)/tree/@commit.id" style="padding-top: 9px; padding-bottom: 10px;"><i class="octicon octicon-code link"></i></a></li>
|
||||
</ul>
|
||||
<div>
|
||||
<div class="commit-avatar-image">@avatar(commit, 40)</div>
|
||||
<div class="commit-message-box">
|
||||
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
|
||||
@if(commit.description.isDefined){
|
||||
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
|
||||
}
|
||||
<br>
|
||||
@if(commit.description.isDefined){
|
||||
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
|
||||
}
|
||||
<div class="small">
|
||||
@user(commit.authorName, commit.authorEmailAddress, "username")
|
||||
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
|
||||
@if(commit.isDifferentFromAuthor) {
|
||||
<span class="octicon octicon-arrow-right" style="margin-top : -2px;"></span>
|
||||
@user(commit.committerName, commit.committerEmailAddress, "username")
|
||||
<span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -9,18 +9,16 @@
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
hasOriginWritePermission: Boolean,
|
||||
collaborators: List[String],
|
||||
milestones: List[gitbucket.core.model.Milestone],
|
||||
labels: List[gitbucket.core.model.Label])(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.menu("pulls", repository){
|
||||
<div class="pullreq-info">
|
||||
<div id="compare-info">
|
||||
<a href="#" id="edit-compare-condition" class="btn btn-mini pull-right">Edit</a>
|
||||
<span class="label label-info monospace">@originRepository.owner:@originId</span> ... <span class="label label-info monospace">@forkedRepository.owner:@forkedId</span>
|
||||
</div>
|
||||
<div id="compare-edit" style="display: none;">
|
||||
<a href="#" id="cancel-condition-editing" class="pull-right"><i class="icon-remove-circle"></i></a>
|
||||
<div id="compare-edit">
|
||||
@helper.html.dropdown(originRepository.owner + "/" + originRepository.name, "base fork") {
|
||||
@members.map { case (owner, name) =>
|
||||
<li><a href="#" class="origin-owner" data-owner="@owner" data-name="@name">@helper.html.checkicon(owner == originRepository.owner) @owner/@name</a></li>
|
||||
@@ -43,41 +41,52 @@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if(commits.nonEmpty && hasWritePermission){
|
||||
<div style="margin-bottom: 10px;" id="create-pull-request">
|
||||
<a href="#" class="btn btn-success" id="show-form">Create pull request</a>
|
||||
<div class="check-conflict" style="display: none;">
|
||||
<img src="@assets/common/images/indicator.gif"/> Checking...
|
||||
</div>
|
||||
<div id="pull-request-form" class="box" style="display: none;">
|
||||
<div class="box-content">
|
||||
<form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
|
||||
<div style="width: 240px; position: absolute; margin-left: 610px;">
|
||||
<div class="check-conflict" style="display: none;">
|
||||
<img src="@assets/common/images/indicator.gif"/> Checking...
|
||||
</div>
|
||||
@if(commits.nonEmpty && loginAccount.isDefined){
|
||||
<div style="margin-bottom: 10px; padding: 8px; background-color: #fff9ea" id="create-pull-request" class="box-content">
|
||||
<a href="#" class="btn btn-success" id="show-form">Create pull request</a>
|
||||
|
||||
<span class="muted">Discuss and review the changes in this comparison with others.</span>
|
||||
</div>
|
||||
<div id="pull-request-form" @*class="box"*@ style="display: none; margin-bottom: 20px;">
|
||||
<form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-box">
|
||||
<div class="box-content">
|
||||
<span class="error" id="error-title"></span>
|
||||
<input type="text" name="title" style="width: 690px" placeholder="Title"/>
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true,
|
||||
style = "width: 690px; height: 200px;"
|
||||
)
|
||||
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
|
||||
<input type="hidden" name="targetBranch" value="@originId"/>
|
||||
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
|
||||
<input type="hidden" name="requestRepositoryName" value="@forkedRepository.name"/>
|
||||
<input type="hidden" name="requestBranch" value="@forkedId"/>
|
||||
<input type="hidden" name="commitIdFrom" value="@sourceId"/>
|
||||
<input type="hidden" name="commitIdTo" value="@commitId"/>
|
||||
<div class="align-right">
|
||||
<input type="submit" class="btn btn-success" value="Create pull request"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 600px; border-right: 1px solid #d4d4d4;">
|
||||
<span class="error" id="error-title"></span>
|
||||
<input type="text" name="title" style="width: 580px" placeholder="Title"/>
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "width: 580px; height: 200px;"
|
||||
)
|
||||
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
|
||||
<input type="hidden" name="targetBranch" value="@originId"/>
|
||||
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
|
||||
<input type="hidden" name="requestRepositoryName" value="@forkedRepository.name"/>
|
||||
<input type="hidden" name="requestBranch" value="@forkedId"/>
|
||||
<input type="hidden" name="commitIdFrom" value="@sourceId"/>
|
||||
<input type="hidden" name="commitIdTo" value="@commitId"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="span2">
|
||||
@issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), labels, hasOriginWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
@if(commits.isEmpty){
|
||||
@@ -90,72 +99,119 @@
|
||||
</tr>
|
||||
</table>
|
||||
} else {
|
||||
@pulls.html.commits(commits, Some(comments), repository)
|
||||
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, hasWritePermission, false)
|
||||
<div style="margin-bottom: 10px;" class="box-content">
|
||||
<table class="fill-width">
|
||||
<tr>
|
||||
<td style="width: 25%; text-align: center;">
|
||||
<i class="octicon octicon-commit"></i>
|
||||
@defining(commits.flatten){ commits =>
|
||||
<strong>@commits.size</strong> @plural(commits.size, "commit")
|
||||
}
|
||||
</td>
|
||||
<td style="width: 25%; text-align: center;">
|
||||
<i class="octicon octicon-diff"></i>
|
||||
<strong>@diffs.size</strong> @plural(diffs.size, "file") changed
|
||||
</td>
|
||||
<td style="width: 25%; text-align: center;">
|
||||
<i class="octicon octicon-comment"></i>
|
||||
@defining(comments.collect { case c: gitbucket.core.model.CommitComment => c }){ comments =>
|
||||
<strong>@comments.size</strong> commit @plural(comments.size, "comment")
|
||||
}
|
||||
</td>
|
||||
<td style="width: 25%; text-align: center;">
|
||||
<i class="octicon octicon-organization"></i>
|
||||
@defining(commits.flatMap(_.map(_.authorEmailAddress)).distinct){ contributors =>
|
||||
<strong>@contributors.size</strong> @plural(contributors.size, "contributor")
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box" style="margin-bottom: 20px;">
|
||||
@commits.map { day =>
|
||||
<div style="margin-top: 8px; margin-bottom: 8px;" class="muted">
|
||||
Commits on @date(day.head.commitTime)
|
||||
</div>
|
||||
<table style="width: 100%;">
|
||||
@day.map { commit =>
|
||||
<tr>
|
||||
<td style="width: 20%;">
|
||||
<i class="octicon octicon-git-commit"></i>
|
||||
@avatar(commit, 20)
|
||||
@user(commit.authorName, commit.authorEmailAddress, "username strong")
|
||||
</td>
|
||||
<td><span class="monospace">@commit.shortMessage</span></td>
|
||||
@*
|
||||
<span class="badge" style="display: inline">@if(comments.isDefined){
|
||||
@comments.get.flatMap @{
|
||||
case comment: CommitComment => Some(comment)
|
||||
case other => None
|
||||
}.count(t => t.commitId == commit.id && !t.pullRequest)
|
||||
}</span>
|
||||
*@
|
||||
<td style="width: 10%; text-align: right;">
|
||||
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, false, false)
|
||||
<p>Showing you all comments on commits in this comparison.</p>
|
||||
@issues.html.commentlist(None, comments, hasWritePermission, repository, None)
|
||||
@issues.html.commentlist(None, comments, false, repository, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#edit-compare-condition').click(function(){
|
||||
$('#compare-info').hide();
|
||||
$('#compare-edit').show();
|
||||
});
|
||||
|
||||
$('#cancel-condition-editing').click(function(){
|
||||
$('#compare-info').show();
|
||||
$('#compare-edit').hide();
|
||||
});
|
||||
|
||||
$('a.origin-owner, a.forked-owner, a.origin-branch, a.forked-branch').click(function(){
|
||||
var e = $(this);
|
||||
e.parents('ul').find('i').attr('class', 'icon-white');
|
||||
e.find('i').attr('class', 'icon-ok');
|
||||
e.find('i').attr('class', 'octicon-check');
|
||||
e.parents('div.btn-group').find('button span.strong').text(e.text());
|
||||
|
||||
@if(members.isEmpty){
|
||||
location.href = '@url(repository)/compare/' +
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'));
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'));
|
||||
} else {
|
||||
location.href = '@path/' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + '/' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) +'/compare/' +
|
||||
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'));
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + '/' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('name')) +'/compare/' +
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'));
|
||||
}
|
||||
});
|
||||
|
||||
$('#show-form').click(function(){
|
||||
$(this).hide();
|
||||
$('#create-pull-request').hide();
|
||||
$('#pull-request-form').show();
|
||||
});
|
||||
if(location.search.substr(1).split("&").indexOf("expand=1")!=-1){
|
||||
$('#show-form').click();
|
||||
}
|
||||
|
||||
@if(hasWritePermission){
|
||||
@if(loginAccount.isDefined){
|
||||
function checkConflict(from, to){
|
||||
$('.check-conflict').show();
|
||||
$.get('@url(repository)/compare/' + from + '...' + to + '/mergecheck',
|
||||
$.get('@url(forkedRepository)/compare/' + from + '...' + to + '/mergecheck',
|
||||
function(data){ $('.check-conflict').html(data); });
|
||||
}
|
||||
|
||||
@if(members.isEmpty){
|
||||
checkConflict(
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')),
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'))
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')),
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'))
|
||||
);
|
||||
} else {
|
||||
checkConflict(
|
||||
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')),
|
||||
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'))
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')),
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="span2">
|
||||
@issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||
@issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
@(hasConflict: Boolean)
|
||||
@if(hasConflict){
|
||||
<h4>We can’t automatically merge these branches</h4>
|
||||
<p>Don't worry, you can still submit the pull request.</p>
|
||||
} else {
|
||||
<h4 style="color: #468847;">Able to merge</h4>
|
||||
<p>These branches can be automatically merged.</p>
|
||||
}
|
||||
<input type="submit" class="btn btn-success btn-block" value="Create pull request"/>
|
||||
<div style="margin-top: 8px;">
|
||||
@if(hasConflict){
|
||||
<i class="octicon octicon-x" style="color: #bd2c00"></i>
|
||||
<span class="strong" style="color: #bd2c00;">Can’t automatically merge.</span>
|
||||
<span class="muted">Don’t worry, you can still create the pull request.</span>
|
||||
} else {
|
||||
<i class="octicon octicon-check" style="color: #6cc644"></i>
|
||||
<span class="strong" style="color: #6cc644">Able to merge.</span>
|
||||
<span class="muted">These branches can be automatically merged.</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
issue: gitbucket.core.model.Issue,
|
||||
pullreq: gitbucket.core.model.PullRequest,
|
||||
statuses: List[model.CommitStatus],
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
requestRepositoryUrl: String)(implicit context: gitbucket.core.controller.Context)
|
||||
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.service.SystemSettingsService
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@import model.CommitState
|
||||
@@ -22,100 +23,101 @@
|
||||
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
||||
</div>
|
||||
}
|
||||
}else{
|
||||
@defining(statuses.groupBy(_.state)){ stateMap => @defining(CommitState.combine(stateMap.keySet)){ state =>
|
||||
<div class="build-status-item">
|
||||
<a class="pull-right" id="toggle-all-checks"></a>
|
||||
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
|
||||
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
|
||||
<span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
|
||||
</div>
|
||||
<div class="build-statuses-list" style="@if(state==CommitState.SUCCESS){ display:none; }else{ }">
|
||||
@statuses.map{ status =>
|
||||
} else {
|
||||
@defining(statuses.groupBy(_.state)){ stateMap =>
|
||||
@defining(CommitState.combine(stateMap.keySet)){ state =>
|
||||
<div class="build-status-item">
|
||||
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
|
||||
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
|
||||
<span class="text-@{status.state.name}">@status.context</span>
|
||||
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
||||
<a class="pull-right" id="toggle-all-checks"></a>
|
||||
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
|
||||
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
|
||||
<span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} }
|
||||
<div class="build-statuses-list" style="@if(state==CommitState.SUCCESS){ display:none; }else{ }">
|
||||
@statuses.map{ status =>
|
||||
<div class="build-status-item">
|
||||
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
|
||||
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
|
||||
<span class="text-@{status.state.name}">@status.context</span>
|
||||
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="pull-right">
|
||||
<input type="button" class="btn @if(!hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
|
||||
</div>
|
||||
<div>
|
||||
@if(hasConflict){
|
||||
<span class="strong">We can’t automatically merge this pull request.</span>
|
||||
} else{ @if(hasProblem){
|
||||
<span class="strong">Merge with caution!</span>
|
||||
} else {
|
||||
<span class="strong">This pull request can be automatically merged.</span>
|
||||
} }
|
||||
</div>
|
||||
<div class="small">
|
||||
@if(hasConflict){
|
||||
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
|
||||
} else {
|
||||
You can also merge branches on the <a href="#" id="show-command-line">command line</a>.
|
||||
}
|
||||
</div>
|
||||
<div id="command-line" style="display: none;">
|
||||
<hr>
|
||||
@if(hasConflict){
|
||||
<span class="strong">Checkout via command line</span>
|
||||
<p>
|
||||
If you cannot merge a pull request automatically here, you have the option of checking
|
||||
it out via command line to resolve conflicts and perform a manual merge.
|
||||
</p>
|
||||
} else {
|
||||
<span class="strong">Merging via command line</span>
|
||||
<p>
|
||||
If you do not want to use the merge button or an automatic merge cannot be performed,
|
||||
you can perform a manual merge on the command line.
|
||||
</p>
|
||||
}
|
||||
@helper.html.copy("repository-url-copy", requestRepositoryUrl){
|
||||
<input type="text" style="width: 500px;" value="@requestRepositoryUrl" id="repository-url" readonly>
|
||||
}
|
||||
<div>
|
||||
<p>
|
||||
<span class="strong">Step 1:</span> Check out a new branch to test the changes — run this from your project directory
|
||||
</p>
|
||||
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}"){ command =>
|
||||
@helper.html.copy("merge-command-copy-1", command){
|
||||
<pre style="width: 500px; float: left;">@command</pre>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<span class="strong">Step 2:</span> Bring in @{pullreq.requestUserName}'s changes and test
|
||||
</p>
|
||||
@defining(s"git pull ${requestRepositoryUrl} ${pullreq.requestBranch}"){ command =>
|
||||
@helper.html.copy("merge-command-copy-2", command){
|
||||
<pre style="width: 500px; float: left;">@command</pre>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<span class="strong">Step 3:</span> Merge the changes and update the server
|
||||
</p>
|
||||
@defining(s"git checkout ${pullreq.branch}\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
|
||||
@helper.html.copy("merge-command-copy-3", command){
|
||||
<pre style="width: 500px; float: left;">@command</pre>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pull-right">
|
||||
<input type="button" class="btn @if(!hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
|
||||
</div>
|
||||
<div>
|
||||
@if(hasConflict){
|
||||
<span class="strong">We can’t automatically merge this pull request.</span>
|
||||
} else {
|
||||
@if(hasProblem){
|
||||
<span class="strong">Merge with caution!</span>
|
||||
} else {
|
||||
<span class="strong">This pull request can be automatically merged.</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="small">
|
||||
@if(hasConflict){
|
||||
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
|
||||
} else {
|
||||
You can also merge branches on the <a href="#" id="show-command-line">command line</a>.
|
||||
}
|
||||
</div>
|
||||
<div id="command-line" style="display: none;">
|
||||
<hr>
|
||||
@if(hasConflict){
|
||||
<span class="strong">Checkout via command line</span>
|
||||
<p>
|
||||
If you cannot merge a pull request automatically here, you have the option of checking
|
||||
it out via command line to resolve conflicts and perform a manual merge.
|
||||
</p>
|
||||
} else {
|
||||
<span class="strong">Merging via command line</span>
|
||||
<p>
|
||||
If you do not want to use the merge button or an automatic merge cannot be performed,
|
||||
you can perform a manual merge on the command line.
|
||||
</p>
|
||||
}
|
||||
@helper.html.copy("repository-url-copy", forkedRepository.httpUrl, true){
|
||||
<div class="btn-group" data-toggle="buttons-radio">
|
||||
<button class="btn btn-small active" type="button" id="repository-url-http">HTTP</button>
|
||||
@if(settings.ssh && loginAccount.isDefined){
|
||||
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button>
|
||||
}
|
||||
</div>
|
||||
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly>
|
||||
}
|
||||
<div>
|
||||
<p>
|
||||
<span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes.
|
||||
</p>
|
||||
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" +
|
||||
s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command =>
|
||||
@helper.html.copy("merge-command-copy-1", command){
|
||||
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;" id="merge-command">@Html(command)</pre>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<span class="strong">Step 2:</span> Merge the changes and update on the server.
|
||||
</p>
|
||||
@defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" +
|
||||
s"git push origin ${pullreq.branch}"){ command =>
|
||||
@helper.html.copy("merge-command-copy-2", command){
|
||||
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;">@command</pre>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="confirm-merge-form" style="display: none;">
|
||||
<form method="POST" action="@url(repository)/pull/@pullreq.issueId/merge">
|
||||
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/merge">
|
||||
<div class="strong">
|
||||
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
|
||||
</div>
|
||||
@@ -149,5 +151,31 @@ $(function(){
|
||||
$('#merge-pull-request').hide();
|
||||
$('#confirm-merge-form').show();
|
||||
});
|
||||
|
||||
@if(settings.ssh && loginAccount.isDefined){
|
||||
$('#repository-url-http').click(function(){
|
||||
// Update URL box
|
||||
$('#repository-url').val('@forkedRepository.httpUrl');
|
||||
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
|
||||
// Update command guidance
|
||||
$('#merge-command').text($('#merge-command').text().replace(
|
||||
'@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)',
|
||||
'@forkedRepository.httpUrl'
|
||||
));
|
||||
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
|
||||
});
|
||||
|
||||
$('#repository-url-ssh').click(function(){
|
||||
// Update URL box
|
||||
$('#repository-url').val('@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
|
||||
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
|
||||
// Update command guidance
|
||||
$('#merge-command').text($('#merge-command').text().replace(
|
||||
'@forkedRepository.httpUrl',
|
||||
'@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)'
|
||||
));
|
||||
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -18,9 +18,9 @@
|
||||
<div>
|
||||
<div class="show-title pull-right">
|
||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||
<a class="btn btn-small" href="#" id="edit">Edit</a>
|
||||
<a class="btn" href="#" id="edit">Edit</a>
|
||||
}
|
||||
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||
</div>
|
||||
<div class="edit-title pull-right" style="display: none;">
|
||||
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@html.menu("code", repository){
|
||||
<div class="head">
|
||||
<div class="pull-right hide-if-blame"><div class="btn-group">
|
||||
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="icon icon-th-list"></i></a>
|
||||
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="octicon octicon-list-unordered"></i></a>
|
||||
</div></div>
|
||||
<div class="line-age-legend">
|
||||
<span>Newer</span>
|
||||
@@ -47,57 +47,50 @@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered blobview">
|
||||
<tr>
|
||||
<th style="font-weight: normal;">
|
||||
<div class="pull-left">
|
||||
@avatar(latestCommit, 20)
|
||||
@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")
|
||||
<span class="muted">@helper.html.datetimeago(latestCommit.commitTime)</span>
|
||||
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
|
||||
</div>
|
||||
<div class="btn-group pull-right">
|
||||
@if(hasWritePermission && content.viewType == "text" && repository.branchList.contains(branch)){
|
||||
<a class="btn btn-mini" href="@url(repository)/edit/@encodeRefName(branch)/@pathList.mkString("/")">Edit</a>
|
||||
}
|
||||
<a class="btn btn-mini" href="?raw=true">Raw</a>
|
||||
@if(content.viewType == "text"){
|
||||
<a class="btn btn-mini blame-action" href="@url(repository)/blame/@latestCommit.id/@pathList.mkString("/")" data-url="@url(repository)/get-blame/@latestCommit.id/@pathList.mkString("/")" data-repository="@url(repository)">Blame</a>
|
||||
}
|
||||
<a class="btn btn-mini" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
|
||||
@if(hasWritePermission){
|
||||
<a class="btn btn-mini btn-danger" href="@url(repository)/remove/@encodeRefName(branch)/@pathList.mkString("/")">Delete</a>
|
||||
}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@if(content.viewType == "text"){
|
||||
@defining(isRenderable(pathList.reverse.head)){ isRrenderable =>
|
||||
@if(!isBlame && isRrenderable) {
|
||||
<div class="box-content markdown-body" style="border: none; padding-left: 16px; padding-right: 16px;">
|
||||
@renderMarkup(pathList, content.content.get, branch, repository, false, false, true)
|
||||
</div>
|
||||
} else {
|
||||
<pre class="prettyprint linenums blob @if(!isRrenderable){ no-renderable } ">@content.content.get</pre>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if(content.viewType == "image"){
|
||||
<img src="?raw=true"/>
|
||||
}
|
||||
@if(content.viewType == "large" || content.viewType == "binary"){
|
||||
<div style="text-align: center; padding-top: 20px; padding-bottom: 20px;">
|
||||
<a href="?raw=true">View Raw</a><br>
|
||||
<br>
|
||||
(Sorry about that, but we can't show files that are this big right now)
|
||||
<div class="box-header">
|
||||
@avatar(latestCommit, 20)
|
||||
@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")
|
||||
<span class="muted">@helper.html.datetimeago(latestCommit.commitTime)</span>
|
||||
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
|
||||
<div class="btn-group pull-right">
|
||||
@if(hasWritePermission && content.viewType == "text" && repository.branchList.contains(branch)){
|
||||
<a class="btn btn-mini" href="@url(repository)/edit/@encodeRefName(branch)/@pathList.mkString("/")">Edit</a>
|
||||
}
|
||||
<a class="btn btn-mini" href="?raw=true">Raw</a>
|
||||
@if(content.viewType == "text"){
|
||||
<a class="btn btn-mini blame-action" href="@url(repository)/blame/@latestCommit.id/@pathList.mkString("/")" data-url="@url(repository)/get-blame/@latestCommit.id/@pathList.mkString("/")" data-repository="@url(repository)">Blame</a>
|
||||
}
|
||||
<a class="btn btn-mini" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
|
||||
@if(hasWritePermission && repository.branchList.contains(branch)){
|
||||
<a class="btn btn-mini btn-danger" href="@url(repository)/remove/@encodeRefName(branch)/@pathList.mkString("/")">Delete</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if(content.viewType == "text"){
|
||||
@defining(isRenderable(pathList.reverse.head)){ isRrenderable =>
|
||||
@if(!isBlame && isRrenderable) {
|
||||
<div class="box-content-bottom markdown-body" style="padding-left: 20px; padding-right: 20px;">
|
||||
@renderMarkup(pathList, content.content.get, branch, repository, false, false, true)
|
||||
</div>
|
||||
} else {
|
||||
<div class="box-content-bottom">
|
||||
<pre class="prettyprint linenums blob @if(!isRrenderable){ no-renderable } ">@content.content.get</pre>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
@if(content.viewType == "image"){
|
||||
<div class="box-content-bottom">
|
||||
<img src="?raw=true"/>
|
||||
</div>
|
||||
}
|
||||
@if(content.viewType == "large" || content.viewType == "binary"){
|
||||
<div class="box-content-bottom" style="text-align: center; padding-top: 20px; padding-bottom: 20px;">
|
||||
<a href="?raw=true">View Raw</a><br>
|
||||
<br>
|
||||
(Sorry about that, but we can't show files that are this big right now)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
<script src="@assets/vendors/jquery/jquery.ba-hashchange.js"></script>
|
||||
@@ -111,36 +104,38 @@ $(window).load(function(){
|
||||
function updateSourceLineNum(){
|
||||
$('.source-line-num').remove();
|
||||
var pos = pre.find('ol.linenums').position();
|
||||
$('<div class="source-line-num">').css({
|
||||
height:pre.height(),
|
||||
width:'48px',
|
||||
cursor:'pointer',
|
||||
position: 'absolute',
|
||||
top : pos.top + 'px',
|
||||
left : pos.left + 'px'
|
||||
}).click(function(e){
|
||||
$(window).hashchange(function(){})
|
||||
var pos = $(this).data("pos");
|
||||
if(!pos){
|
||||
pos = $('ol.linenums li').map(function(){ return {id:$(this).attr("id"),top:$(this).position().top} }).toArray();
|
||||
$(this).data("pos",pos);
|
||||
}
|
||||
for(var i=0;i<pos.length-1;i++){
|
||||
if(pos[i+1].top>e.pageY){
|
||||
break;
|
||||
if(pos){
|
||||
$('<div class="source-line-num">').css({
|
||||
height : pre.height(),
|
||||
width : '48px',
|
||||
cursor : 'pointer',
|
||||
position: 'absolute',
|
||||
top : pos.top + 'px',
|
||||
left : pos.left + 'px'
|
||||
}).click(function(e){
|
||||
$(window).hashchange(function(){})
|
||||
var pos = $(this).data("pos");
|
||||
if(!pos){
|
||||
pos = $('ol.linenums li').map(function(){ return { id: $(this).attr("id"), top: $(this).position().top} }).toArray();
|
||||
$(this).data("pos",pos);
|
||||
}
|
||||
}
|
||||
var line = pos[i].id.replace(/^L/,'');
|
||||
var hash = location.hash;
|
||||
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
|
||||
var lines = hash.split('-');
|
||||
location.hash = lines[0] + '-L' + line;
|
||||
} else {
|
||||
var p = $("#L"+line).attr('id',"");
|
||||
location.hash = '#L' + line;
|
||||
p.attr('id','L'+line);
|
||||
}
|
||||
}).appendTo(pre);
|
||||
for(var i = 0; i < pos.length-1; i++){
|
||||
if(pos[i + 1].top > e.pageY){
|
||||
break;
|
||||
}
|
||||
}
|
||||
var line = pos[i].id.replace(/^L/,'');
|
||||
var hash = location.hash;
|
||||
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
|
||||
var lines = hash.split('-');
|
||||
location.hash = lines[0] + '-L' + line;
|
||||
} else {
|
||||
var p = $("#L"+line).attr('id',"");
|
||||
location.hash = '#L' + line;
|
||||
p.attr('id','L'+line);
|
||||
}
|
||||
}).appendTo(pre);
|
||||
}
|
||||
}
|
||||
var repository = $('.blame-action').data('repository');
|
||||
$('.blame-action').click(function(e){
|
||||
@@ -152,11 +147,11 @@ $(window).load(function(){
|
||||
});
|
||||
|
||||
function updateBlame(){
|
||||
var m = /^\/(blame|blob)(\/.*)$/.exec(location.href.substring(repository.length));
|
||||
var m = /\/(blame|blob)(\/.*)$/.exec(location.href);
|
||||
var mode = m[1];
|
||||
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1]=='blame'?'/blob':'/blame')+m[2]);
|
||||
if(pre.parents("td").find(".blame").length){
|
||||
pre.parents("div.container").toggleClass("blame-container", mode=='blame');
|
||||
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1] == 'blame' ? '/blob' : '/blame') + m[2]);
|
||||
if(pre.parents("div.box-content-bottom").find(".blame").length){
|
||||
pre.parents("div.container").toggleClass("blame-container", mode == 'blame');
|
||||
updateSourceLineNum();
|
||||
return;
|
||||
}
|
||||
@@ -164,47 +159,47 @@ $(window).load(function(){
|
||||
updateSourceLineNum();
|
||||
return;
|
||||
}
|
||||
$(document.body).toggleClass('no-box-shadow',document.body.style.boxShadow===undefined);
|
||||
$(document.body).toggleClass('no-box-shadow', document.body.style.boxShadow===undefined);
|
||||
$('.blame-action').addClass("active");
|
||||
var base = $('<div class="blame">').css({height:pre.height()}).prependTo(pre.parents("td")[0]);
|
||||
var base = $('<div class="blame">').css({height: pre.height()}).prependTo(pre.parents("div.box-content-bottom"));
|
||||
base.parents("div.container").addClass("blame-container");
|
||||
updateSourceLineNum();
|
||||
$.get($('.blame-action').data('url')).done(function(data){
|
||||
var blame = data.blame;
|
||||
var index = [];
|
||||
for(var i=0;i<blame.length;i++){
|
||||
for(var j=0;j<blame[i].lines.length;j++){
|
||||
index[blame[i].lines[j]]=blame[i];
|
||||
for(var i = 0; i < blame.length; i++){
|
||||
for(var j = 0; j < blame[i].lines.length; j++){
|
||||
index[blame[i].lines[j]] = blame[i];
|
||||
}
|
||||
}
|
||||
var blame, lastDiv, now=new Date().getTime();
|
||||
var blame, lastDiv, now = new Date().getTime();
|
||||
|
||||
$('pre.prettyprint ol.linenums li').each(function(i, e){
|
||||
var p=$(e).position();
|
||||
var h=$(e).height();
|
||||
var p = $(e).position();
|
||||
var h = $(e).height();
|
||||
if(blame == index[i]){
|
||||
lastDiv.css("min-height",(p.top+h+1) - lastDiv.position().top);
|
||||
lastDiv.css("min-height",(p.top + h + 1) - lastDiv.position().top);
|
||||
}else{
|
||||
$(e).addClass('blame-sep')
|
||||
blame = index[i];
|
||||
var sha = $('<div class="blame-sha">')
|
||||
.append($('<a>').attr("href",data.root+'/commit/'+blame.id).text(blame.id.substr(0,7)));
|
||||
.append($('<a>').attr("href", data.root + '/commit/' + blame.id).text(blame.id.substr(0,7)));
|
||||
if(blame.prev){
|
||||
sha.append($('<br />'))
|
||||
.append($('<a class="muted-link">').text('prev').attr("href",data.root+'/blame/'+blame.prev+'/'+(blame.prevPath||data.path)));
|
||||
.append($('<a class="muted-link">').text('prev').attr("href", data.root + '/blame/' + blame.prev + '/' + (blame.prevPath || data.path)));
|
||||
}
|
||||
lastDiv = $('<div class="blame-info">')
|
||||
.addClass('heat'+Math.min(10,Math.max(1,Math.ceil((now-blame.commited)/(24*3600*1000*70)))))
|
||||
.toggleClass('blame-last',blame.id==data.last)
|
||||
.addClass('heat' + Math.min(10, Math.max(1, Math.ceil((now - blame.commited) / (24 * 3600 * 1000 * 70)))))
|
||||
.toggleClass('blame-last', blame.id == data.last)
|
||||
.data('line', (i + 1))
|
||||
.css({
|
||||
"top" : p.top + 'px',
|
||||
"min-height" : h+'px'
|
||||
"top" : p.top + 'px',
|
||||
"min-height" : h + 'px'
|
||||
})
|
||||
.append(sha)
|
||||
.append($(blame.avatar).addClass('avatar').css({"float":"left"}))
|
||||
.append($(blame.avatar).addClass('avatar').css({"float": "left"}))
|
||||
.append($('<div class="blame-commit-title">').text(blame.message))
|
||||
.append($('<div class="muted">').html(blame.author+ " authed "+blame.authed))
|
||||
.append($('<div class="muted">').html(blame.author + " authed " + blame.authed))
|
||||
.appendTo(base);
|
||||
}
|
||||
});
|
||||
@@ -214,6 +209,8 @@ $(window).load(function(){
|
||||
updateBlame();
|
||||
});
|
||||
|
||||
var scrolling = false;
|
||||
|
||||
/**
|
||||
* Hightlight lines which are specified by URL hash.
|
||||
*/
|
||||
@@ -224,7 +221,7 @@ function updateHighlighting(){
|
||||
var lines = hash.substr(1).split('-');
|
||||
if(lines.length == 1){
|
||||
$('#' + lines[0]).addClass('highlight');
|
||||
if(!updateHighlighting.scrolling){
|
||||
if(!scrolling){
|
||||
$(window).scrollTop($('#' + lines[0]).offset().top - 40);
|
||||
}
|
||||
} else if(lines.length > 1){
|
||||
@@ -233,11 +230,11 @@ function updateHighlighting(){
|
||||
for(var i = start; i <= end; i++){
|
||||
$('#L' + i).addClass('highlight');
|
||||
}
|
||||
if(!updateHighlighting.scrolling){
|
||||
if(!scrolling){
|
||||
$(window).scrollTop($('#L' + start).offset().top - 40);
|
||||
}
|
||||
}
|
||||
updateHighlighting.scrolling = true;
|
||||
scrolling = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -35,7 +35,7 @@
|
||||
}.getOrElse {
|
||||
encodeRefName(repository.repository.defaultBranch)
|
||||
}}...@{encodeRefName(branch.name)}?expand=1" class="btn btn-small">New Pull Request</a>
|
||||
}else{
|
||||
} else {
|
||||
<a href="@url(repository)/compare/@{repository.repository.parentUserName.map { parent =>
|
||||
urlEncode(parent) + ":" + encodeRefName(repository.repository.defaultBranch)
|
||||
}.getOrElse {
|
||||
@@ -45,9 +45,9 @@
|
||||
}
|
||||
@if(hasWritePermission){
|
||||
@if(prs.map(!_._2.closed).getOrElse(false)){
|
||||
<a class="btn disabled btn-mini" data-toggle="tooltip" title="You can’t delete this branch because it has an open pull request"><i class="icon icon-trash icon-white"></i></a>
|
||||
<a class="btn disabled btn-mini" data-toggle="tooltip" title="You can’t delete this branch because it has an open pull request"><i class="octicon octicon-trashcan"></i></a>
|
||||
}else{
|
||||
<a href="@url(repository)/delete/@encodeRefName(branch.name)" class="btn @if(info.isMerged){ btn-warning }else{ btn-danger } delete-branch btn-mini" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="icon icon-trash icon-white"></i></a>
|
||||
<a href="@url(repository)/delete/@encodeRefName(branch.name)" class="btn @if(info.isMerged){ btn-warning }else{ btn-danger } delete-branch btn-mini" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="octicon octicon-trashcan"></i></a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
@html.menu("code", repository){
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>
|
||||
<th class="box-header">
|
||||
<div class="pull-right align-right">
|
||||
<a href="@url(repository)/tree/@commit.id" class="btn btn-small">Browse code</a>
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
@if(commit.isDifferentFromAuthor) {
|
||||
<div class="committer">
|
||||
<span class="icon-arrow-right"></span>
|
||||
<span class="octicon octicon-arrow-right"></span>
|
||||
<span>@user(commit.committerName, commit.committerEmailAddress, "username strong")</span>
|
||||
<span class="muted"> committed @helper.html.datetimeago(commit.commitTime)</span>
|
||||
</div>
|
||||
|
||||
@@ -10,72 +10,81 @@
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.menu("code", repository){
|
||||
<div class="head">
|
||||
@helper.html.branchcontrol(
|
||||
branch,
|
||||
repository,
|
||||
hasWritePermission
|
||||
){
|
||||
@repository.branchList.map { x =>
|
||||
<li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
|
||||
}
|
||||
}
|
||||
@if(pathList.isEmpty){
|
||||
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> / Commit History
|
||||
@helper.html.branchcontrol(
|
||||
branch,
|
||||
repository,
|
||||
hasWritePermission
|
||||
){
|
||||
@repository.branchList.map { x =>
|
||||
<li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if(pathList.nonEmpty){
|
||||
<span class="muted">History for</span>
|
||||
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
|
||||
<a class="strong" href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
|
||||
@pathList.zipWithIndex.map { case (section, i) =>
|
||||
@if(i == pathList.length - 1){
|
||||
@section
|
||||
<span class="strong">@section</span>
|
||||
} else {
|
||||
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
|
||||
<a class="strong" href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@commits.map { day =>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>@date(day.head.commitTime)</th>
|
||||
</tr>
|
||||
@day.map { commit =>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="pull-right align-right">
|
||||
<a href="@url(repository)/commit/@commit.id" class="btn btn-small monospace">@commit.id.substring(0, 10)</a><br>
|
||||
<a href="@url(repository)/tree/@commit.id" class="small">Browse code</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="commit-avatar-image">@avatar(commit, 40)</div>
|
||||
<div class="commit-message-box">
|
||||
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
|
||||
@if(commit.description.isDefined){
|
||||
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
|
||||
}
|
||||
<br>
|
||||
@if(commit.description.isDefined){
|
||||
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
|
||||
}
|
||||
<div class="small">
|
||||
@user(commit.authorName, commit.authorEmailAddress, "username")
|
||||
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
|
||||
@if(commit.isDifferentFromAuthor) {
|
||||
<span class="icon-arrow-right" style="margin-top : -2px ;"></span>
|
||||
@user(commit.committerName, commit.committerEmailAddress, "username")
|
||||
<span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
|
||||
<div class="commit-list">
|
||||
@commits.map { day =>
|
||||
<div class="muted" style="background-color: white;">
|
||||
<i class="octicon octicon-git-commit"></i> Commits on @date(day.head.commitTime)
|
||||
</div>
|
||||
<div class="box-commits">
|
||||
@day.map { commit =>
|
||||
<div class="box-content-row" style="padding: 8px;">
|
||||
<ul class="nav nav-pills-group pull-right" style="margin-top: 2px; margin-bottom: 0px; margin-right: 4px;">
|
||||
<li class="first"><a href="@url(repository)/commit/@commit.id" class="link monospace">@commit.id.substring(0, 7)</a></li>
|
||||
<li class="last"><a href="@url(repository)/tree/@commit.id" style="padding-top: 9px; padding-bottom: 10px;"><i class="octicon octicon-code link"></i></a></li>
|
||||
</ul>
|
||||
<div>
|
||||
<div class="commit-avatar-image">@avatar(commit, 40)</div>
|
||||
<div class="commit-message-box">
|
||||
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
|
||||
@if(commit.description.isDefined){
|
||||
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
|
||||
}
|
||||
<br>
|
||||
@if(commit.description.isDefined){
|
||||
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
|
||||
}
|
||||
<div class="small">
|
||||
@user(commit.authorName, commit.authorEmailAddress, "username")
|
||||
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
|
||||
@if(commit.isDifferentFromAuthor) {
|
||||
<span class="octicon octicon-arrow-right" style="margin-top : -2px;"></span>
|
||||
@user(commit.committerName, commit.committerEmailAddress, "username")
|
||||
<span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="pagination" style="text-align: center; margin-top: 30px">
|
||||
<ul>
|
||||
@if(page <= 1){
|
||||
<li class="disabled"><span>Newer</span></li>
|
||||
} else {
|
||||
<li><a href="?page=@{page - 1}">Newer</a></li>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
<div class="btn-group">
|
||||
<button class="btn" onclick="location.href='?page=@{page - 1}'"@if(page <= 1){ disabled="true"}>< Newer</button>
|
||||
<button class="btn" onclick="location.href='?page=@{page + 1}'"@if(!hasNext){ disabled="true"}>Older ></button>
|
||||
@if(!hasNext){
|
||||
<li class="disabled"><span>Older</span></li>
|
||||
} else {
|
||||
<li><a href="?page=@{page + 1}">Older</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ $(function(){
|
||||
enableTaskList : false
|
||||
}, function(data){
|
||||
$('#preview').empty().append(
|
||||
$('<div class="markdown-body" style="padding-left: 16px; padding-right: 16px;">').html(data));
|
||||
$('<div class="markdown-body" style="padding-left: 20px; padding-right: 20px;">').html(data));
|
||||
prettyPrint();
|
||||
});
|
||||
} else {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user