Compare commits

...

51 Commits
3.7 ... 3.8

Author SHA1 Message Date
Naoki Takezoe
97bd324cb0 Update README.md for GitBucket 3.8 release 2015-10-31 11:53:06 +09:00
Naoki Takezoe
2738406710 Update version number to 3.8 2015-10-31 11:07:02 +09:00
Naoki Takezoe
8e78345cbe Merge pull request #939 from nus/feature/upload-pdf
Support uploading PDF files
2015-10-31 11:05:41 +09:00
Naoki Takezoe
bd9e064137 Merge pull request #925 from team-lab/feature/add-compare-link-for-webhook-push
Feature/add properties for webhook push
2015-10-31 02:57:24 +09:00
Naoki Takezoe
eb49365bcb Merge pull request #960 from arteria/master
Creating repositories over API
2015-10-30 11:38:31 +09:00
Naoki Takezoe
de92e28c7a Adjust whitespace 2015-10-29 01:09:07 +09:00
Naoki Takezoe
c18f8fd87b Merge pull request #961 from superhj1987/master
Update PullRequestsController.scala
2015-10-29 01:06:06 +09:00
Naoki Takezoe
34272cb4c4 Update README.md 2015-10-29 01:00:41 +09:00
Naoki Takezoe
b3f8a02494 Update README.md 2015-10-29 00:59:47 +09:00
hangjian
f767a55350 Merge branch 'master' of github.com:gitbucket/gitbucket 2015-10-28 09:32:44 +08:00
Bryant Hang
9b2c3848d9 Update PullRequestsController.scala
when the forked repository is the original repository(forkedRepository.repository.originRepositoryName&originUserName is None),then 404 will occur,so add the if to solve it.
2015-10-27 23:09:08 +08:00
Jannis Vamvas
e34d016581 Change group repository creation API endpoint to /orgs/:org/repos 2015-10-27 10:27:47 +01:00
Jannis Vamvas
b64b447b42 Extend API to allow creating repositories 2015-10-26 14:30:49 +01:00
Yota Ichino
c6a4c13394 Change messages about file types in the comment form. 2015-10-25 22:56:31 +09:00
Yota Ichino
c8822cb4ca Write a Content-Disposition header for attach files. 2015-10-25 22:37:13 +09:00
Yota Ichino
c05e7218f3 Add document file formats for upload
The following extension is added.
- .docx
- .pptx
- .txt
- .xlsx
2015-10-25 22:03:16 +09:00
Naoki Takezoe
01872d3440 Fix order of updating pull request when repository is renamed 2015-10-24 23:31:37 +09:00
Naoki Takezoe
91bbb3e4dc Update description about support 2015-10-24 20:44:38 +09:00
Naoki Takezoe
80ebd9fb0e (refs #945)Remove ?raw=true from url other than images 2015-10-18 23:04:47 +09:00
Naoki Takezoe
2ab217251a Remove unnecessary TODO 2015-10-18 19:01:04 +09:00
Naoki Takezoe
2b20f6c74c (refs #946)Insert refer comment when pull request is created 2015-10-18 18:57:23 +09:00
Naoki Takezoe
042f855cd5 (refs #947)Fix referenced link from pull request 2015-10-18 18:06:03 +09:00
Naoki Takezoe
720ab7e0a3 Update url in docs 2015-10-17 16:17:43 +09:00
Naoki Takezoe
4b1b100aa5 Update README.md 2015-10-17 02:16:13 +09:00
Naoki Takezoe
541b7e2a79 Merge pull request #944 from lefou/t943-url-in-repo-desc
Detect links and render them as HTML links in repo description
2015-10-15 09:20:31 +09:00
Tobias Roeser
00cd3adc7b Refined regex and removed explicit TLDs
Also made regex val private.
2015-10-14 22:42:35 +02:00
Tobias Roeser
5a97a518a6 Detect links and render them as HTML links in repo description 2015-10-14 14:41:09 +02:00
Naoki Takezoe
d7219068cd (refs #907)Omit diffs if updated files are 100 over 2015-10-14 02:26:59 +09:00
Naoki Takezoe
a79c07f095 Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-10-14 02:24:43 +09:00
Naoki Takezoe
9f27f70c87 (refs #907)Omit the compare view for large diffs 2015-10-14 02:24:38 +09:00
Naoki Takezoe
8fa79db368 Merge pull request #940 from team-lab/fix/933-download-large-file
(ref #933) fix/Unable to download large file
2015-10-14 00:11:29 +09:00
nazoking
a1efa60741 fix return value 2015-10-13 18:57:20 +09:00
Naoki Takezoe
6166eb3743 Merge pull request #905 from kanmi/ssh-host-key
Specify option to generate an RSA host key
2015-10-13 00:16:50 +09:00
nazoking
5194fc5f15 (ref #933) fix/Unable to download large file (fix method name) 2015-10-12 23:25:07 +09:00
nazoking
1a97beb8cf (ref #933) fix/Unable to download large file 2015-10-12 21:58:56 +09:00
Yota Ichino
99d23398ad Change the upload form for PDF files 2015-10-12 21:45:16 +09:00
Yota Ichino
6accdefb8c Change a path for uploading a file 2015-10-12 21:44:58 +09:00
Yota Ichino
98fc64deaa Enable to upload a PDF file 2015-10-12 21:44:41 +09:00
Naoki Takezoe
30a8cefc37 Merge pull request #938 from nus/change-place-of-buttons
Change place of buttons
2015-10-12 17:31:04 +09:00
Naoki Takezoe
9d47c3ccb3 Bump markedj to 1.0.4-SNAPSHOT 2015-10-12 16:52:00 +09:00
Yota Ichino
5a1ab8d485 Set tabindex for the comment form. 2015-10-12 16:41:15 +09:00
Yota Ichino
5d526f243e Change place of Comment button and Close button. 2015-10-12 15:52:47 +09:00
nazoking
d13bb47ee7 remove ApiPushCommit class (merge to ApiCommit) 2015-10-06 02:27:42 +09:00
nazoking
8b8c6ee861 fix repository.url on webhook push payload 2015-10-06 02:15:03 +09:00
nazoking
0a2d95e434 add head_commit on webhook push payload 2015-10-06 02:14:02 +09:00
nazoking
fdd119c477 add compare, after and before property on webhook push payload 2015-10-06 01:39:19 +09:00
Naoki Takezoe
4f94ca1384 Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-10-04 13:17:13 +09:00
Naoki Takezoe
ed21ee8bdb Improve header anchor behavior 2015-10-04 13:17:07 +09:00
Naoki Takezoe
efd257dee0 Update README.md 2015-10-03 19:07:46 +09:00
Naoki Takezoe
bacf391a39 (refs #867)Providing checksum has started since 3.7 2015-10-03 14:26:36 +09:00
kanmi
1fddc01f6e Specify option to generate an RSA key 2015-09-16 01:28:31 +09:00
40 changed files with 532 additions and 235 deletions

View File

@@ -1,5 +1,7 @@
# Guideline for Issues # 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. - 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.

View File

@@ -1,4 +1,4 @@
GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://travis-ci.org/takezoe/gitbucket.svg?branch=master)](https://travis-ci.org/takezoe/gitbucket) GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.png)](https://gitter.im/gitbucket/gitbucket) [![Build Status](https://travis-ci.org/gitbucket/gitbucket.svg?branch=master)](https://travis-ci.org/gitbucket/gitbucket)
========= =========
GitBucket is the easily installable GitHub clone powered by Scala. GitBucket is the easily installable GitHub clone powered by Scala.
@@ -20,12 +20,12 @@ The current version of GitBucket provides a basic features below:
- Gravatar support - Gravatar support
- Plug-in system - 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 Installation
-------- --------
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases). 1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases).
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher. 2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser. 3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
@@ -42,7 +42,7 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk. To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo) For Installation on Windows Server with IIS see [this wiki page](https://github.com/gitbucket/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X ### Mac OS X
#### Installing Via Homebrew #### Installing Via Homebrew
@@ -65,7 +65,7 @@ Or, if you don't want/need launchctl, you can just run:
``` ```
#### Manual Installation #### Manual Installation
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/` On OS X, generate `gitbucket.plist` by [this script](https://raw.githubusercontent.com/gitbucket/gitbucket/master/contrib/macosx/makePlist) and copy it to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to Run the following commands in `Terminal` to
@@ -80,21 +80,32 @@ 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-announce-plugin](https://github.com/gitbucket-plugins/gitbucket-announce-plugin)
- [gitbucket-h2-backup-plugin](https://github.com/gitbucket-plugins/gitbucket-h2-backup-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-desktopnotify-plugin](https://github.com/yoshiyoshifujii/gitbucket-desktopnotify-plugin)
- [gitbucket-commitgraphs-plugin](https://github.com/yoshiyoshifujii/gitbucket-commitgraphs-plugin)
You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/). You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
Support 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. - 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 Release Notes
-------- --------
### 3.8 - 31 Oct 2015
- Moved to 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 ### 3.7 - 3 Oct 2015
- Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown - Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown
- Clone in desktop button - Clone in desktop button
- Providing MD5 and SHA-1 checksum for `gitbucket.war` has started
### 3.6 - 30 Aug 2015 ### 3.6 - 30 Aug 2015
- User interface Improvements: Especially, commit list, issues and pull request have been updated largely. - User interface Improvements: Especially, commit list, issues and pull request have been updated largely.

View File

@@ -38,7 +38,7 @@ createDir "$GITBUCKET_DIR"
createDir "$GITBUCKET_LOG_DIR" createDir "$GITBUCKET_LOG_DIR"
echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE" echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/gitbucket/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
sudo rm -f "$GITBUCKET_LOG_DIR/run.log" sudo rm -f "$GITBUCKET_LOG_DIR/run.log"

View File

@@ -3,7 +3,7 @@ Summary: GitHub clone written with Scala.
Version: 2.6 Version: 2.6
Release: 1%{?dist} Release: 1%{?dist}
License: Apache License: Apache
URL: https://github.com/takezoe/gitbucket URL: https://github.com/gitbucket/gitbucket
Group: System/Servers Group: System/Servers
Source0: %{name}.war Source0: %{name}.war
Source1: %{name}.init Source1: %{name}.init

View File

@@ -2,7 +2,7 @@ Automatic Schema Updating
======== ========
GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading. GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading.
To release a new version of GitBucket, add the version definition to the [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 ```scala
object AutoUpdate { object AutoUpdate {
@@ -16,7 +16,7 @@ object AutoUpdate {
... ...
``` ```
Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/takezoe/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```. Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/gitbucket/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```.
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version. GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.

View File

@@ -10,7 +10,7 @@ import sbtassembly.AssemblyKeys._
object MyBuild extends Build { object MyBuild extends Build {
val Organization = "gitbucket" val Organization = "gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val Version = "3.7.0" val Version = "3.8.0"
val ScalaVersion = "2.11.6" val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1" val ScalatraVersion = "2.3.1"
@@ -51,11 +51,12 @@ object MyBuild extends Build {
"org.json4s" %% "json4s-jackson" % "3.2.11", "org.json4s" %% "json4s-jackson" % "3.2.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0", "jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"commons-io" % "commons-io" % "2.4", "commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.3", "io.github.gitbucket" % "markedj" % "1.0.4-SNAPSHOT",
"org.apache.commons" % "commons-compress" % "1.9", "org.apache.commons" % "commons-compress" % "1.9",
"org.apache.commons" % "commons-email" % "1.3.3", "org.apache.commons" % "commons-email" % "1.3.3",
"org.apache.httpcomponents" % "httpclient" % "4.3.6", "org.apache.httpcomponents" % "httpclient" % "4.3.6",
"org.apache.sshd" % "apache-sshd" % "0.11.0", "org.apache.sshd" % "apache-sshd" % "0.11.0",
"org.apache.tika" % "tika-core" % "1.10",
"com.typesafe.slick" %% "slick" % "2.1.0", "com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.180", "com.h2database" % "h2" % "1.4.180",

View File

@@ -55,7 +55,12 @@
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/> tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target> </target>
<target name="all" depends="rename"> <target name="checksum" depends="rename">
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="MD5" format="MD5SUM" forceOverwrite="yes" fileext=".md5"/>
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="SHA" format="MD5SUM" forceOverwrite="yes" fileext=".sha1"/>
</target>
<target name="all" depends="checksum">
</target> </target>

View File

@@ -128,7 +128,7 @@ INSERT INTO ACCOUNT (
'root@localhost', 'root@localhost',
'dc76e9f0c0006e8f919e0c515c66dbba3982f785', 'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
true, true,
'https://github.com/takezoe/gitbucket', 'https://github.com/gitbucket/gitbucket',
SYSDATE, SYSDATE,
SYSDATE, SYSDATE,
NULL NULL

View File

@@ -20,13 +20,21 @@ case class ApiCommit(
removed: List[String], removed: List[String],
modified: List[String], modified: List[String],
author: ApiPersonIdent, author: ApiPersonIdent,
committer: ApiPersonIdent)(repositoryName:RepositoryName) extends FieldSerializable{ committer: ApiPersonIdent)(repositoryName:RepositoryName, urlIsHtmlUrl: Boolean) extends FieldSerializable{
val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}") val url = if(urlIsHtmlUrl){
val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}") ApiPath(s"/${repositoryName.fullName}/commit/${id}")
}else{
ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
}
val html_url = if(urlIsHtmlUrl){
None
}else{
Some(ApiPath(s"/${repositoryName.fullName}/commit/${id}"))
}
} }
object ApiCommit{ object ApiCommit{
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = { def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo, urlIsHtmlUrl: Boolean = false): ApiCommit = {
val diffs = JGitUtil.getDiffs(git, commit.id, false) val diffs = JGitUtil.getDiffs(git, commit.id, false)
ApiCommit( ApiCommit(
id = commit.id, id = commit.id,
@@ -43,6 +51,7 @@ object ApiCommit{
}, },
author = ApiPersonIdent.author(commit), author = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit) committer = ApiPersonIdent.committer(commit)
)(repositoryName) )(repositoryName, urlIsHtmlUrl)
} }
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
} }

View File

@@ -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)
}

View File

@@ -13,10 +13,14 @@ case class ApiRepository(
forks: Int, forks: Int,
`private`: Boolean, `private`: Boolean,
default_branch: String, default_branch: String,
owner: ApiUser) { owner: ApiUser)(urlIsHtmlUrl: Boolean) {
val forks_count = forks val forks_count = forks
val watchers_count = watchers val watchers_count = watchers
val url = ApiPath(s"/api/v3/repos/${full_name}") val url = if(urlIsHtmlUrl){
ApiPath(s"/${full_name}")
}else{
ApiPath(s"/api/v3/repos/${full_name}")
}
val http_url = ApiPath(s"/git/${full_name}.git") val http_url = ApiPath(s"/git/${full_name}.git")
val clone_url = ApiPath(s"/git/${full_name}.git") val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}") val html_url = ApiPath(s"/${full_name}")
@@ -27,7 +31,8 @@ object ApiRepository{
repository: Repository, repository: Repository,
owner: ApiUser, owner: ApiUser,
forkedCount: Int =0, forkedCount: Int =0,
watchers: Int = 0): ApiRepository = watchers: Int = 0,
urlIsHtmlUrl: Boolean = false): ApiRepository =
ApiRepository( ApiRepository(
name = repository.repositoryName, name = repository.repositoryName,
full_name = s"${repository.userName}/${repository.repositoryName}", full_name = s"${repository.userName}/${repository.repositoryName}",
@@ -37,7 +42,7 @@ object ApiRepository{
`private` = repository.isPrivate, `private` = repository.isPrivate,
default_branch = repository.defaultBranch, default_branch = repository.defaultBranch,
owner = owner owner = owner
) )(urlIsHtmlUrl)
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository = def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount) ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
@@ -45,4 +50,7 @@ object ApiRepository{
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository = def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
this(repositoryInfo.repository, ApiUser(owner)) this(repositoryInfo.repository, ApiUser(owner))
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
} }

View 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("-")
}
}

View File

@@ -366,56 +366,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){ LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { member =>
addCollaborator(form.owner, form.name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
} }
// redirect to the repository // redirect to the repository
@@ -423,6 +374,54 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
}) })
/**
* Create user repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/user/repos")(usersOnly {
val owner = context.loginAccount.get.userName
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${owner}/${data.name}") {
if(getRepository(owner, data.name, context.baseUrl).isEmpty){
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(owner, data.name, context.baseUrl).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else {
ApiError(
"A repository with this name already exists on this account",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound
})
/**
* Create group repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/orgs/:org/repos")(managersOnly {
val groupName = params("org")
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${groupName}/${data.name}") {
if(getRepository(groupName, data.name, context.baseUrl).isEmpty){
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(groupName, data.name, context.baseUrl).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
} else {
ApiError(
"A repository with this name already exists for this group",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound
})
get("/:owner/:repository/fork")(readableUsersOnly { repository => get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
@@ -496,6 +495,59 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
}) })
private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) {
val ownerAccount = getAccountByUserName(owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(name, owner, description, isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(owner).foreach { member =>
addCollaborator(owner, name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(owner, name)
// Create the actual repository
val gitdir = getRepositoryDir(owner, name)
JGitUtil.initRepository(gitdir)
if(createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(description.nonEmpty){
name + "\n" +
"===============\n" +
"\n" +
description.get
} else {
name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, owner, name)
// Record activity
recordCreateRepositoryActivity(owner, name, loginUserName)
}
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = { private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929") createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc") createLabel(userName, repositoryName, "duplicate", "cccccc")

View File

@@ -17,22 +17,22 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){ post("/image"){
execute { (file, fileId) => execute({ (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get) FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
session += Keys.Session.Upload(fileId) -> file.name session += Keys.Session.Upload(fileId) -> file.name
} }, FileUtil.isImage)
} }
post("/image/:owner/:repository"){ post("/file/:owner/:repository"){
execute { (file, fileId) => execute({ (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File( FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")), getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get) fileId + "." + FileUtil.getExtension(file.getName)), file.get)
} }, FileUtil.isUploadableType)
} }
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match { private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId => defining(FileUtil.generateFileId){ fileId =>
f(file, fileId) f(file, fileId)

View File

@@ -332,6 +332,7 @@ trait IssuesControllerBase extends ControllerBase {
(Directory.getAttachedDir(repository.owner, repository.name) match { (Directory.getAttachedDir(repository.owner, repository.name) match {
case dir if(dir.exists && dir.isDirectory) => case dir if(dir.exists && dir.isDirectory) =>
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
RawData(FileUtil.getMimeType(file.getName), file) RawData(FileUtil.getMimeType(file.getName), file)
} }
case _ => None case _ => None
@@ -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) = { private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
StringUtil.extractIssueId(message).foreach { issueId => StringUtil.extractIssueId(message).foreach { issueId =>
val content = fromIssue.issueId + ":" + fromIssue.title val content = fromIssue.issueId + ":" + fromIssue.title

View File

@@ -294,6 +294,9 @@ trait PullRequestsControllerBase extends ControllerBase {
originRepositoryName <- if(originOwner == forkedOwner) { originRepositoryName <- if(originOwner == forkedOwner) {
// Self repository // Self repository
Some(forkedRepository.name) Some(forkedRepository.name)
} else if(forkedRepository.repository.originUserName.isEmpty){
// when ForkedRepository is the original repository
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
} else if(Some(originOwner) == forkedRepository.repository.originUserName){ } else if(Some(originOwner) == forkedRepository.repository.originUserName){
// Original repository // Original repository
forkedRepository.repository.originRepositoryName forkedRepository.repository.originRepositoryName
@@ -436,8 +439,11 @@ trait PullRequestsControllerBase extends ControllerBase {
// call web hook // call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get) callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications
getIssue(owner, name, issueId.toString) foreach { issue => 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().toNotify(repository, issue, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") 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. * Parses branch identifier and extracts owner and branch name as tuple.
* *

View File

@@ -14,6 +14,7 @@ import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId
class RepositorySettingsController extends RepositorySettingsControllerBase class RepositorySettingsController extends RepositorySettingsControllerBase
@@ -165,13 +166,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
val commits = if(repository.commitCount == 0) List.empty else git.log val commits = if(repository.commitCount == 0) List.empty else git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch)) .add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(3) .setMaxCount(4)
.call.iterator.asScala.map(new CommitInfo(_)) .call.iterator.asScala.map(new CommitInfo(_)).toList
getAccountByUserName(repository.owner).foreach { ownerAccount => getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook("push", callWebHook("push",
List(WebHook(repository.owner, repository.name, form.url)), List(WebHook(repository.owner, repository.name, form.url)),
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount) WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, (if(commits.isEmpty){Nil}else{commits.tail}), 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 += "url" -> form.url

View File

@@ -293,8 +293,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map { objectId => getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){ if(raw){
// Download // Download
JGitUtil.getContentFromId(git, objectId, true).map { bytes => JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
RawData("application/octet-stream", bytes) //RawData("application/octet-stream", bytes)
contentType = "application/octet-stream"
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.getOutputStream)
()
} getOrElse NotFound } getOrElse NotFound
} else { } else {
html.blob(id, repository, path.split("/").toList, html.blob(id, repository, path.split("/").toList,
@@ -665,7 +669,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, "push") { callWebHookOf(repository.owner, repository.name, "push") {
getAccountByUserName(repository.owner).map{ ownerAccount => getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount) WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
} }
} }
} }

View File

@@ -92,7 +92,7 @@ trait IssuesService {
def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={ def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={
if(issueList.isEmpty){ if(issueList.isEmpty){
Map.empty Map.empty
}else{ } else {
import scala.slick.jdbc._ import scala.slick.jdbc._
val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ") val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ")
implicit val qset = SetParameter[Seq[(String, String, Int)]] { implicit val qset = SetParameter[Seq[(String, String, Int)]] {

View File

@@ -66,10 +66,6 @@ trait RepositoryService { self: AccountService =>
(t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind) (t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t =>
t.requestRepositoryName === oldRepositoryName.bind
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
// Updates activity fk before deleting repository because activity is sorted by activityId // Updates activity fk before deleting repository because activity is sorted by activityId
// and it can't be changed by deleting-and-inserting record. // and it can't be changed by deleting-and-inserting record.
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity => Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
@@ -98,6 +94,11 @@ trait RepositoryService { self: AccountService =>
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update source repository of pull requests
PullRequests.filter { t =>
(t.requestUserName === oldUserName.bind) && (t.requestRepositoryName === oldRepositoryName.bind)
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
// Convert labelId // Convert labelId
val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap
val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap

View File

@@ -12,6 +12,7 @@ import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.message.BasicNameValuePair import org.apache.http.message.BasicNameValuePair
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -192,21 +193,33 @@ object WebHookService {
case class WebHookPushPayload( case class WebHookPushPayload(
pusher: ApiUser, pusher: ApiUser,
ref: String, ref: String,
commits: List[ApiPushCommit], before: String,
after: String,
commits: List[ApiCommit],
repository: ApiRepository repository: ApiRepository
) extends WebHookPayload ) extends FieldSerializable with WebHookPayload {
val compare = commits.size match {
case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initalied repository
case 1 => ApiPath(s"/${repository.full_name}/commit/${after}")
case _ if before.filterNot(_=='0').isEmpty => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}")
}
val head_commit = commits.lastOption
}
object WebHookPushPayload { object WebHookPushPayload {
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo, def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account): WebHookPushPayload = commits: List[CommitInfo], repositoryOwner: Account,
newId: ObjectId, oldId: ObjectId): WebHookPushPayload =
WebHookPushPayload( WebHookPushPayload(
ApiUser(pusher), pusher = ApiUser(pusher),
refName, ref = refName,
commits.map{ commit => ApiPushCommit(git, RepositoryName(repositoryInfo), commit) }, before = ObjectId.toString(oldId),
ApiRepository( after = ObjectId.toString(newId),
commits = commits.map{ commit => ApiCommit.forPushPayload(git, RepositoryName(repositoryInfo), commit) },
repository = ApiRepository.forPushPayload(
repositoryInfo, repositoryInfo,
owner= ApiUser(repositoryOwner) owner= ApiUser(repositoryOwner))
)
) )
} }

View File

@@ -21,6 +21,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(3, 8),
new Version(3, 7) with SystemSettingsService { new Version(3, 7) with SystemSettingsService {
override def update(conn: Connection, cl: ClassLoader): Unit = { override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl) super.update(conn, cl)

View File

@@ -203,7 +203,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
callWebHookOf(owner, repository, "push"){ callWebHookOf(owner, repository, "push"){
for(pusherAccount <- getAccountByUserName(pusher); for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner)) yield { ownerAccount <- getAccountByUserName(owner)) yield {
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount) WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
newId = command.getNewId(), oldId = command.getOldId())
} }
} }
} }

View File

@@ -14,7 +14,7 @@ object SshServer {
private def configure(port: Int, baseUrl: String) = { private def configure(port: Int, baseUrl: String) = {
server.setPort(port) server.setPort(port)
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser")) server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser", "RSA"))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator) server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
server.setCommandFactory(new GitCommandFactory(baseUrl)) server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setShellFactory(new NoShell) server.setShellFactory(new NoShell)

View File

@@ -1,7 +1,7 @@
package gitbucket.core.util package gitbucket.core.util
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import java.net.URLConnection import org.apache.tika.Tika
import java.io.File import java.io.File
import ControlUtil._ import ControlUtil._
import scala.util.Random import scala.util.Random
@@ -9,8 +9,8 @@ import scala.util.Random
object FileUtil { object FileUtil {
def getMimeType(name: String): String = def getMimeType(name: String): String =
defining(URLConnection.getFileNameMap()){ fileNameMap => defining(new Tika()){ tika =>
fileNameMap.getContentTypeFor(name) match { tika.detect(name) match {
case null => "application/octet-stream" case null => "application/octet-stream"
case mimeType => mimeType case mimeType => mimeType
} }
@@ -28,6 +28,8 @@ object FileUtil {
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/") def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isUploadableType(name: String): Boolean = mimeTypeWhiteList contains getMimeType(name)
def isLarge(size: Long): Boolean = (size > 1024 * 1000) def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0) def isText(content: Array[Byte]): Boolean = !content.contains(0)
@@ -50,4 +52,14 @@ object FileUtil {
FileUtils.deleteDirectory(dir) FileUtils.deleteDirectory(dir)
} }
} }
val mimeTypeWhiteList: Array[String] = Array(
"application/pdf",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"image/gif",
"image/jpeg",
"image/png",
"text/plain")
} }

View File

@@ -65,6 +65,7 @@ object Implicits {
def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{ def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{
case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */) case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
case path if path.startsWith("api/v3/orgs/") => path.substring(12/* "/api/v3/orgs".length */)
case path => path case path => path
}).split("/") }).split("/")

