Merge branch 'master' into feature/webhook-scope

This commit is contained in:
nazoking
2015-11-06 14:48:03 +09:00
36 changed files with 524 additions and 221 deletions

View File

@@ -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.

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.
@@ -20,12 +20,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.
@@ -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.
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 +65,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
@@ -81,18 +81,28 @@ GitBucket has the plug-in system to extend GitBucket from outside of GitBucket.
- [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.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

View File

@@ -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"

View File

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

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.
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.

View File

@@ -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.8.0"
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
@@ -51,11 +51,12 @@ object MyBuild extends Build {
"org.json4s" %% "json4s-jackson" % "3.2.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.4-SNAPSHOT",
"io.github.gitbucket" % "markedj" % "1.0.4",
"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",

View File

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

View File

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

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,
`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)
}

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 =>
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 +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 =>
val loginAccount = context.loginAccount.get
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 = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")

View File

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

View File

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

View File

@@ -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.
*

View File

@@ -15,6 +15,7 @@ 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
@@ -182,11 +183,21 @@ trait RepositorySettingsControllerBase extends ControllerBase {
import scala.concurrent.ExecutionContext.Implicits.global
val url = params("url")
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(_))
val ownerAccount = getAccountByUserName(repository.owner).get
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()))
}
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))
@@ -194,10 +205,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
case e:org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url"))
case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage))
}
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push,
List(WebHook(repository.owner, repository.name, url)),
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
).head
contentType = formats("json")
var result = Map(
"url" -> url,

View File

@@ -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,
@@ -665,7 +669,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
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)
}
}
}

View File

@@ -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)]] {

View File

@@ -66,10 +66,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 =>
@@ -98,6 +94,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

View File

@@ -12,6 +12,7 @@ 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
@@ -244,21 +245,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))
)
}

View File

@@ -203,7 +203,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
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())
}
}
}

View File

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

View File

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

View File

@@ -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("/")

View File

@@ -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.
*/

View File

@@ -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("&", "&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
// 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)}""")
}
}

View File

@@ -110,7 +110,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 = {

View File

@@ -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 &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.
@@ -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>"""))
}
}

View File

@@ -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 &amp; dropping, or selecting them.</div>
<div class="clickable">Attach images or documents by dragging &amp; 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();
}
});

View File

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

View File

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

View File

@@ -55,7 +55,7 @@
} 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>

View File

@@ -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">

View File

@@ -64,6 +64,11 @@ h6 {
.align-right {
text-align: right;
}
blockquote p {
font-size: 15px;
}
/* ======================================================================== */
/* Global Header */
/* ======================================================================== */

View File

@@ -45,7 +45,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 +85,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 +93,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,

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