mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 00:27:36 +02:00
Compare commits
96 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 | ||
|
|
1fddc01f6e |
@@ -1,5 +1,7 @@
|
||||
# Guideline for Issues
|
||||
|
||||
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/takezoe/gitbucket) before raise an issue.
|
||||
- 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/takezoe/gitbucket_ja).
|
||||
- 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.
|
||||
|
||||
36
README.md
36
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
|
||||
--------
|
||||
@@ -20,12 +19,12 @@ The current version of GitBucket provides a basic features below:
|
||||
- Gravatar support
|
||||
- Plug-in system
|
||||
|
||||
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.
|
||||
|
||||
@@ -40,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
|
||||
@@ -65,7 +64,7 @@ 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
|
||||
|
||||
@@ -80,21 +79,38 @@ GitBucket has the plug-in system to extend GitBucket from outside of GitBucket.
|
||||
- [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/takezoe/gitbucket) before raise an issue.
|
||||
- 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/takezoe/gitbucket_ja).
|
||||
- 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.
|
||||
|
||||
@@ -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 [gitbucket.core.servlet.AutoUpdate](https://github.com/takezoe/gitbucket/blob/master/src/main/scala/gitbucket/core/servlet/AutoUpdate.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.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import sbtassembly.AssemblyKeys._
|
||||
object MyBuild extends Build {
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "3.7.0"
|
||||
val Version = "3.9.0"
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
|
||||
@@ -49,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",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.3",
|
||||
"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>
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -20,13 +20,21 @@ case class ApiCommit(
|
||||
removed: List[String],
|
||||
modified: List[String],
|
||||
author: ApiPersonIdent,
|
||||
committer: ApiPersonIdent)(repositoryName:RepositoryName) extends FieldSerializable{
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.util.JGitUtil
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import org.eclipse.jgit.diff.DiffEntry
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/activity/events/types/#pushevent
|
||||
*/
|
||||
case class ApiPushCommit(
|
||||
id: String,
|
||||
message: String,
|
||||
timestamp: Date,
|
||||
added: List[String],
|
||||
removed: List[String],
|
||||
modified: List[String],
|
||||
author: ApiPersonIdent,
|
||||
committer: ApiPersonIdent)(repositoryName:RepositoryName) extends FieldSerializable {
|
||||
val url = ApiPath(s"/${repositoryName.fullName}/commit/${id}")
|
||||
}
|
||||
|
||||
object ApiPushCommit{
|
||||
def apply(commit: ApiCommit, repositoryName: RepositoryName): ApiPushCommit = ApiPushCommit(
|
||||
id = commit.id,
|
||||
message = commit.message,
|
||||
timestamp = commit.timestamp,
|
||||
added = commit.added,
|
||||
removed = commit.removed,
|
||||
modified = commit.modified,
|
||||
author = commit.author,
|
||||
committer = commit.committer)(repositoryName)
|
||||
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiPushCommit =
|
||||
ApiPushCommit(ApiCommit(git, repositoryName, commit), repositoryName)
|
||||
}
|
||||
@@ -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_count = watchers
|
||||
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
||||
val url = if(urlIsHtmlUrl){
|
||||
ApiPath(s"/${full_name}")
|
||||
}else{
|
||||
ApiPath(s"/api/v3/repos/${full_name}")
|
||||
}
|
||||
val http_url = ApiPath(s"/git/${full_name}.git")
|
||||
val 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("-")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -332,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
|
||||
@@ -352,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
|
||||
|
||||
@@ -294,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
|
||||
@@ -436,8 +439,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
// call web hook
|
||||
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// notifications
|
||||
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}")
|
||||
}
|
||||
@@ -447,6 +453,19 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
// TODO Same method exists in IssueController. Should it moved to IssueService?
|
||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
||||
if(getIssue(owner, repository, issueId).isDefined){
|
||||
// Not add if refer comment already exist.
|
||||
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
@@ -32,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.
|
||||
@@ -40,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)
|
||||
@@ -293,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,
|
||||
@@ -366,7 +370,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
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)
|
||||
@@ -391,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 =>
|
||||
@@ -663,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -92,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)]] {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
commits: List[ApiPushCommit],
|
||||
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 => ApiPushCommit(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))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -276,4 +370,38 @@ object WebHookService {
|
||||
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,8 @@ 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)
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -7,13 +7,31 @@ 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,
|
||||
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 username/project@SHA to link
|
||||
@@ -26,10 +44,12 @@ trait LinkConverter { self: RequestCache =>
|
||||
// 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)}""")
|
||||
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)}""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +63,12 @@ trait LinkConverter { self: RequestCache =>
|
||||
// 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)}""")
|
||||
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)}""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +76,12 @@ trait LinkConverter { self: RequestCache =>
|
||||
.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)}""")
|
||||
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)}""")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ object Markdown {
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -110,7 +111,7 @@ object Markdown {
|
||||
}
|
||||
|
||||
override def link(href: String, title: String, text: String): String = {
|
||||
super.link(fixUrl(href, true), title, text)
|
||||
super.link(fixUrl(href, false), title, text)
|
||||
}
|
||||
|
||||
override def image(href: String, title: String, text: String): String = {
|
||||
|
||||
@@ -93,6 +93,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
pages: List[String] = Nil)(implicit context: Context): Html =
|
||||
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){
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@plugins.map {plugin =>
|
||||
<div class="box">
|
||||
<div class="box-header">@plugin.pluginName</div>
|
||||
<div class="box-content">
|
||||
<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>
|
||||
|
||||
@@ -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 -->
|
||||
<!--====================================================================-->
|
||||
@@ -289,7 +289,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Enable notification not only SMTP configuration if you want to send nitification email.
|
||||
Enable notification not only SMTP configuration if you want to send notification email.
|
||||
</p>
|
||||
</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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@import gitbucket.core._
|
||||
@import gitbucket.core.view.helpers._
|
||||
<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"}>
|
||||
@@ -15,9 +16,9 @@
|
||||
@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
|
||||
}
|
||||
@@ -32,7 +33,7 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="box-content-bottom 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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
} else {
|
||||
<div class="box-content-bottom" style="padding: 0px;">
|
||||
@defining(3){ max =>
|
||||
@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)
|
||||
|
||||
@@ -18,14 +18,15 @@
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "",
|
||||
elastic = true
|
||||
elastic = true,
|
||||
tabIndex = 1
|
||||
)
|
||||
<div style="text-align: 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"/>
|
||||
<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>
|
||||
|
||||
@@ -46,19 +46,19 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="box-content-bottom 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>
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<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="margin-bottom: 10px;" class="box-content">
|
||||
<table class="fill-width">
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
@if(content.viewType == "text"){
|
||||
@defining(isRenderable(pathList.reverse.head)){ isRrenderable =>
|
||||
@if(!isBlame && isRrenderable) {
|
||||
<div class="box-content-bottom markdown-body" style="padding-left: 16px; padding-right: 16px;">
|
||||
<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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
@readme.map { case(filePath, content) =>
|
||||
<div id="readme">
|
||||
<div class="box-header">@filePath.reverse.head</div>
|
||||
<div class="box-content-bottom markdown-body" style="padding-left: 16px; padding-right: 16px;">@renderMarkup(filePath, content, branch, repository, false, false, true)</div>
|
||||
<div class="box-content-bottom markdown-body" style="padding-left: 20px; padding-right: 20px;">@renderMarkup(filePath, content, branch, repository, false, false, true)</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@menu("danger", repository){
|
||||
<div class="box">
|
||||
<div class="box-header">Danger Zone</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-bottom">
|
||||
<form id="transfer-form" method="post" action="@url(repository)/settings/transfer" validate="true" autocomplete="off">
|
||||
<fieldset>
|
||||
<label class="strong">Transfer Ownership</label>
|
||||
|
||||
164
src/main/twirl/gitbucket/core/settings/edithooks.scala.html
Normal file
164
src/main/twirl/gitbucket/core/settings/edithooks.scala.html
Normal file
@@ -0,0 +1,164 @@
|
||||
@(webHook: gitbucket.core.model.WebHook,
|
||||
events: Set[gitbucket.core.model.WebHook.Event],
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
info: Option[Any],
|
||||
create: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@import gitbucket.core.model.WebHook._
|
||||
@check(name: String, event: Event)={
|
||||
name="@(name).@event.name" value="on" @if(events(event)){checked}
|
||||
}
|
||||
@html.main("Settings", Some(repository)){
|
||||
@html.menu("settings", repository){
|
||||
@menu("hooks", repository){
|
||||
@helper.html.information(info)
|
||||
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
Webhook / Manage webhook
|
||||
</div>
|
||||
<div class="box-content-bottom">
|
||||
<form method="POST" validate="true">
|
||||
<div>
|
||||
<span class="error" id="error-url"></span>
|
||||
</div>
|
||||
<label class="strong">Payload URL</label>
|
||||
@if(create){
|
||||
<input type="text" name="url" id="url" value="@webHook.url" class="input-xxlarge" style="margin-bottom: 0px;" required />
|
||||
} else {
|
||||
<input type="text" value="@webHook.url" style="margin-bottom: 0px;" class="input-xxlarge" disabled />
|
||||
<input type="hidden" value="@webHook.url" name="url" />
|
||||
}
|
||||
<button class="btn" id="test">Test Hook</button>
|
||||
|
||||
<hr />
|
||||
<div>
|
||||
<span class="error" id="error-events"></span>
|
||||
</div>
|
||||
<label class="strong">Which events would you like to trigger this webhook?</label>
|
||||
<!--
|
||||
<label class="checkbox"><input type="checkbox" @check("events",CommitComment) />Commit comment <small class="help-block">Commit or diff commented on. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Create) />Create <small class="help-block">Branch, or tag created. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Delete) />Delete <small class="help-block">Branch, or tag deleted. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Deployment) />Deployment <small class="help-block">Repository deployed. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",DeploymentStatus) />Deployment status <small class="help-block">Deployment status updated from the API. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Fork) />Fork <small class="help-block">Repository forked. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Gollum) />Gollum <small class="help-block">Wiki page updated. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Member) />Member <small class="help-block">Collaborator added to a non-organization repository. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",PageBuild) />Page build <small class="help-block">Pages site built. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Public) />Public <small class="help-block">Repository changes from private to public. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Release) />Release <small class="help-block">Release published in a repository. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",TeamAdd) />Team add <small class="help-block">Team added or modified on a repository. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Watch) />Watch <small class="help-block">User stars a repository.</small></label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Status) />Status <small class="help-block">Commit status updated from the API. </small> </label>
|
||||
-->
|
||||
<label class="checkbox"><input type="checkbox" @check("events",IssueComment) />Issue comment <small class="help-block">Issue commented on. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Issues) />Issues <small class="help-block">Issue opened, closed<!-- , assigned, or labeled -->. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",PullRequest) />Pull Request <small class="help-block">Pull Request opened, closed<!-- , assigned, labeled -->, or synchronized. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",PullRequestReviewComment) />Pull Request review comment <small class="help-block">Pull Request diff commented on. </small> </label>
|
||||
<label class="checkbox"><input type="checkbox" @check("events",Push) />Push <small class="help-block">Git push to a repository. </small> </label>
|
||||
<hr />
|
||||
@if(!create){
|
||||
<input type="submit" class="btn btn-success" value="Update webhook" formaction="@url(repository)/settings/hooks/edit/@urlEncode(webHook.url)" />
|
||||
<a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="btn text-error" onclick="return confirm('delete webhook for @webHook.url ?')">
|
||||
Delete webhook
|
||||
</a>
|
||||
} else {
|
||||
<input type="submit" class="btn" value="Add webhook" formaction="@url(repository)/settings/hooks/new" />
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal hide" id="test-report-modal">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>WebHook Test</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>request to <span id="test-modal-url" style="word-break: break-all; word-wrap: break-word; white-space: pre; white-space: pre-wrap;"></span></p>
|
||||
<div id="test-report" style="display:none">
|
||||
<ul class="nav nav-tabs" id="test-report-tab">
|
||||
<li class="active"><a href="#REQUEST">REQUEST</a></li>
|
||||
<li><a href="#RESPONCE">RESPONCE <span class="badge badge-success" id="res-status"></span></a></li>
|
||||
</ul>
|
||||
<div class="tab-content" style="max-height: 300px;">
|
||||
<div class="tab-pane active" id="REQUEST">
|
||||
<div id="req-errors" class="alert alert-error">
|
||||
ERROR<span id="req-errors-body"></span>
|
||||
</div>
|
||||
<div id="req-success" style="display:none">
|
||||
Headers
|
||||
<pre id="req-headers"></pre>
|
||||
Payload
|
||||
<pre id="req-payload"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="RESPONCE">
|
||||
<div id="res-errors" class="alert alert-error">
|
||||
ERROR<span id="res-errors-body"></span>
|
||||
</div>
|
||||
<div id="res-success" style="display:none">
|
||||
Headers
|
||||
<pre id="res-headers"></pre>
|
||||
Body
|
||||
<pre id="res-body"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#test-report-tab a').click(function (e) {
|
||||
e.preventDefault();
|
||||
$(this).tab('show');
|
||||
});
|
||||
$('#test').click(function(e){
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
var url = this.form.url.value;
|
||||
if(!/^https?:\/\/.+/.test(url)){
|
||||
alert("invalid url");
|
||||
return;
|
||||
}
|
||||
$("#test-modal-url").text(url)
|
||||
$("#test-report-modal").modal('show')
|
||||
$("#test-report").hide();
|
||||
$.ajax({
|
||||
method:'POST',
|
||||
url:'@url(repository)/settings/hooks/test?url=' + encodeURIComponent(url),
|
||||
success: function(e){
|
||||
//console.log(e);
|
||||
$('#test-report-tab a:first').tab('show');
|
||||
$("#test-report").show();
|
||||
$("#req-success").toggle(e.request&&!e.request.error);
|
||||
$("#req-errors").toggle(e.request&&!!e.request.error);
|
||||
$("#req-errors-body").text(e.request.error);
|
||||
function headers(h){
|
||||
h = h["headers"];
|
||||
return h ? $.map(h, function(h){
|
||||
return $("<div>").append($('<b>').text(h[0] + ":"),$('<span>').text(" " + h[1]))
|
||||
}):"";
|
||||
}
|
||||
$("#req-headers").html(headers(e.request));
|
||||
$("#req-payload").text(e.request && e.request.payload ? JSON.stringify(JSON.parse(e.request.payload),undefined,4) : "");
|
||||
$("#res-success").toggle(e.responce && !e.responce.error);
|
||||
$("#res-errors").toggle(e.responce && !!e.responce.error);
|
||||
$("#res-errors-body").text(e.responce.error);
|
||||
var success = !!(e.responce && e.responce.status && /^2\d\d$/.test(e.responce.status.statusCode));
|
||||
$("#res-status").text((e.responce && e.responce.status && e.responce.status.statusCode) || "ERROR");
|
||||
$("#res-status").toggleClass("badge-success", success).toggleClass("badge-important", !success);
|
||||
$("#res-headers").html(headers(e.responce));
|
||||
$("#res-body").text(e.responce && e.responce.body ? e.responce.body : "");
|
||||
},
|
||||
});
|
||||
return false;
|
||||
});
|
||||
})
|
||||
</script>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
@(webHooks: List[gitbucket.core.model.WebHook],
|
||||
enteredUrl: Option[Any],
|
||||
@(webHooks: List[(gitbucket.core.model.WebHook, Set[gitbucket.core.model.WebHook.Event])],
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@@ -8,20 +7,42 @@
|
||||
@html.menu("settings", repository){
|
||||
@menu("hooks", repository){
|
||||
@helper.html.information(info)
|
||||
<h3>WebHook URLs</h3>
|
||||
<ul>
|
||||
@webHooks.map { webHook =>
|
||||
<li>@webHook.url <a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="remove">(remove)</a></li>
|
||||
}
|
||||
</ul>
|
||||
<form method="POST" validate="true">
|
||||
<div>
|
||||
<span class="error" id="error-url"></span>
|
||||
</div>
|
||||
<input type="text" name="url" id="url" value="@enteredUrl" style="width: 300px; margin-bottom: 0px;"/>
|
||||
<input type="submit" class="btn" formaction="@url(repository)/settings/hooks/add" value="Add"/>
|
||||
<input type="submit" class="btn" formaction="@url(repository)/settings/hooks/test" value="Test Hook"/>
|
||||
</form>
|
||||
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<a href="@url(repository)/settings/hooks/new" class="btn btn-mini pull-right">Add webhook</a>
|
||||
Webhooks
|
||||
</div>
|
||||
<div class="box-content-bottom">
|
||||
<p>
|
||||
Webhooks allow external services to be notified when certain events happen within your repository.
|
||||
When the specified events happen, we’ll send a POST request to each of the URLs you provide.
|
||||
Learn more in <a href="https://github.com/takezoe/gitbucket/wiki/API-WebHook" target="_blank">GitBucket Wiki Webhook Page</a>.
|
||||
</p>
|
||||
|
||||
<table class="table table-condensed" style="margin-bottom:0">
|
||||
@webHooks.map { case (webHook, events) =>
|
||||
<tr><td style="vertical-align: middle;">
|
||||
<a href="@url(repository)/settings/hooks/edit/@urlEncode(webHook.url)" class="css-truncate" style="max-width:360px">
|
||||
<span class="css-truncate-target">@webHook.url</span>
|
||||
</a>
|
||||
<em class="css-truncate" style="max-width: 225px;">(<span class="css-truncate-target">@events.map(_.name).mkString(", ")</span>)</em>
|
||||
</td><td>
|
||||
<div class="btn-group pull-right">
|
||||
<a href="@url(repository)/settings/hooks/edit/@urlEncode(webHook.url)" class="btn btn-small">
|
||||
<span class="octicon octicon-pencil"></span>
|
||||
</a>
|
||||
<a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="btn btn-small btn-danger" onclick="return confirm('delete webhook for @webHook.url ?')">
|
||||
<span class="octicon octicon-x"></span>
|
||||
</a>
|
||||
</div>
|
||||
</td></tr>
|
||||
}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<form id="form" method="post" action="@url(repository)/settings/options" validate="true">
|
||||
<div class="box">
|
||||
<div class="box-header">Settings</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-bottom">
|
||||
<fieldset>
|
||||
<label for="repositoryName" class="strong">Repository Name:</label>
|
||||
<input type="text" name="repositoryName" id="repositoryName" value="@repository.name"/>
|
||||
|
||||
@@ -64,6 +64,11 @@ h6 {
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
/* Global Header */
|
||||
/* ======================================================================== */
|
||||
@@ -251,6 +256,10 @@ div.box-header {
|
||||
background: #f2f8fa;
|
||||
}
|
||||
|
||||
.inline-comment div.box-content-bottom {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.commit-list {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1525,7 +1534,7 @@ div.markdown-body li {
|
||||
|
||||
div.markdown-body p {
|
||||
margin: 15px 0;
|
||||
line-height: 1.7;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
div.markdown-body pre {
|
||||
|
||||
@@ -23,18 +23,9 @@ $(function(){
|
||||
$(e.target).children('a.markdown-anchor-link').show();
|
||||
});
|
||||
$('.markdown-head').mouseleave(function(e){
|
||||
var anchorLink = $(e.target).children('a.markdown-anchor-link');
|
||||
if(anchorLink.data('active') !== true){
|
||||
anchorLink.hide();
|
||||
}
|
||||
$(e.target).children('a.markdown-anchor-link').hide();
|
||||
});
|
||||
|
||||
$('a.markdown-anchor-link').mouseenter(function(e){
|
||||
$(e.target).data('active', true);
|
||||
});
|
||||
|
||||
$('a.markdown-anchor-link').mouseleave(function(e){
|
||||
$(e.target).data('active', false);
|
||||
$(e.target).hide();
|
||||
});
|
||||
|
||||
@@ -360,12 +351,12 @@ function scrollIntoView(target){
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* escape html
|
||||
*/
|
||||
function escapeHtml(text){
|
||||
return text.replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"').replace(/>/g,'>');
|
||||
}
|
||||
///**
|
||||
// * escape html
|
||||
// */
|
||||
//function escapeHtml(text){
|
||||
// return text.replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"').replace(/>/g,'>');
|
||||
//}
|
||||
|
||||
/**
|
||||
* calculate string ranking for path.
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.json4s.jackson.JsonMethods.{pretty, parse}
|
||||
import org.json4s._
|
||||
import org.specs2.matcher._
|
||||
|
||||
import java.util.{Calendar, TimeZone}
|
||||
import java.util.{Calendar, TimeZone, Date}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ class JsonFormatSpec extends Specification {
|
||||
d.set(2011,3,14,16,0,49)
|
||||
d.getTime
|
||||
}
|
||||
def date(date:String): Date = {
|
||||
val f = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
f.setTimeZone(TimeZone.getTimeZone("UTC"))
|
||||
f.parse(date)
|
||||
}
|
||||
val sha1 = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
|
||||
val repo1Name = RepositoryName("octocat/Hello-World")
|
||||
implicit val context = JsonFormat.Context("http://gitbucket.exmple.com")
|
||||
@@ -45,7 +50,7 @@ class JsonFormatSpec extends Specification {
|
||||
forks = 0,
|
||||
`private` = false,
|
||||
default_branch = "master",
|
||||
owner = apiUser)
|
||||
owner = apiUser)(urlIsHtmlUrl = false)
|
||||
val repositoryJson = s"""{
|
||||
"name" : "Hello-World",
|
||||
"full_name" : "octocat/Hello-World",
|
||||
@@ -85,7 +90,7 @@ class JsonFormatSpec extends Specification {
|
||||
"url": "http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/statuses"
|
||||
}"""
|
||||
|
||||
val apiPushCommit = ApiPushCommit(
|
||||
val apiPushCommit = ApiCommit(
|
||||
id = "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
message = "Update README.md",
|
||||
timestamp = date1,
|
||||
@@ -93,7 +98,7 @@ class JsonFormatSpec extends Specification {
|
||||
removed = Nil,
|
||||
modified = List("README.md"),
|
||||
author = ApiPersonIdent("baxterthehacker","baxterthehacker@users.noreply.github.com",date1),
|
||||
committer = ApiPersonIdent("baxterthehacker","baxterthehacker@users.noreply.github.com",date1))(RepositoryName("baxterthehacker", "public-repo"))
|
||||
committer = ApiPersonIdent("baxterthehacker","baxterthehacker@users.noreply.github.com",date1))(RepositoryName("baxterthehacker", "public-repo"), true)
|
||||
val apiPushCommitJson = s"""{
|
||||
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
// "distinct": true,
|
||||
@@ -305,6 +310,51 @@ class JsonFormatSpec extends Specification {
|
||||
// "deletions": 3,
|
||||
// "changed_files": 5
|
||||
}"""
|
||||
|
||||
// https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
|
||||
val apiPullRequestReviewComment = ApiPullRequestReviewComment(
|
||||
id = 29724692,
|
||||
// "diff_hunk": "@@ -1 +1 @@\n-# public-repo",
|
||||
path = "README.md",
|
||||
// "position": 1,
|
||||
// "original_position": 1,
|
||||
commit_id = "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
// "original_commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
user = apiUser,
|
||||
body = "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("baxterthehacker/public-repo"), 1)
|
||||
val apiPullRequestReviewCommentJson = s"""{
|
||||
"url": "http://gitbucket.exmple.com/api/v3/repos/baxterthehacker/public-repo/pulls/comments/29724692",
|
||||
"id": 29724692,
|
||||
// "diff_hunk": "@@ -1 +1 @@\\n-# public-repo",
|
||||
"path": "README.md",
|
||||
// "position": 1,
|
||||
// "original_position": 1,
|
||||
"commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
// "original_commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
"user": $apiUserJson,
|
||||
"body": "Maybe you should use more emojji on this line.",
|
||||
"created_at": "2015-05-05T23:40:27Z",
|
||||
"updated_at": "2015-05-05T23:40:27Z",
|
||||
"html_url": "http://gitbucket.exmple.com/baxterthehacker/public-repo/pull/1#discussion_r29724692",
|
||||
"pull_request_url": "http://gitbucket.exmple.com/api/v3/repos/baxterthehacker/public-repo/pulls/1",
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "http://gitbucket.exmple.com/api/v3/repos/baxterthehacker/public-repo/pulls/comments/29724692"
|
||||
},
|
||||
"html": {
|
||||
"href": "http://gitbucket.exmple.com/baxterthehacker/public-repo/pull/1#discussion_r29724692"
|
||||
},
|
||||
"pull_request": {
|
||||
"href": "http://gitbucket.exmple.com/api/v3/repos/baxterthehacker/public-repo/pulls/1"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
|
||||
def beFormatted(json2Arg:String) = new Matcher[String] {
|
||||
def apply[S <: String](e: Expectable[S]) = {
|
||||
import java.util.regex.Pattern
|
||||
@@ -358,5 +408,8 @@ class JsonFormatSpec extends Specification {
|
||||
"apiPullRequest" in {
|
||||
JsonFormat(apiPullRequest) must beFormatted(apiPullRequestJson)
|
||||
}
|
||||
"apiPullRequestReviewComment" in {
|
||||
JsonFormat(apiPullRequestReviewComment) must beFormatted(apiPullRequestReviewCommentJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import org.specs2.mutable.Specification
|
||||
import gitbucket.core.model.WebHook
|
||||
|
||||
|
||||
class WebHookServiceSpec extends Specification with ServiceSpecBase {
|
||||
@@ -16,12 +17,12 @@ class WebHookServiceSpec extends Specification with ServiceSpecBase {
|
||||
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root")
|
||||
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root")
|
||||
generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2")
|
||||
service.addWebHookURL("user1", "repo1", "webhook1-1")
|
||||
service.addWebHookURL("user1", "repo1", "webhook1-2")
|
||||
service.addWebHookURL("user2", "repo2", "webhook2-1")
|
||||
service.addWebHookURL("user2", "repo2", "webhook2-2")
|
||||
service.addWebHookURL("user3", "repo3", "webhook3-1")
|
||||
service.addWebHookURL("user3", "repo3", "webhook3-2")
|
||||
service.addWebHook("user1", "repo1", "webhook1-1", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user1", "repo1", "webhook1-2", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user2", "repo2", "webhook2-1", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user2", "repo2", "webhook2-2", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user3", "repo3", "webhook3-1", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user3", "repo3", "webhook3-2", Set(WebHook.PullRequest))
|
||||
|
||||
service.getPullRequestsByRequestForWebhook("user1","repo1","master1") must_== Map.empty
|
||||
|
||||
@@ -41,4 +42,35 @@ class WebHookServiceSpec extends Specification with ServiceSpecBase {
|
||||
r2((issue32, issueUser, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
} }
|
||||
}
|
||||
|
||||
"add and get and update and delete" in { withTestDB { implicit session =>
|
||||
val user1 = generateNewUserWithDBRepository("user1","repo1")
|
||||
service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest))
|
||||
service.getWebHooks("user1", "repo1") must_== List((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest)))
|
||||
service.getWebHook("user1", "repo1", "http://example.com") must_== Some((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest)))
|
||||
service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) must_== List((WebHook("user1","repo1","http://example.com")))
|
||||
service.getWebHooksByEvent("user1", "repo1", WebHook.Push) must_== Nil
|
||||
service.getWebHook("user1", "repo1", "http://example.com2") must_== None
|
||||
service.getWebHook("user2", "repo1", "http://example.com") must_== None
|
||||
service.getWebHook("user1", "repo2", "http://example.com") must_== None
|
||||
service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues))
|
||||
service.getWebHook("user1", "repo1", "http://example.com") must_== Some((WebHook("user1","repo1","http://example.com"),Set(WebHook.Push, WebHook.Issues)))
|
||||
service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) must_== Nil
|
||||
service.getWebHooksByEvent("user1", "repo1", WebHook.Push) must_== List((WebHook("user1","repo1","http://example.com")))
|
||||
service.deleteWebHook("user1", "repo1", "http://example.com")
|
||||
service.getWebHook("user1", "repo1", "http://example.com") must_== None
|
||||
} }
|
||||
"getWebHooks, getWebHooksByEvent" in { withTestDB { implicit session =>
|
||||
val user1 = generateNewUserWithDBRepository("user1","repo1")
|
||||
service.addWebHook("user1", "repo1", "http://example.com/1", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push))
|
||||
service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push))
|
||||
service.getWebHooks("user1", "repo1") must_== List(
|
||||
WebHook("user1","repo1","http://example.com/1")->Set(WebHook.PullRequest),
|
||||
WebHook("user1","repo1","http://example.com/2")->Set(WebHook.Push),
|
||||
WebHook("user1","repo1","http://example.com/3")->Set(WebHook.PullRequest,WebHook.Push))
|
||||
service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) must_== List(
|
||||
WebHook("user1","repo1","http://example.com/1"),
|
||||
WebHook("user1","repo1","http://example.com/3"))
|
||||
} }
|
||||
}
|
||||
38
src/test/scala/gitbucket/core/view/HelpersSpec.scala
Normal file
38
src/test/scala/gitbucket/core/view/HelpersSpec.scala
Normal file
@@ -0,0 +1,38 @@
|
||||
package gitbucket.core.view
|
||||
|
||||
import org.specs2.mutable._
|
||||
|
||||
class HelpersSpec extends Specification {
|
||||
|
||||
import helpers._
|
||||
|
||||
"detect and render links" should {
|
||||
|
||||
"pass identical string when no link is present" in {
|
||||
val before = "Description"
|
||||
val after = detectAndRenderLinks(before).toString()
|
||||
after mustEqual before
|
||||
}
|
||||
|
||||
"convert a single link" in {
|
||||
val before = "http://example.com"
|
||||
val after = detectAndRenderLinks(before).toString()
|
||||
after mustEqual """<a href="http://example.com">http://example.com</a>"""
|
||||
}
|
||||
|
||||
"convert a single link within trailing text" in {
|
||||
val before = "Example Project. http://example.com"
|
||||
val after = detectAndRenderLinks(before).toString()
|
||||
after mustEqual """Example Project. <a href="http://example.com">http://example.com</a>"""
|
||||
}
|
||||
|
||||
"convert a mulitple links within text" in {
|
||||
val before = "Example Project. http://example.com. (See also https://github.com/)"
|
||||
val after = detectAndRenderLinks(before).toString()
|
||||
after mustEqual """Example Project. <a href="http://example.com">http://example.com</a>. (See also <a href="https://github.com/">https://github.com/</a>)"""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user