View File

@@ -100,8 +100,18 @@ object JGitUtil {
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
} }
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String], case class DiffInfo(
oldIsImage: Boolean, newIsImage: Boolean, oldObjectId: Option[String], newObjectId: Option[String]) changeType: ChangeType,
oldPath: String,
newPath: String,
oldContent: Option[String],
newContent: Option[String],
oldIsImage: Boolean,
newIsImage: Boolean,
oldObjectId: Option[String],
newObjectId: Option[String],
tooLarge: Boolean
)
/** /**
* The file content data for the file content view of the repository viewer. * The file content data for the file content view of the repository viewer.
@@ -495,11 +505,31 @@ object JGitUtil {
while(treeWalk.next){ while(treeWalk.next){
val newIsImage = FileUtil.isImage(treeWalk.getPathString) val newIsImage = FileUtil.isImage(treeWalk.getPathString)
buffer.append((if(!fetchContent){ buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None, false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name)) DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
newPath = treeWalk.getPathString,
oldContent = None,
newContent = None,
oldIsImage = false,
newIsImage = newIsImage,
oldObjectId = None,
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
tooLarge = false
)
} else { } else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, DiffInfo(
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray), changeType = ChangeType.ADD,
false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name)) oldPath = null,
newPath = treeWalk.getPathString,
oldContent = None,
newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
oldIsImage = false,
newIsImage = newIsImage,
oldObjectId = None,
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
tooLarge = false
)
})) }))
} }
(buffer.toList, None) (buffer.toList, None)
@@ -518,16 +548,52 @@ object JGitUtil {
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
git.getRepository.getConfig.setString("diff", null, "renames", "copies") git.getRepository.getConfig.setString("diff", null, "renames", "copies")
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
val oldIsImage = FileUtil.isImage(diff.getOldPath) val diffs = git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala
val newIsImage = FileUtil.isImage(diff.getNewPath) diffs.map { diff =>
if(!fetchContent || oldIsImage || newIsImage){ if(diffs.size > 100){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None, oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name)) DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = None,
newContent = None,
oldIsImage = false,
newIsImage = false,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
tooLarge = true
)
} else { } else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, val oldIsImage = FileUtil.isImage(diff.getOldPath)
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), val newIsImage = FileUtil.isImage(diff.getNewPath)
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), if(!fetchContent || oldIsImage || newIsImage){
oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name)) DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = None,
newContent = None,
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
tooLarge = false
)
} else {
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 }.toList
} }
@@ -713,7 +779,7 @@ object JGitUtil {
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try { def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
using(git.getRepository.getObjectDatabase){ db => using(git.getRepository.getObjectDatabase){ db =>
val loader = db.open(id) val loader = db.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){ if(loader.isLarge || (fetchLargeFile == false && FileUtil.isLarge(loader.getSize))){
None None
} else { } else {
Some(loader.getBytes) Some(loader.getBytes)
@@ -723,6 +789,22 @@ object JGitUtil {
case e: MissingObjectException => None case e: MissingObjectException => None
} }
/**
* Get objectLoader of the given object id from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param f the function process ObjectLoader
* @return None if object does not exist
*/
def getObjectLoaderFromId[A](git: Git, id: ObjectId)(f: ObjectLoader => A):Option[A] = try {
using(git.getRepository.getObjectDatabase){ db =>
Some(f(db.open(id)))
}
} catch {
case e: MissingObjectException => None
}
/** /**
* Returns all commit id in the specified repository. * Returns all commit id in the specified repository.
*/ */

View File

@@ -7,13 +7,31 @@ import gitbucket.core.util.Implicits.RichString
trait LinkConverter { self: RequestCache => trait LinkConverter { self: RequestCache =>
/** /**
* Converts issue id, username and commit id to link. * Creates a link to the issue or the pull request from the issue id.
*/ */
protected def convertRefsLinks(value: String, repository: RepositoryService.RepositoryInfo, protected def createIssueLink(repository: RepositoryService.RepositoryInfo, issueId: Int)(implicit context: Context): String = {
val userName = repository.repository.userName
val repositoryName = repository.repository.repositoryName
getIssue(userName, repositoryName, issueId.toString) match {
case Some(issue) if (issue.isPullRequest) =>
s"""<a href="${context.path}/${userName}/${repositoryName}/pull/${issueId}">Pull #${issueId}</a>"""
case Some(_) =>
s"""<a href="${context.path}/${userName}/${repositoryName}/issues/${issueId}">Issue #${issueId}</a>"""
case None =>
s"Unknown #${issueId}"
}
}
/**
* Converts issue id, username and commit id to link in the given text.
*/
protected def convertRefsLinks(text: String, repository: RepositoryService.RepositoryInfo,
issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = { issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = {
// escape HTML tags // escape HTML tags
val escaped = if(escapeHtml) value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") else value val escaped = if(escapeHtml) text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") else text
escaped escaped
// convert username/project@SHA to link // convert username/project@SHA to link
@@ -26,10 +44,12 @@ trait LinkConverter { self: RequestCache =>
// convert username/project#Num to link // convert username/project#Num to link
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m => .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 { getIssue(m.group(2), m.group(3), m.group(4)) match {
case Some(issue) if (issue.isPullRequest) 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>""") 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 Some(_) =>
case None => Some( s"""${m.group(2)}/${m.group(3)}#${m.group(4)}""") 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 // convert username#Num to link
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r ) { m => .replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r ) { m =>
getIssue(m.group(2), repository.name, m.group(3)) match { getIssue(m.group(2), repository.name, m.group(3)) match {
case Some(issue) if(issue.isPullRequest) 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>""") 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 Some(_) =>
case None => Some(s"""${m.group(2)}#${m.group(3)}""") 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 => .replaceBy(("(?<=(^|\\W))(GH-|" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r){ m =>
val prefix = if(m.group(2) == "issue:") "#" else m.group(2) val prefix = if(m.group(2) == "issue:") "#" else m.group(2)
getIssue(repository.owner, repository.name, m.group(3)) match { getIssue(repository.owner, repository.name, m.group(3)) match {
case Some(issue) if(issue.isPullRequest) 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>""") 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 Some(_) =>
case None => Some(s"""${m.group(2)}${m.group(3)}""") 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)}""")
} }
} }

View File

@@ -110,7 +110,7 @@ object Markdown {
} }
override def link(href: String, title: String, text: String): String = { 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 = { override def image(href: String, title: String, text: String): String = {

View File

@@ -93,6 +93,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
pages: List[String] = Nil)(implicit context: Context): Html = pages: List[String] = Nil)(implicit context: Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, true, enableTaskList, hasWritePermission, pages)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, true, enableTaskList, hasWritePermission, pages))
/**
* Render the given source (only markdown is supported in default) as HTML.
* You can test if a file is renderable in this method by [[isRenderable()]].
*/
def renderMarkup(filePath: List[String], fileContent: String, branch: String, def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: RepositoryService.RepositoryInfo, repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = { 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)) renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context))
} }
/**
* Tests whether the given file is renderable. It's tested by the file extension.
*/
def isRenderable(fileName: String): Boolean = { def isRenderable(fileName: String): Boolean = {
PluginRegistry().renderableExtensions.exists(extension => fileName.toLowerCase.endsWith("." + extension)) PluginRegistry().renderableExtensions.exists(extension => fileName.toLowerCase.endsWith("." + extension))
} }
/**
* Creates a link to the issue or the pull request from the issue id.
*/
def issueLink(repository: RepositoryService.RepositoryInfo, issueId: Int)(implicit context: Context): Html = {
Html(createIssueLink(repository, issueId))
}
/** /**
* Returns &lt;img&gt; which displays the avatar icon for the given user name. * Returns &lt;img&gt; which displays the avatar icon for the given user name.
* This method looks up Gravatar if avatar icon has not been configured in user settings. * This method looks up Gravatar if avatar icon has not been configured in user settings.
@@ -275,4 +289,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
case CommitState.ERROR => "Failed" case CommitState.ERROR => "Failed"
case CommitState.FAILURE => "Failed" case CommitState.FAILURE => "Failed"
} }
// This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string)
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
def detectAndRenderLinks(text: String): Html = {
Html(detectAndRenderLinksRegex.replaceAllIn(text, m => s"""<a href="${m.group(0)}">${m.group(0)}</a>"""))
}
} }

View File

@@ -1,22 +1,24 @@
@(owner: String, repository: String)(textarea: Html)(implicit context: gitbucket.core.controller.Context) @(owner: String, repository: String)(textarea: Html)(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
@import gitbucket.core.util.FileUtil
<div class="muted attachable"> <div class="muted attachable">
@textarea @textarea
<div class="clickable">Attach images by dragging &amp; dropping, or selecting them.</div> <div class="clickable">Attach images or documents by dragging &amp; dropping, or selecting them.</div>
</div> </div>
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId => @defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
<script> <script>
$(function(){ $(function(){
try { try {
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({ $([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
url: '@path/upload/image/@owner/@repository', url: '@path/upload/file/@owner/@repository',
maxFilesize: 10, maxFilesize: 10,
acceptedFiles: 'image/*', acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.', dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>", previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) { success: function(file, id) {
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')'; var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +
$('#@textareaId').val($('#@textareaId').val() + images); '](@baseUrl/@owner/@repository/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + attachFile);
$(file.previewElement).prevAll('div.dz-preview').addBack().remove(); $(file.previewElement).prevAll('div.dz-preview').addBack().remove();
} }
}); });

View File

@@ -92,30 +92,38 @@
<td style="padding: 0;"> <td style="padding: 0;">
@if(diff.oldObjectId == diff.newObjectId){ @if(diff.oldObjectId == diff.newObjectId){
<div class="diff-same">File renamed without changes</div> <div class="diff-same">File renamed without changes</div>
} else { @if(diff.newContent != None || diff.oldContent != None){
<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 { } 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> </td>
</tr> </tr>
</table> </table>

View File

@@ -8,6 +8,7 @@
styleClass: String = "", styleClass: String = "",
placeholder: String = "Leave a comment", placeholder: String = "Leave a comment",
elastic: Boolean = false, elastic: Boolean = false,
tabIndex: Int = -2,
uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context) uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
@import gitbucket.core._ @import gitbucket.core._
@@ -22,6 +23,7 @@
<span id="error-content" class="error"></span> <span id="error-content" class="error"></span>
@textarea = { @textarea = {
<textarea id="content@uid" name="content" placeholder="@placeholder" <textarea id="content@uid" name="content" placeholder="@placeholder"
@if(tabIndex > -2){ tabindex="@tabIndex"}
@if(style.nonEmpty){ style="@style"} @if(style.nonEmpty){ style="@style"}
@if(styleClass.nonEmpty){ class="@styleClass" }>@content</textarea> @if(styleClass.nonEmpty){ class="@styleClass" }>@content</textarea>
} }

View File

@@ -18,14 +18,15 @@
enableTaskList = true, enableTaskList = true,
hasWritePermission = hasWritePermission, hasWritePermission = hasWritePermission,
style = "", style = "",
elastic = true elastic = true,
tabIndex = 1
) )
<div style="text-align: right;"> <div style="text-align: right;">
<input type="hidden" name="issueId" value="@issue.issueId"/> <input type="hidden" name="issueId" value="@issue.issueId"/>
<input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){ @if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/> <input type="submit" class="btn" tabindex="3" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
} }
<input type="submit" class="btn btn-success" tabindex="2" formaction="@url(repository)/issue_comments/new" value="Comment"/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -55,7 +55,7 @@
} else { } else {
@if(comment.action == "refer"){ @if(comment.action == "refer"){
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) => @defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong> <strong>@issueLink(repository, issueId.toInt): @rest.mkString(":")</strong>
} }
} else { } else {
<div class="markdown-body">@markdown(comment.content, repository, false, true, true, hasWritePermission)</div> <div class="markdown-body">@markdown(comment.content, repository, false, true, true, hasWritePermission)</div>

View File

@@ -104,7 +104,7 @@
<div style="margin-right: @if(expand){180px} else {50px};"> <div style="margin-right: @if(expand){180px} else {50px};">
@if(expand){ @if(expand){
@repository.repository.description.map { description => @repository.repository.description.map { description =>
<p class="description">@description</p> <p class="description">@detectAndRenderLinks(description)</p>
} }
<div style="margin-bottom: 10px;" class="box-content"> <div style="margin-bottom: 10px;" class="box-content">
<table class="fill-width"> <table class="fill-width">

View File

@@ -1681,13 +1681,15 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
} }
.markdown-head { .markdown-head {
left: -18px;
padding-left: 18px;
position: relative; position: relative;
line-height: 1.7; line-height: 1.7;
} }
a.markdown-anchor-link { a.markdown-anchor-link {
position: absolute; position: absolute;
left: -18px; left: 0px;
display: none; display: none;
color: #999; color: #999;
/* From octicon style */ /* From octicon style */

View File

@@ -23,18 +23,9 @@ $(function(){
$(e.target).children('a.markdown-anchor-link').show(); $(e.target).children('a.markdown-anchor-link').show();
}); });
$('.markdown-head').mouseleave(function(e){ $('.markdown-head').mouseleave(function(e){
var anchorLink = $(e.target).children('a.markdown-anchor-link'); $(e.target).children('a.markdown-anchor-link').hide();
if(anchorLink.data('active') !== true){
anchorLink.hide();
}
}); });
$('a.markdown-anchor-link').mouseenter(function(e){
$(e.target).data('active', true);
});
$('a.markdown-anchor-link').mouseleave(function(e){ $('a.markdown-anchor-link').mouseleave(function(e){
$(e.target).data('active', false);
$(e.target).hide(); $(e.target).hide();
}); });
@@ -360,12 +351,12 @@ function scrollIntoView(target){
} }
} }
/** ///**
* escape html // * escape html
*/ // */
function escapeHtml(text){ //function escapeHtml(text){
return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;').replace(/>/g,'&gt;'); // return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;').replace(/>/g,'&gt;');
} //}
/** /**
* calculate string ranking for path. * calculate string ranking for path.

View File

@@ -45,7 +45,7 @@ class JsonFormatSpec extends Specification {
forks = 0, forks = 0,
`private` = false, `private` = false,
default_branch = "master", default_branch = "master",
owner = apiUser) owner = apiUser)(urlIsHtmlUrl = false)
val repositoryJson = s"""{ val repositoryJson = s"""{
"name" : "Hello-World", "name" : "Hello-World",
"full_name" : "octocat/Hello-World", "full_name" : "octocat/Hello-World",
@@ -85,7 +85,7 @@ class JsonFormatSpec extends Specification {
"url": "http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/statuses" "url": "http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/statuses"
}""" }"""
val apiPushCommit = ApiPushCommit( val apiPushCommit = ApiCommit(
id = "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", id = "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
message = "Update README.md", message = "Update README.md",
timestamp = date1, timestamp = date1,
@@ -93,7 +93,7 @@ class JsonFormatSpec extends Specification {
removed = Nil, removed = Nil,
modified = List("README.md"), modified = List("README.md"),
author = ApiPersonIdent("baxterthehacker","baxterthehacker@users.noreply.github.com",date1), 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"""{ val apiPushCommitJson = s"""{
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", "id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
// "distinct": true, // "distinct": true,

View 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>)"""
}
}
}