Compare commits

...

68 Commits
2.7 ... 2.8

Author SHA1 Message Date
Naoki Takezoe
0085cb24ad Add description about 2.8 2015-02-01 13:00:31 +09:00
Naoki Takezoe
6a758902ef Small fix for #615 2015-02-01 12:55:37 +09:00
Naoki Takezoe
0d81a9a9b6 Merge pull request #615 from team-lab/fix/xss-by-raw-html
fix/xss by raw html
2015-02-01 12:44:15 +09:00
Naoki Takezoe
6e4f6da633 Merge pull request #612 from team-lab/fix/update-pullrequest-on-commit-by-online-editor
fix/update pullrequest when file edited by online editor
2015-01-31 18:19:46 +09:00
Naoki Takezoe
15118ca5c1 Merge pull request #614 from HairyFotr/patch-typo
Fix typo
2015-01-31 13:58:55 +09:00
HairyFotr
8161560757 Fix typo 2015-01-30 21:34:25 +01:00
nazoking
9ba564c864 test/html is cause of xss 2015-01-30 15:32:53 +09:00
nazoking
06b5b92673 update pullrequest commitId on file edited by online editor 2015-01-30 04:14:04 +09:00
Naoki Takezoe
b9b6589bd7 Update README.md 2015-01-29 21:47:54 +09:00
Naoki Takezoe
b79f6a5fa0 Update README.md 2015-01-29 21:47:00 +09:00
Shintaro Murakami
bd046da3d0 (refs #532) Fix rendering of link over image 2015-01-28 00:09:34 +09:00
Naoki Takezoe
a889ed7c46 Merge pull request #591 from marklacroix/anon-access
(refs #274) Add option to deny anonymous (i.e. unauthorized) access
2015-01-27 10:28:11 +09:00
Naoki Takezoe
e24684cb2b Update favicon 2015-01-25 20:01:12 +09:00
Naoki Takezoe
5f939c18b4 (refs #609)Convert labelId when rename repository 2015-01-25 14:45:37 +09:00
takezoe
d412dd5009 (refs #600)Fix broken layout 2015-01-25 01:16:03 +09:00
Mark LaCroix
8643bfeb37 Merge remote-tracking branch 'upstream/master' into anon-access
Conflicts:
	src/main/scala/app/SystemSettingsController.scala
	src/main/scala/service/SystemSettingsService.scala
	src/test/scala/view/AvatarImageProviderSpec.scala
2015-01-21 15:49:42 -05:00
Naoki Takezoe
31b6adf0e5 Merge pull request #606 from bati11/fix-mergeguide-text
Fix merge guide's text
2015-01-21 01:39:37 +09:00
bati11
f1ac2b3507 Change checkout branch name from "master" to ${pullreq.branch} 2015-01-20 23:32:47 +09:00
Naoki Takezoe
135e1ef73d Merge pull request #602 from mrkm4ntr/default-privacy-option-to-create-repo
(refs #495,#595) Add configuration to set default visibility option to create new repositories.
2015-01-20 10:59:02 +09:00
Naoki Takezoe
da55bf6af3 Apply new icon 2015-01-18 14:17:41 +09:00
Naoki Takezoe
883a9c8b17 Improve Wiki rendering performance 2015-01-18 14:06:19 +09:00
Naoki Takezoe
7da89940e3 Use the issues list template for the pull request list in the dashboard 2015-01-18 03:59:33 +09:00
Shintaro Murakami
3233b0ae3c Fix failed test. 2015-01-17 23:28:08 +09:00
Shintaro Murakami
4c2ed09915 (refs #495,#595) Add configuration to set default visibility option to create new repositories. 2015-01-17 23:04:41 +09:00
Naoki Takezoe
256b6c480f Merge pull request #546 from rlazoti/add-link-to-dashboard
Add repository's link to issue and pull request list on dashboard
2015-01-17 16:44:04 +09:00
Naoki Takezoe
dc311837f9 Merge pull request #596 from tnayuki/streaming-archive
not to make a temporary file when archive
2015-01-17 16:29:36 +09:00
Naoki Takezoe
92aec48c99 Merge pull request #547 from mslinn/master
Change Bootstrap's default color pink for code tags to match github's color
2015-01-17 16:17:43 +09:00
Naoki Takezoe
a6ada8c457 Merge pull request #582 from team-lab/add-message-to-login
Show information message on singin view.
2015-01-17 14:50:37 +09:00
Shintaro Murakami
dcc601502e (refs #589) Prevent adding event handler several times 2015-01-14 23:04:23 +09:00
Shintaro Murakami
dd58d8c804 (refs #598) Exclude count of pull requests from that of issues. 2015-01-12 22:50:12 +09:00
Shintaro Murakami
2ade54b7e3 (#refs 549) Change "…" at skipped line to pseudo element 2015-01-11 01:04:13 +09:00
Shintaro Murakami
136c5854f3 (refs #593) Expand column size of FILE_NAME in COMIT_COMMENT 2015-01-11 00:28:52 +09:00
Shintaro Murakami
c597238d9c (refs #549) Selecting lines in diff without line numbers. 2015-01-10 01:22:47 +09:00
Toru Nayuki
2552a58e08 not to make a temporary file when archive 2015-01-09 18:39:48 +09:00
nazoking
74ad5872a3 Revert "add information to singup view"
This reverts commit f7fd53bf09.
2015-01-09 14:55:28 +09:00
Shintaro Murakami
485d502bd3 (refs #584) Refactored 2015-01-09 00:16:09 +09:00
Naoki Takezoe
47bc8d030e Merge pull request #590 from ghmer/master
Allow LDAPS connections instead of only allowing TLS enabled connections
2015-01-08 02:44:36 +09:00
Mark LaCroix
48fe7133f7 Add anonymous access option to tests 2015-01-07 09:47:36 -05:00
Mark LaCroix
5d962dc5e4 Add option to deny anonymous (i.e. unauthorized) access 2015-01-07 09:17:22 -05:00
Mario Enrico Ragucci
31e8e5a951 code alignment. We want a pretty pull request! 2015-01-07 07:46:59 +01:00
Mario Enrico Ragucci
858373c628 small beautifying change to have code properly aligned 2015-01-07 07:45:18 +01:00
Mario Enrico Ragucci
7f142d2c0d Introducing "Enable SSL" option on LDAP settings 2015-01-07 07:41:41 +01:00
Shintaro Murakami
08b86232a8 Merge pull request #589 from mrkm4ntr/toggle-line-notes
Add checkbox to toggle inline notes.
2015-01-06 23:48:11 +09:00
Shintaro Murakami
6bf4f42fdb Add checkbox to toggle line notes 2015-01-06 23:27:03 +09:00
Shintaro Murakami
f3c7de36d8 Remove filter setting for old plugin 2015-01-05 22:28:30 +09:00
Naoki Takezoe
19f556de57 Merge pull request #587 from mrkm4ntr/comment-for-split-diff
(refs #564) Comment for side-by-side diff available
2015-01-04 13:46:33 +09:00
Naoki Takezoe
e4467df411 Merge pull request #586 from team-lab/feature/add-stat-icon-on-diff
add icon on each diff header
2015-01-04 13:25:15 +09:00
Shintaro Murakami
8d305a1fb1 Merge branch 'master' of https://github.com/takezoe/gitbucket into comment-for-split-diff 2015-01-04 10:47:51 +09:00
Shintaro Murakami
b47153e645 Remove old plugin test 2015-01-04 10:40:10 +09:00
Shintaro Murakami
c71766c84b (refs #564) Comment for side-by-side diff available 2015-01-03 17:33:33 +09:00
Naoki Takezoe
23e4d679ae Merge branch 'purge-old-plugin-system' 2015-01-03 04:47:10 +09:00
Naoki Takezoe
182acb2e02 Trim each lines of command guidance 2015-01-02 19:11:50 +09:00
nazoking
b255b15006 add icon on diff view 2015-01-02 17:35:53 +09:00
Naoki Takezoe
b458f88161 Remove enable.plugin flag 2015-01-02 02:27:35 +09:00
Naoki Takezoe
398d8f2f1c Merge branch 'master' into purge-old-plugin-system 2015-01-02 02:03:00 +09:00
Naoki Takezoe
85c1a56cbf Purge old plugin system 2015-01-02 01:59:21 +09:00
Shintaro Murakami
da216c6960 (refs #585) Fix issue in markdown preview 2014-12-31 16:24:30 +09:00
Naoki Takezoe
bc91b153bf Merge pull request #574 from michaeljayt/add-fork-options
Add fork to group options
2014-12-31 01:08:27 +09:00
Shintaro Murakami
bc50b47d3a (refs #584) Fix the activity of commenting to pull request. 2014-12-31 00:27:47 +09:00
michaeljayt
aed15a7f25 Skip the group popup when user has no group 2014-12-30 14:26:30 +08:00
michaeljayt
a1f09117b0 Fix security issue on fork 2014-12-30 08:50:19 +08:00
michaeljayt
0a4a4a51ca Add fork to group options 2014-12-30 08:50:19 +08:00
nazoking
f7fd53bf09 add information to singup view 2014-12-29 20:54:22 +09:00
nazoking
cbfb863a54 Add information message to singin view. 2014-12-29 19:56:52 +09:00
Mike Slinn
2848f07b83 Merge remote-tracking branch 'upstream/master' 2014-11-08 04:11:55 -08:00
Mike Slinn
55224ddcd8 Changed Bootstrap's default color pink for code tags to match github's color 2014-11-08 04:07:14 -08:00
Rodrigo Lazoti
054ae75b6b Add repository's link to issues and pull request list on dashboard 2014-11-07 10:55:08 -02:00
Mike Slinn
a10188260c Update README.md 2014-10-03 15:26:42 -07:00
66 changed files with 1051 additions and 1268 deletions

View File

@@ -1,7 +1,7 @@
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 is the easily installable Github clone written with Scala.
GitBucket is the easily installable GitHub clone powered by Scala.
Features
@@ -79,6 +79,13 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 2.8 - 1 Feb 2015
- New logo and icons
- New system setting options to control visibility
- Comment on side-by-side diff
- Information message on sign-in page
- Fork repository by group account
### 2.7 - 29 Dec 2014
- Comment for commit and diff
- Fix security issue in markdown rendering

View File

@@ -8,6 +8,6 @@ Common scripts are in this directory.
This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
To run:
1. Edit `gitbucket.conf` to suit.
2. Type: `install`
1. Edit `gitbucket.conf` to suit.
2. Type: `install`

View File

@@ -44,7 +44,6 @@ object MyBuild extends Build {
"org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.4.180",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",

View File

@@ -0,0 +1 @@
ALTER TABLE COMMIT_COMMENT ALTER COLUMN FILE_NAME NVARCHAR(260);

View File

@@ -1,4 +1,4 @@
import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
import app._
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
@@ -10,12 +10,11 @@ class ScalatraBootstrap extends LifeCycle {
// Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
// Register controllers
context.mount(new AnonymousAccessController, "/*")
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")

View File

@@ -88,6 +88,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
case class AccountForm(accountName: String)
val accountForm = mapping(
"account" -> trim(label("Group/User name", text(required, validAccountName)))
)(AccountForm.apply)
/**
* Displays user information.
*/
@@ -129,8 +135,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_avatar"){
val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image =>
contentType = FileUtil.getMimeType(image)
new java.io.File(getUserUploadDir(userName), image)
RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image))
} getOrElse {
contentType = "image/png"
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
@@ -285,7 +290,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Show the new repository form.
*/
get("/new")(usersOnly {
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic)
})
/**
@@ -354,11 +359,31 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName)
groups match {
case _: List[String] =>
val managerPermissions = groups.map { group =>
val members = getGroupMembers(group)
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
}
_root_.helper.html.forkrepository(
repository,
(groups zip managerPermissions).toMap
)
case _ => redirect(s"/${loginUserName}")
}
})
LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}")
redirect(s"/${accountName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
@@ -366,7 +391,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
createRepository(
repositoryName = repository.name,
userName = loginUserName,
userName = accountName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
@@ -376,22 +401,22 @@ trait AccountControllerBase extends AccountManagementControllerBase {
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
insertDefaultLabels(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
getRepositoryDir(accountName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
getWikiRepositoryDir(accountName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
redirect(s"/${accountName}/${repository.name}")
}
}
})
@@ -431,4 +456,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case None => Some("Key is invalid.")
}
}
private def validAccountName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
getAccountByUserName(value) match {
case Some(_) => None
case None => Some("Invalid Group/User Account.")
}
}
}
}

View File

@@ -0,0 +1,14 @@
package app
class AnonymousAccessController extends AnonymousAccessControllerBase
trait AnonymousAccessControllerBase extends ControllerBase {
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register")) {
Unauthorized()
} else {
pass()
}
}
}

View File

@@ -134,6 +134,18 @@ abstract class ControllerBase extends ScalatraFilter
if (path.startsWith("http")) path
else baseUrl + super.url(path, params, false, false, false)
/**
* Use this method to response the raw data against XSS.
*/
protected def RawData[T](contentType: String, rawData: T): T = {
if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){
this.contentType = "text/plain"
} else {
this.contentType = contentType
}
response.addHeader("X-Content-Type-Options", "nosniff")
rawData
}
}
/**

View File

@@ -292,8 +292,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 =>
contentType = FileUtil.getMimeType(file.getName)
file
RawData(FileUtil.getMimeType(file.getName), file)
}
case _ => None
}) getOrElse NotFound

View File

@@ -21,7 +21,7 @@ import service.WebHookService.WebHookPayload
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService
/**
@@ -29,7 +29,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
*/
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
@@ -57,7 +57,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
oldLineNumber: Option[Int],
newLineNumber: Option[Int],
content: String,
pullRequest: Boolean
issueId: Option[Int]
)
val editorForm = mapping(
@@ -83,7 +83,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"oldLineNumber" -> trim(label("Old line number", optional(number()))),
"newLineNumber" -> trim(label("New line number", optional(number()))),
"content" -> trim(label("Content", text(required))),
"pullRequest" -> trim(label("In pull request", boolean()))
"issueId" -> trim(label("Issue Id", optional(number())))
)(CommentForm.apply)
/**
@@ -214,8 +214,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
RawData(FileUtil.getContentType(path, bytes), bytes)
}
} else {
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
@@ -247,20 +246,23 @@ trait RepositoryViewerControllerBase extends ControllerBase {
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
form.fileName, form.oldLineNumber, form.newLineNumber, form.pullRequest)
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
})
ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository =>
val id = params("id")
val fileName = params.get("fileName")
val oldLineNumber = params.get("oldLineNumber") flatMap {b => Some(b.toInt)}
val newLineNumber = params.get("newLineNumber") flatMap {b => Some(b.toInt)}
val pullRequest = params.get("pullRequest")
val oldLineNumber = params.get("oldLineNumber") map (_.toInt)
val newLineNumber = params.get("newLineNumber") map (_.toInt)
val issueId = params.get("issueId") map (_.toInt)
repo.html.commentform(
commitId = id,
fileName, oldLineNumber, newLineNumber, pullRequest.map(_.toBoolean).getOrElse(false),
fileName, oldLineNumber, newLineNumber, issueId,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
repository = repository
)
@@ -269,8 +271,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.pullRequest)
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
@@ -436,6 +441,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
context.loginAccount match {
case None => List()
case account: Option[model.Account] => getGroupsByUserName(account.get.userName)
}, // groups of current user
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
flash.get("info"), flash.get("error"))
@@ -486,6 +495,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// update pull request
updatePullRequests(repository.owner, repository.name, branch)
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
@@ -522,7 +534,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
val revision = name.stripSuffix(suffix)
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists) {
@@ -530,21 +542,23 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
workDir.mkdirs
val file = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
val filename = repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new java.io.FileOutputStream(file)) { out =>
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(out)
.call()
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
file
response.setHeader("Content-Disposition", s"attachment; filename=${filename}")
response.setBufferSize(1024 * 1024);
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(response.getOutputStream)
.call()
Unit
}
}

View File

@@ -3,15 +3,8 @@ package app
import service.{AccountService, SystemSettingsService}
import SystemSettingsService._
import util.AdminAuthenticator
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import ssh.SshServer
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import plugin.{Plugin, PluginSystem}
import org.scalatra.Ok
import util.Implicits._
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator
@@ -23,6 +16,8 @@ trait SystemSettingsControllerBase extends ControllerBase {
"baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"ssh" -> trim(label("SSH access", boolean())),
@@ -48,6 +43,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply).verifying { settings =>
@@ -85,118 +81,4 @@ trait SystemSettingsControllerBase extends ControllerBase {
redirect("/admin/system")
})
get("/admin/plugins")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins)
} else NotFound
})
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds)
installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
get("/admin/plugins/available")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
admin.plugins.html.available(availablePlugins)
} else NotFound
})
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
get("/admin/plugins/console")(adminOnly {
if(enablePluginSystem){
admin.plugins.html.console()
} else NotFound
})
post("/admin/plugins/console")(adminOnly {
if(enablePluginSystem){
val script = request.getParameter("script")
val result = plugin.ScalaPlugin.eval(script)
Ok()
} else NotFound
})
// TODO Move these methods to PluginSystem or Service?
private def deletePlugins(pluginIds: List[String]): Unit = {
pluginIds.foreach { pluginId =>
plugin.PluginSystem.uninstall(pluginId)
val dir = new java.io.File(PluginHome, pluginId)
if(dir.exists && dir.isDirectory){
FileUtils.deleteQuietly(dir)
PluginSystem.uninstall(pluginId)
}
}
}
private def installPlugins(pluginIds: List[String]): Unit = {
val dir = getPluginCacheDir()
val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id)
if(pluginDir.exists){
FileUtils.deleteDirectory(pluginDir)
}
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
PluginSystem.installPlugin(plugin.id)
}
}
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
val repositoryRoot = getPluginCacheDir()
if(repositoryRoot.exists && repositoryRoot.isDirectory){
PluginSystem.repositories.flatMap { repo =>
val repoDir = new java.io.File(repositoryRoot, repo.id)
if(repoDir.exists && repoDir.isDirectory){
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
val propertyFile = new java.io.File(plugin, "plugin.properties")
val properties = new java.util.Properties()
if(propertyFile.exists && propertyFile.isFile){
using(new FileInputStream(propertyFile)){ in =>
properties.load(in)
}
}
SystemSettingsControllerBase.AvailablePlugin(
repository = repo.id,
id = properties.getProperty("id"),
version = properties.getProperty("version"),
author = properties.getProperty("author"),
url = properties.getProperty("url"),
description = properties.getProperty("description"),
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
case Some(x) => "installed"
case None => "available"
})
}
} else Nil
}
} else Nil
}
}
object SystemSettingsControllerBase {
case class AvailablePlugin(repository: String, id: String, version: String,
author: String, url: String, description: String, status: String)
}

View File

@@ -164,8 +164,7 @@ trait WikiControllerBase extends ControllerBase {
val path = multiParams("splat").head
getFileContent(repository.owner, repository.name, path).map { bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
RawData(FileUtil.getContentType(path, bytes), bytes)
} getOrElse NotFound
})

View File

@@ -1,22 +0,0 @@
package plugin
import plugin.PluginSystem._
import java.sql.Connection
trait Plugin {
val id: String
val version: String
val author: String
val url: String
val description: String
def repositoryMenus : List[RepositoryMenu]
def globalMenus : List[GlobalMenu]
def repositoryActions : List[RepositoryAction]
def globalActions : List[Action]
def javaScripts : List[JavaScript]
}
object PluginConnectionHolder {
val threadLocal = new ThreadLocal[Connection]
}

View File

@@ -1,194 +0,0 @@
package plugin
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
import util.Directory._
import util.ControlUtil._
import org.apache.commons.io.{IOUtils, FileUtils}
import Security._
import service.PluginService
import model.Profile._
import profile.simple._
import java.io.FileInputStream
import java.sql.Connection
import app.Context
import service.RepositoryService.RepositoryInfo
/**
* Provides extension points to plug-ins.
*/
object PluginSystem extends PluginService {
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
private val initialized = new AtomicBoolean(false)
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
def install(plugin: Plugin): Unit = {
pluginsMap.put(plugin.id, plugin)
}
def plugins: List[Plugin] = pluginsMap.values.toList
def uninstall(id: String)(implicit session: Session): Unit = {
pluginsMap.remove(id)
// Delete from PLUGIN table
deletePlugin(id)
// Drop tables
val pluginDir = new java.io.File(PluginHome)
val sqlFile = new java.io.File(pluginDir, s"${id}/sql/drop.sql")
if(sqlFile.exists){
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
using(session.conn.createStatement()){ stmt =>
stmt.executeUpdate(sql)
}
}
}
def repositories: List[PluginRepository] = repositoriesList.toList
/**
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
*/
def init()(implicit session: Session): Unit = {
if(initialized.compareAndSet(false, true)){
// Load installed plugins
val pluginDir = new java.io.File(PluginHome)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
installPlugin(dir.getName)
}
}
// Add default plugin repositories
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
}
}
// TODO Method name seems to not so good.
def installPlugin(id: String)(implicit session: Session): Unit = {
val pluginHome = new java.io.File(PluginHome)
val pluginDir = new java.io.File(pluginHome, id)
val scalaFile = new java.io.File(pluginDir, "plugin.scala")
if(scalaFile.exists && scalaFile.isFile){
val properties = new java.util.Properties()
using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
properties.load(in)
}
val pluginId = properties.getProperty("id")
val version = properties.getProperty("version")
val author = properties.getProperty("author")
val url = properties.getProperty("url")
val description = properties.getProperty("description")
val source = s"""
|val id = "${pluginId}"
|val version = "${version}"
|val author = "${author}"
|val url = "${url}"
|val description = "${description}"
""".stripMargin + FileUtils.readFileToString(scalaFile, "UTF-8")
try {
// Compile and eval Scala source code
ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
ScalaPlugin.compileTemplate(
id.replace("-", ""),
file.getName.stripSuffix(".scala.html"),
IOUtils.toString(new FileInputStream(file)))
}.mkString("\n") + source)
// Migrate database
val plugin = getPlugin(pluginId)
if(plugin.isEmpty){
registerPlugin(model.Plugin(pluginId, version))
migrate(session.conn, pluginId, "0.0")
} else {
updatePlugin(model.Plugin(pluginId, version))
migrate(session.conn, pluginId, plugin.get.version)
}
} catch {
case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e)
}
}
}
// TODO Should PluginSystem provide a way to migrate resources other than H2?
private def migrate(conn: Connection, pluginId: String, current: String): Unit = {
val pluginDir = new java.io.File(PluginHome)
// TODO Is ot possible to use this migration system in GitBucket migration?
val dim = current.split("\\.")
val currentVersion = Version(dim(0).toInt, dim(1).toInt)
val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql")
if(sqlDir.exists && sqlDir.isDirectory){
sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file =>
val array = file.getName.replaceFirst("\\.sql", "").split("_")
Version(array(0).toInt, array(1).toInt)
}
.sorted.reverse.takeWhile(_ > currentVersion)
.reverse.foreach { version =>
val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql")
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
using(conn.createStatement()){ stmt =>
stmt.executeUpdate(sql)
}
}
}
}
case class Version(major: Int, minor: Int) extends Ordered[Version] {
override def compare(that: Version): Int = {
if(major != that.major){
major.compare(that.major)
} else{
minor.compare(that.minor)
}
}
def displayString: String = major + "." + minor
}
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
def javaScripts : List[JavaScript] = pluginsMap.values.flatMap(_.javaScripts).toList
// Case classes to hold plug-ins information internally in GitBucket
case class PluginRepository(id: String, url: String)
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
case class Action(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any)
case class RepositoryAction(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any)
case class Button(label: String, href: String)
case class JavaScript(filter: String => Boolean, script: String)
/**
* Checks whether the plugin is updatable.
*/
def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
if(oldVersion == newVersion){
false
} else {
val dim1 = oldVersion.split("\\.").map(_.toInt)
val dim2 = newVersion.split("\\.").map(_.toInt)
dim1.zip(dim2).foreach { case (a, b) =>
if(a < b){
return true
} else if(a > b){
return false
}
}
return false
}
}
}

View File

@@ -1,66 +0,0 @@
package plugin
import util.Directory._
import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
import org.quartz.{Scheduler, JobExecutionContext, Job}
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
class PluginUpdateJob extends Job {
private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
private var failedCount = 0
/**
* Clone or pull all plugin repositories
*
* TODO Support plugin repository access through the proxy server
*/
override def execute(context: JobExecutionContext): Unit = {
try {
if(failedCount > 3){
logger.error("Skip plugin information updating because failed count is over limit")
} else {
logger.info("Start plugin information updating")
PluginSystem.repositories.foreach { repository =>
logger.info(s"Updating ${repository.id}: ${repository.url}...")
val dir = getPluginCacheDir()
val repo = new java.io.File(dir, repository.id)
if(repo.exists){
// pull if the repository is already cloned
Git.open(repo).pull().call()
} else {
// clone if the repository is not exist
Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
}
}
logger.info("End plugin information updating")
}
} catch {
case e: Exception => {
failedCount = failedCount + 1
logger.error("Failed to update plugin information", e)
}
}
}
}
object PluginUpdateJob {
def schedule(scheduler: Scheduler): Unit = {
val job = newJob(classOf[PluginUpdateJob])
.withIdentity("pluginUpdateJob")
.build()
val trigger = newTrigger()
.withIdentity("pluginUpdateTrigger")
.startNow()
.withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
.build()
scheduler.scheduleJob(job, trigger)
}
}

View File

@@ -1,77 +0,0 @@
package plugin
import scala.collection.mutable.ListBuffer
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import app.Context
import plugin.PluginSystem._
import plugin.PluginSystem.RepositoryMenu
import plugin.Security._
import service.RepositoryService.RepositoryInfo
import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox
import play.twirl.compiler.TwirlCompiler
import scala.io.Codec
// TODO This is a sample implementation for Scala based plug-ins.
class ScalaPlugin(val id: String, val version: String,
val author: String, val url: String, val description: String) extends Plugin {
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[RepositoryAction]()
private val globalActionList = ListBuffer[Action]()
private val javaScriptList = ListBuffer[JavaScript]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
def javaScripts : List[JavaScript] = javaScriptList.toList
def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
}
def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
globalMenuList += GlobalMenu(label, url, icon, condition)
}
def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
globalActionList += Action(method, path, security, function)
}
def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = {
repositoryActionList += RepositoryAction(method, path, security, function)
}
def addJavaScript(filter: String => Boolean, script: String): Unit = {
javaScriptList += JavaScript(filter, script)
}
}
object ScalaPlugin {
def define(id: String, version: String, author: String, url: String, description: String)
= new ScalaPlugin(id, version, author, url, description)
def eval(source: String): Any = {
val toolbox = currentMirror.mkToolBox()
val tree = toolbox.parse(source)
toolbox.eval(tree)
}
def compileTemplate(packageName: String, name: String, source: String): String = {
val result = TwirlCompiler.parseAndGenerateCodeNewParser(
Array(packageName, name),
source.getBytes("UTF-8"),
Codec(scala.util.Properties.sourceEncoding),
"",
"play.twirl.api.HtmlFormat.Appendable",
"play.twirl.api.HtmlFormat",
"",
false)
result.replaceFirst("package .*", "")
}
}

View File

@@ -1,36 +0,0 @@
package plugin
/**
* Defines enum case classes to specify permission for actions which is provided by plugin.
*/
object Security {
sealed trait Security
/**
* All users and guests
*/
case class All() extends Security
/**
* Only signed-in users
*/
case class Login() extends Security
/**
* Only repository owner and collaborators
*/
case class Member() extends Security
/**
* Only repository owner and managers of group repository
*/
case class Owner() extends Security
/**
* Only administrators
*/
case class Admin() extends Security
}

View File

@@ -1,56 +0,0 @@
import java.sql.PreparedStatement
import play.twirl.api.Html
import util.ControlUtil._
import scala.collection.mutable.ListBuffer
package object plugin {
case class Redirect(path: String)
case class Fragment(html: Html)
case class RawData(contentType: String, content: Array[Byte])
object db {
// TODO labelled place holder support
def select(sql: String, params: Any*): Seq[Map[String, String]] = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
setParams(stmt, params: _*)
using(stmt.executeQuery()){ rs =>
val list = new ListBuffer[Map[String, String]]()
while(rs.next){
defining(rs.getMetaData){ meta =>
val map = Range(1, meta.getColumnCount + 1).map { i =>
val name = meta.getColumnName(i)
(name, rs.getString(name))
}.toMap
list += map
}
}
list
}
}
}
}
// TODO labelled place holder support
def update(sql: String, params: Any*): Int = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
setParams(stmt, params: _*)
stmt.executeUpdate()
}
}
}
private def setParams(stmt: PreparedStatement, params: Any*): Unit = {
params.zipWithIndex.foreach { case (p, i) =>
p match {
case x: String => stmt.setString(i + 1, x)
case x: Int => stmt.setInt(i + 1, x)
case x: Boolean => stmt.setBoolean(i + 1, x)
}
}
}
}
}

View File

@@ -160,10 +160,10 @@ trait ActivityService {
None,
currentDate)
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String)(implicit s: Session): Unit =
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
None,
currentDate)

View File

@@ -3,6 +3,7 @@ package service
import model.Profile._
import profile.simple._
import model.{PullRequest, Issue}
import util.JGitUtil
trait PullRequestService { self: IssuesService =>
import PullRequestService._
@@ -81,6 +82,18 @@ trait PullRequestService { self: IssuesService =>
.map { case (t1, t2) => t1 }
.list
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){
val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest(
pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.issueId,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
}
}
}
object PullRequestService {

View File

@@ -2,7 +2,7 @@ package service
import model.Profile._
import profile.simple._
import model.{Repository, Account, Collaborator}
import model.{Repository, Account, Collaborator, Label}
import util.JGitUtil
trait RepositoryService { self: AccountService =>
@@ -94,9 +94,17 @@ trait RepositoryService { self: AccountService =>
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = 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
IssueLabels.insertAll(issueLabels.map(x => x.copy(
labelId = newLabelMap(oldLabelMap(x.labelId)),
userName = newUserName,
repositoryName = newRepositoryName
)) :_*)
if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
@@ -189,7 +197,7 @@ trait RepositoryService { self: AccountService =>
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
issues.size,
issues.count(_ == false),
issues.count(_ == true),
getForkedCount(
repository.originUserName.getOrElse(repository.userName),

View File

@@ -14,6 +14,8 @@ trait SystemSettingsService {
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
settings.information.foreach(x => props.setProperty(Information, x))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
props.setProperty(Ssh, settings.ssh.toString)
@@ -42,6 +44,7 @@ trait SystemSettingsService {
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x))
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
ldap.ssl.foreach(x => props.setProperty(LdapSsl, x.toString))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
}
@@ -63,6 +66,8 @@ trait SystemSettingsService {
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
getValue(props, Ssh, false),
@@ -92,6 +97,7 @@ trait SystemSettingsService {
getOptionValue(props, LdapFullNameAttribute, None),
getOptionValue(props, LdapMailAddressAttribute, None),
getOptionValue[Boolean](props, LdapTls, None),
getOptionValue[Boolean](props, LdapSsl, None),
getOptionValue(props, LdapKeystore, None)))
} else {
None
@@ -109,6 +115,8 @@ object SystemSettingsService {
baseUrl: Option[String],
information: Option[String],
allowAccountRegistration: Boolean,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean,
gravatar: Boolean,
notification: Boolean,
ssh: Boolean,
@@ -134,6 +142,7 @@ object SystemSettingsService {
fullNameAttribute: Option[String],
mailAttribute: Option[String],
tls: Option[Boolean],
ssl: Option[Boolean],
keystore: Option[String])
case class Smtp(
@@ -152,6 +161,8 @@ object SystemSettingsService {
private val BaseURL = "base_url"
private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration"
private val AllowAnonymousAccess = "allow_anonymous_access"
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val Ssh = "ssh"
@@ -174,6 +185,7 @@ object SystemSettingsService {
private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val LdapTls = "ldap.tls"
private val LdapSsl = "ldap.ssl"
private val LdapKeystore = "ldap.keystore"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
@@ -195,7 +207,7 @@ object SystemSettingsService {
else value
}
// TODO temporary flag
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
// // TODO temporary flag
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
}

View File

@@ -11,8 +11,6 @@ import util.ControlUtil._
import util.JDBCUtil._
import org.eclipse.jgit.api.Git
import util.Directory
import plugin.PluginUpdateJob
import service.SystemSettingsService
object AutoUpdate {
@@ -54,6 +52,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(2, 8),
new Version(2, 7) {
override def update(conn: Connection): Unit = {
super.update(conn)
@@ -197,11 +196,10 @@ object AutoUpdate {
* Update database schema automatically in the context initializing.
*/
class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory
import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private val scheduler = StdSchedulerFactory.getDefaultScheduler
// private val scheduler = StdSchedulerFactory.getDefaultScheduler
override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
@@ -236,31 +234,9 @@ class AutoUpdateListener extends ServletContextListener {
}
logger.debug("End schema update")
}
if(SystemSettingsService.enablePluginSystem){
getDatabase(context).withSession { implicit session =>
logger.debug("Starting plugin system...")
try {
plugin.PluginSystem.init()
scheduler.start()
PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
} catch {
case ex: Throwable => {
logger.error("Failed to initialize plugin system", ex)
ex.printStackTrace()
throw ex
}
}
}
}
}
def contextDestroyed(sce: ServletContextEvent): Unit = {
scheduler.shutdown()
}
private def getConnection(servletContext: ServletContext): Connection =

View File

@@ -28,33 +28,45 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
override def setCharacterEncoding(encoding: String) = {}
}
val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString)
val settings = loadSystemSettings()
try {
defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
chain.doFilter(req, wrappedResponse)
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => getWritableUser(username, password, repository) match {
case Some(account) => {
request.setAttribute(Keys.Request.UserName, account.userName)
chain.doFilter(req, wrappedResponse)
defining(request.paths){
case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
case Some(repository) => {
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
chain.doFilter(req, wrappedResponse)
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => {
authenticate(settings, username, password) match {
case Some(account) => {
if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
}
chain.doFilter(req, wrappedResponse)
}
case None => requireAuth(response)
}
}
case None => requireAuth(response)
case _ => requireAuth(response)
}
case _ => requireAuth(response)
}
}
}
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
case _ => {
logger.debug(s"Not enough path arguments: ${request.paths}")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
} catch {
@@ -65,13 +77,6 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
}
}
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
(implicit session: Session): Option[Account] =
authenticate(loadSystemSettings(), username, password) match {
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
case _ => None
}
private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)

View File

@@ -174,7 +174,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
case ReceiveCommand.Type.CREATE |
ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(branchName)
updatePullRequests(owner, repository, branchName)
case _ =>
}
}
@@ -211,26 +211,4 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
}
}
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
oldGit.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
.call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
pullreq.userName, pullreq.repositoryName, pullreq.branch,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
}
}
}
}

View File

@@ -1,192 +0,0 @@
package servlet
import javax.servlet._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.apache.commons.io.IOUtils
import play.twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService}
import model.{Account, Session}
import util.{JGitUtil, Keys}
import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
import service.RepositoryService.RepositoryInfo
import plugin.Security._
class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
(req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => {
Database(req.getServletContext) withTransaction { implicit session =>
val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res)
}
}
}
}
}
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: Session): Boolean = {
plugin.PluginSystem.globalActions.find(x =>
x.method.toLowerCase == request.getMethod.toLowerCase && path.matches(x.path)
).map { action =>
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
if(authenticate(action.security, context)){
val result = try {
PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, context)
} finally {
PluginConnectionHolder.threadLocal.remove()
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
true
} getOrElse false
}
private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: Session): Boolean = {
val elements = path.split("/")
if(elements.length > 3){
val owner = elements(1)
val name = elements(2)
val remain = elements.drop(3).mkString("/", "/", "")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository =>
plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action =>
if(authenticate(action.security, context, repository)){
val result = try {
PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, context, repository)
} finally {
PluginConnectionHolder.threadLocal.remove()
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
true
}
} getOrElse false
} else false
}
private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
context: app.Context): Unit = {
result match {
case null|None => renderError(request, response, context, 404)
case x: String => renderGlobalHtml(request, response, context, x)
case Some(x: String) => renderGlobalHtml(request, response, context, x)
case x: Html => renderGlobalHtml(request, response, context, x.toString)
case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString)
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString)
case x: RawData => renderRawData(request, response, context, x)
case Some(x: RawData) => renderRawData(request, response, context, x)
case x: Redirect => response.sendRedirect(x.path)
case Some(x: Redirect) => response.sendRedirect(x.path)
case x: AnyRef => renderJson(request, response, x)
}
}
/**
* Authentication for global action
*/
private def authenticate(security: Security, context: app.Context)(implicit session: Session): Boolean = {
// Global Action
security match {
case All() => true
case Login() => context.loginAccount.isDefined
case Admin() => context.loginAccount.exists(_.isAdmin)
case _ => false // TODO throw Exception?
}
}
/**
* Authenticate for repository action
*/
private def authenticate(security: Security, context: app.Context, repository: RepositoryInfo)(implicit session: Session): Boolean = {
if(repository.repository.isPrivate){
// Private Repository
security match {
case Admin() => context.loginAccount.exists(_.isAdmin)
case Owner() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
}
case _ => context.loginAccount.exists { account =>
account.isAdmin || account.userName == repository.owner ||
getCollaborators(repository.owner, repository.name).contains(account.userName)
}
}
} else {
// Public Repository
security match {
case All() => true
case Login() => context.loginAccount.isDefined
case Owner() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
}
case Member() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getCollaborators(repository.owner, repository.name).contains(account.userName)
}
case Admin() => context.loginAccount.exists(_.isAdmin)
}
}
}
private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = {
response.sendError(error)
}
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
val html = _root_.html.main("GitBucket", None)(Html(body))(context)
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
}
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
response.setContentType(rawData.contentType)
IOUtils.write(rawData.content, response.getOutputStream)
}
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write
implicit val formats = Serialization.formats(NoTypeHints)
val json = write(obj)
response.setContentType("application/json; charset=UTF-8")
IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream)
}
}

View File

@@ -13,6 +13,7 @@ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec
import java.util.Date
import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
import service.RepositoryService
@@ -674,6 +675,25 @@ object JGitUtil {
}.head.id
}
/**
* Fetch pull request contents into refs/pull/${issueId}/head and return (commitIdTo, commitIdFrom)
*/
def updatePullRequest(userName: String, repositoryName:String, branch: String, issueId: Int,
requestUserName: String, requestRepositoryName: String, requestBranch: String):(String, String) =
using(Git.open(Directory.getRepositoryDir(userName, repositoryName)),
Git.open(Directory.getRepositoryDir(requestUserName, requestRepositoryName))){ (oldGit, newGit) =>
oldGit.fetch
.setRemote(Directory.getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head").setForceUpdate(true))
.call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${issueId}/head").getName
val commitIdFrom = getForkedCommitId(oldGit, newGit,
userName, repositoryName, branch,
requestUserName, requestRepositoryName, requestBranch)
(commitIdTo, commitIdFrom)
}
/**
* Returns the last modified commit of specified path
* @param git the Git object

View File

@@ -48,6 +48,7 @@ object LDAPUtil {
dn = ldapSettings.bindDN.getOrElse(""),
password = ldapSettings.bindPassword.getOrElse(""),
tls = ldapSettings.tls.getOrElse(false),
ssl = ldapSettings.ssl.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "System LDAP authentication failed."
){ conn =>
@@ -65,6 +66,7 @@ object LDAPUtil {
dn = userDN,
password = password,
tls = ldapSettings.tls.getOrElse(false),
ssl = ldapSettings.ssl.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed."
){ conn =>
@@ -96,7 +98,7 @@ object LDAPUtil {
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
}
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, ssl: Boolean, keystore: String, error: String)
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) {
// Dynamically set Sun as the security provider
@@ -109,7 +111,13 @@ object LDAPUtil {
}
}
val conn: LDAPConnection = new LDAPConnection(new LDAPJSSEStartTLSFactory())
val conn: LDAPConnection =
if(ssl) {
new LDAPConnection(new LDAPJSSESecureSocketFactory())
}else {
new LDAPConnection(new LDAPJSSEStartTLSFactory())
}
try {
// Connect to the server
conn.connect(host, port)

View File

@@ -18,9 +18,14 @@ object Markdown {
/**
* Converts Markdown of Wiki pages to HTML.
*/
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean,
enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = {
def toHtml(markdown: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: app.Context): String = {
// escape issue id
val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
@@ -35,12 +40,16 @@ object Markdown {
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML
).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages).toHtml(rootNode)
}
}
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean) extends LinkRenderer with WikiService {
class GitBucketLinkRender(
context: app.Context,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
pages: List[String]) extends LinkRenderer with WikiService {
override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){
try {
@@ -54,7 +63,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){
if(pages.contains(page)){
new Rendering(url, label)
} else {
new Rendering(url, label).withAttribute("class", "absent")
@@ -91,9 +100,10 @@ class GitBucketHtmlSerializer(
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean
hasWritePermission: Boolean,
pages: List[String]
)(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink),
new GitBucketLinkRender(context, repository, enableWikiLink, pages),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache {
@@ -185,6 +195,32 @@ class GitBucketHtmlSerializer(
printTag(node, "li")
}
}
override def visit(node: ExpLinkNode) {
printLink(linkRenderer.render(node, printLinkChildrenToString(node)))
}
def printLinkChildrenToString(node: SuperNode) = {
val priorPrinter = printer
printer = new Printer()
visitLinkChildren(node)
val result = printer.getString()
printer = priorPrinter
result
}
def visitLinkChildren(node: SuperNode) {
import scala.collection.JavaConversions._
node.getChildren.foreach(child => child match {
case node: ExpImageNode => visitLinkChild(node)
case node: SuperNode => visitLinkChildren(node)
case _ => child.accept(this)
})
}
def visitLinkChild(node: ExpImageNode) {
printer.print("<img src=\"").print(fixUrl(node.url, true)).print("\" alt=\"").printEncoded(printChildrenToString(node)).print("\"/>")
}
}
object GitBucketHtmlSerializer {

View File

@@ -38,10 +38,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
/**
*
* Format java.util.Date to "x {seconds/minutes/hours/days} ago"
* If duration over 1 month, format to "d MMM (yyyy)"
*
*/
def datetimeAgoRecentOnly(date: Date): String = {
val duration = new Date().getTime - date.getTime
@@ -79,7 +77,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] =
Seq(
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context))
)
@@ -88,9 +86,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
def markdown(value: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo,
@@ -146,12 +149,12 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/
def activityMessage(message: String)(implicit context: app.Context): Html =
Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
)
@@ -247,6 +250,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
}
def pre(value: Html): Html = Html(s"<pre>${value.body.trim.split("\n").map(_.trim).mkString("\n")}</pre>")
/**
* Implicit conversion to add mkHtml() to Seq[Html].
*/

View File

@@ -1,4 +1,5 @@
@(groupNames: List[String])(implicit context: app.Context)
@(groupNames: List[String],
isCreateRepoOptionPublic: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Create a New Repository"){
@@ -29,7 +30,7 @@
</fieldset>
<fieldset class="margin">
<label class="radio">
<input type="radio" name="isPrivate" value="false" checked>
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}>
<span class="strong"><img src="@assets/common/images/repo_public.png"/>&nbsp;</i>&nbsp;Public</span><br>
<div>
<span>All users and guests can read this repository.</span>
@@ -38,7 +39,7 @@
</fieldset>
<fieldset>
<label class="radio">
<input type="radio" name="isPrivate" value="true">
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
<span class="strong"><img src="@assets/common/images/repo_private.png"/>&nbsp;</i>&nbsp;Private</span><br>
<div>
<span>Only collaborators can read this repository.</span>

View File

@@ -11,11 +11,6 @@
<li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a>
</li>
@if(service.SystemSettingsService.enablePluginSystem){
<li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a>
</li>
}
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>

View File

@@ -1,37 +0,0 @@
@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins"){
@tab("available")
<form action="@path/admin/plugins/_install" method="POST" validate="true">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Version</th>
<th>Provider</th>
<th>Description</th>
</tr>
@plugins.zipWithIndex.map { case (plugin, i) =>
<tr>
<td>
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
@plugin.id
</td>
<td>@plugin.version</td>
<td><a href="@plugin.url">@plugin.author</a></td>
<td>@plugin.description</td>
</tr>
}
</table>
<input type="submit" id="install-plugins" class="btn btn-success" value="Install selected plugins"/>
</form>
}
}
<script>
$(function(){
$('#install-plugins').click(function(){
return confirm('Selected plugin will be installed. Are you sure?');
});
});
</script>

View File

@@ -1,37 +0,0 @@
@()(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("JavaScript Console"){
@admin.html.menu("plugins"){
@tab("console")
<form method="POST">
<div class="box">
<div class="box-header">JavaScript Console</div>
<div class="box-content">
<div id="editor" style="width: 100%; height: 400px;"></div>
</div>
</div>
<fieldset>
<input type="button" id="evaluate" class="btn btn-success" value="Evaluate"/>
</fieldset>
</form>
}
}
<script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script>
$(function(){
var editor = ace.edit("editor");
editor.setTheme("ace/theme/monokai");
editor.getSession().setMode("ace/mode/javascript");
$('#evaluate').click(function(){
$.post('@path/admin/plugins/console', {
script: editor.getValue()
}, function(data){
alert('Success: ' + data);
}).fail(function(error){
alert(error.statusText);
});
});
});
</script>

View File

@@ -1,47 +0,0 @@
@(plugins: List[plugin.Plugin],
updatablePlugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins"){
@tab("installed")
<form method="POST" validate="true">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Version</th>
<th>Provider</th>
<th>Description</th>
</tr>
@plugins.zipWithIndex.map { case (plugin, i) =>
<tr>
<td>
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
@plugin.id
</td>
<td>
@plugin.version
@updatablePlugins.find(_.id == plugin.id).map { x =>
(@x.version is available)
}
</td>
<td><a href="@plugin.url">@plugin.author</a></td>
<td>@plugin.description</td>
</tr>
}
</table>
<input type="submit" id="update-plugins" class="btn btn-success" value="Update selected plugins" formaction="@path/admin/plugins/_update"/>
<input type="submit" id="delete-plugins" class="btn btn-danger" value="Uninstall selected plugins" formaction="@path/admin/plugins/_delete"/>
</form>
}
}
<script>
$(function(){
$('#update-plugins').click(function(){
return confirm('Selected plugin will be updated. Are you sure?');
});
$('#delete-plugins').click(function(){
return confirm('Selected plugin will be removed permanently. Are you sure?');
});
});
</script>

View File

@@ -1,9 +0,0 @@
@(active: String)(implicit context: app.Context)
@import context._
<ul class="nav nav-tabs">
<li@if(active == "installed"){ class="active"}><a href="@path/admin/plugins">Installed plugins</a></li>
<li@if(active == "available"){ class="active"}><a href="@path/admin/plugins/available">Available plugins</a></li>
@*
<li@if(active == "console" ){ class="active"}><a href="@path/admin/plugins/console">JavaScript console</a></li>
*@
</ul>

View File

@@ -53,6 +53,33 @@
<span class="strong">Deny</span> - Only administrators can create accounts.
</label>
</fieldset>
<hr>
<label class="strong">Default option to create a new repository</label>
<fieldset>
<label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(settings.isCreateRepoOptionPublic){ checked}>
<span class="strong">Public</span> - All users and guests can read that repository.
</label>
<label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!settings.isCreateRepoOptionPublic){ checked}>
<span class="strong">Private</span> - Only collaborators can read that repository.
</label>
</fieldset>
<!--====================================================================-->
<!-- Anonymous access -->
<!--====================================================================-->
<hr>
<label class="strong">Anonymous access</label>
<fieldset>
<label class="radio">
<input type="radio" name="allowAnonymousAccess" value="true"@if(settings.allowAnonymousAccess){ checked}>
<span class="strong">Allow</span> - Anyone can view public repositories, user/group profiles.
</label>
<label class="radio">
<input type="radio" name="allowAnonymousAccess" value="false"@if(!settings.allowAnonymousAccess){ checked}>
<span class="strong">Deny</span> - Users must authenticate before viewing any information
</label>
</fieldset>
<!--====================================================================-->
<!-- Services -->
<!--====================================================================-->
@@ -169,6 +196,13 @@
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="ldap.ssl"@if(settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Keystore</label>
<div class="controls">

View File

@@ -8,13 +8,6 @@
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
@*
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li>
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li>
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li>
</ul>
*@
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
@@ -29,7 +22,7 @@
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65;
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {

View File

@@ -11,6 +11,6 @@
@dashboard.html.tab("pulls")
<div class="container">
@issuesnavi(filter, "pulls", condition)
@pullslist(issues, page, openCount, closedCount, condition, filter, groups)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
}

View File

@@ -1,67 +0,0 @@
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
filter: String,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
@*
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/pulls/created_by@condition.toURL">Created</a></li>
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/pulls/assigned@condition.toURL">Assigned</a></li>
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/pulls/mentioned@condition.toURL">Mentioned</a></li>
<li class="pull-right">
<div class="input-prepend" style="margin-bottom: 0px;">
<div class="btn-group">
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
Filter
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="?q=is:open">Open issues and pull requests</a></li>
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
</ul>
</div>
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
</div>
</li>
</ul>
*@
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
@dashboard.html.header(openCount, closedCount, condition, groups)
</th>
</tr>
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td>
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
<span class="pull-right muted">#@issue.issueId</span>
<div style="margin-left: 20px;">
@issue.content.map { content =>
@cut(content, 90)
}.getOrElse {
<span class="muted">No description available</span>
}
</div>
<div class="small muted" style="margin-left: 20px;">
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
</div>

View File

@@ -3,7 +3,7 @@
newCommitId: Option[String],
oldCommitId: Option[String],
showIndex: Boolean,
pullRequest: Boolean,
issueId: Option[Int],
hasWritePermission: Boolean,
showLineNotes: Boolean)(implicit context: app.Context)
@import context._
@@ -46,25 +46,32 @@
<tr>
<th style="font-weight: normal; line-height: 27px;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@diff.oldPath -> @diff.newPath
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
@if(newCommitId.isDefined){
<div class="pull-right align-right">
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div>
}
}
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
@diff.newPath
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/>
}else{
<img src="@assets/common/images/diff_edit.png"/>
} @diff.newPath
@if(newCommitId.isDefined){
<div class="pull-right align-right">
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div>
}
}
@if(diff.changeType == ChangeType.DELETE){
@diff.oldPath
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
@if(oldCommitId.isDefined){
<div class="pull-right align-right">
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a>
</div>
}
@@ -113,7 +120,15 @@ $(function(){
renderDiffs(0);
});
$('.toggle-notes').change(function() {
if (!$(this).prop('checked')) {
$(this).closest('table').find('.not-diff.inline-comment-form').remove();
}
$(this).closest('table').find('.not-diff').toggle();
});
function renderDiffs(viewType){
window.viewType = viewType;
@diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){
@@ -122,26 +137,47 @@ $(function(){
}
}
@if(showLineNotes){
function getInlineContainer(where) {
if (viewType == 0) {
if (where === 'new') {
return $('<tr class="not-diff"><td colspan="2"></td><td colspan="2" class="comment-box-container"></td></tr>');
} else if (where === 'old') {
return $('<tr class="not-diff"><td colspan="2" class="comment-box-container"></td><td colspan="2"></td></tr>');
}
}
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
}
$('.inline-comment').each(function(i, v) {
var $v = $(v), filename = $v.attr('filename'),
oldline = $v.attr('oldline'), newline = $v.attr('newline'),
tmp = $('<tr class="not-diff"><td colspan="3" style="white-space: initial; line-height: initial; padding: 10px;"></td></tr>');
tmp.children('td').html($(this).clone().show());
oldline = $v.attr('oldline'), newline = $v.attr('newline');
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
$(this).hide();
}
var tmp;
var diff;
if (typeof oldline !== 'undefined') {
$('table[filename="' + filename + '"]').find('table.inlinediff').find('.oldline').filter(function() {
return new RegExp('^' + oldline + '$').test($(this).text());
}).parent().nextAll(':not(.not-diff):first').before(tmp);
if (typeof newline !== 'undefined') {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
tmp.children('td:first').html($(this).clone().show());
diff = $('table[filename="' + filename + '"]');
diff.find('table.diff').find('.oldline[line-number=' + oldline + ']')
.parent().nextAll(':not(.not-diff):first').before(tmp);
} else {
$('table[filename="' + filename + '"]').find('table.inlinediff').find('.newline').filter(function() {
return new RegExp('^' + newline + '\\+$').test($(this).text());
}).parent().nextAll(':not(.not-diff):first').before(tmp);
tmp = getInlineContainer('new');
tmp.children('td:last').html($(this).clone().show());
diff = $('table[filename="' + filename + '"]');
diff.find('table.diff').find('.newline[line-number=' + newline + ']')
.parent().nextAll(':not(.not-diff):first').before(tmp);
}
if (!diff.find('.toggle-notes').prop('checked')) {
tmp.hide();
}
});
@if(hasWritePermission) {
$('table.diff tr').hover(
$('table.diff td').hover(
function() {
$(this).find('b').css('display', 'inline-block');
},
@@ -149,32 +185,60 @@ $(function(){
$(this).find('b').css('display', 'none');
}
);
$('.add-comment').click(function() {
var $this = $(this),
$tr = $(this).closest('tr');
if (!$tr.nextAll(':not(.not-diff):first').prev().hasClass('inline-comment-form')) {
var commitId = $(this).closest('.table-bordered').attr('commitId'),
fileName = $(this).closest('.table-bordered').attr('fileName'),
oldLineNumber = $(this).closest('.newline').prev('.oldline').text(),
newLineNumber = $(this).closest('.newline').clone().children().remove().end().text(),
url = '@url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName + '&pullRequest=@pullRequest';
if (!isNaN(oldLineNumber) && oldLineNumber != null && oldLineNumber !== '') {
url += ('&oldLineNumber=' + oldLineNumber)
$('table.diff th').hover(
function() {
$(this).nextAll().find('b').first().css('display', 'inline-block');
},
function() {
$(this).nextAll().find('b').first().css('display', 'none');
}
if (!isNaN(newLineNumber) && newLineNumber != null && newLineNumber !== '') {
url += ('&newLineNumber=' + newLineNumber)
);
$('.add-comment').click(function() {
var $this = $(this),
$tr = $this.closest('tr'),
$check = $this.closest('table:not(.diff)').find('.toggle-notes');
if (!$check.prop('checked')) {
$check.prop('checked', true).trigger('change');
}
$.get(
url,
{
dataType : 'html'
},
function(responseContent) {
$this.hide();
var tmp = $('<tr class="inline-comment-form not-diff"><td colspan="3" style="white-space: initial; padding: 10px;"></td></tr>');
tmp.children('td').html(responseContent);
$tr.nextAll(':not(.not-diff):first').before(tmp);
});
if (!$tr.nextAll(':not(.not-diff):first').prev().hasClass('inline-comment-form')) {
var commitId = $this.closest('.table-bordered').attr('commitId'),
fileName = $this.closest('.table-bordered').attr('fileName'),
oldLineNumber, newLineNumber,
url = '@url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
if (viewType == 0) {
oldLineNumber = $this.parent().prev('.oldline').attr('line-number');
newLineNumber = $this.parent().prev('.newline').attr('line-number');
} else {
oldLineNumber = $this.parent().prevAll('.oldline').attr('line-number');
newLineNumber = $this.parent().prevAll('.newline').attr('line-number');
}
if (!isNaN(oldLineNumber) && oldLineNumber) {
url += ('&oldLineNumber=' + oldLineNumber)
}
if (!isNaN(newLineNumber) && newLineNumber) {
url += ('&newLineNumber=' + newLineNumber)
}
$.get(
url,
{
dataType : 'html'
},
function(responseContent) {
$this.hide();
var tmp;
if (!isNaN(oldLineNumber) && oldLineNumber) {
if (!isNaN(newLineNumber) && newLineNumber) {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
} else {
tmp = getInlineContainer('new');
}
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
$tr.nextAll(':not(.not-diff):first').before(tmp);
}
);
}
});
$('table.diff').on('click', '.btn-default', function() {

View File

@@ -0,0 +1,18 @@
@(repository: service.RepositoryService.RepositoryInfo,
groupAndPerm: Map[String, Boolean])(implicit context: app.Context)
@import context._
@import view.helpers._
<h2 class="facebox-header">Where should we fork this repository?</h2>
<form action="@url(repository)/fork" id="fork" method="post">
<div class="owner-select-grid">
<div class="owner-select-target js-fork-owner-select-target enabled">@avatar(loginAccount.get.userName, 100)<span class="owner css-truncate" title="@@@loginAccount.get.userName">@@@loginAccount.get.userName</span></div>
@for((groupName, isManager) <- groupAndPerm) {
@if(isManager) {
<div class="owner-select-target js-fork-owner-select-target enabled">@avatar(groupName, 100)<span class="owner css-truncate" title="@@@groupName">@@@groupName</span></div>
} else {
<div title="You don't have permission to fork here." class="owner-select-target js-fork-owner-select-target disabled">@avatar(groupName, 100)<span class="owner css-truncate" title="@@@groupName">@@@groupName</span></div>
}
}
</div>
<input id="account" name="account" type="hidden" />
</form>

View File

@@ -1,17 +1,17 @@
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false, uid: Long = new java.util.Date().getTime())(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="tabbable">
<ul class="nav nav-tabs" style="height: 37px;">
<li class="active"><a href="#tab1" data-toggle="tab">Write</a></li>
<li><a href="#tab2" data-toggle="tab" id="preview">Preview</a></li>
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
<li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tab1">
<div class="tab-pane active" id="tab@uid">
<span id="error-content" class="error"></span>
@textarea = {
<textarea id="content" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
<textarea id="content@uid" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
}
@if(enableWikiLink){
@textarea
@@ -19,8 +19,8 @@
@helper.html.attached(repository.owner, repository.name)(textarea)
}
</div>
<div class="tab-pane" id="tab2">
<div class="markdown-body" id="preview-area">
<div class="tab-pane" id="tab@(uid+1)">
<div class="markdown-body" id="preview-area@uid">
</div>
</div>
</div>
@@ -30,18 +30,18 @@
<script>
$(function(){
@if(elastic){
$('#content').elastic();
$('#content@uid').elastic();
}
$('#preview').click(function(){
$(this).closest('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$('#preview@uid').click(function(){
$('#preview-area@uid').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$.post('@url(repository)/_preview', {
content : $('#content').val(),
content : $('#content@uid').val(),
enableWikiLink : @enableWikiLink,
enableRefsLink : @enableRefsLink,
enableTaskList : @enableTaskList
}, function(data){
$('#preview-area').html(data);
$('#preview-area@uid').html(data);
prettyPrint();
});
});

View File

@@ -6,7 +6,7 @@
<head>
<meta charset="utf-8">
<title>@title</title>
<link rel="icon" href="@assets/common/images/favicon.png" type="image/vnd.microsoft.icon" />
<link rel="icon" href="@assets/common/images/gitbucket.png" type="image/vnd.microsoft.icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Le styles -->
<link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
@@ -18,6 +18,7 @@
<link href="@assets/vendors/datepicker/css/datepicker.css" rel="stylesheet">
<link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
<link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/vendors/facebox/facebox.css" rel="stylesheet"/>
<link href="@assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@assets/vendors/jquery/jquery-1.9.1.js"></script>
<script src="@assets/vendors/dropzone/dropzone.js"></script>
@@ -29,6 +30,7 @@
<script src="@assets/vendors/google-code-prettify/prettify.js"></script>
<script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
<script src="@assets/vendors/elastic/jquery.elastic.source.js"></script>
<script src="@assets/vendors/facebox/facebox.js"></script>
</head>
<body>
<form id="search" action="@path/search" method="POST">
@@ -41,7 +43,7 @@
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">
<img src="@assets/common/images/gitbucket.png"/>GitBucket
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
@@ -60,21 +62,11 @@
<li><a href="@path/groups/new">New group</a></li>
</ul>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@plugin.PluginSystem.globalMenus.map { menu =>
@if(menu.condition(context)){
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
}
}
@if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
@plugin.PluginSystem.globalMenus.map { menu =>
@if(menu.condition(context)){
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
}
}
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
}
</div><!--/.nav-collapse -->
@@ -88,9 +80,6 @@
$('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != '';
});
@plugin.PluginSystem.javaScripts.filter(_.filter(context.currentPath)).map { js =>
@Html(js.script)
}
});
</script>
</body>

View File

@@ -2,6 +2,7 @@
repository: service.RepositoryService.RepositoryInfo,
id: Option[String] = None,
expand: Boolean = false,
isNoGroup: Boolean = true,
info: Option[Any] = None,
error: Option[Any] = None)(body: Html)(implicit context: app.Context)
@import context._
@@ -38,7 +39,15 @@
@if(repository.commitCount > 0){
<div class="pull-right">
<div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
@if(loginAccount.isEmpty){
<a title="You must be signed in to fork a repository" href="@path/signin" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
} else {
@if(isNoGroup) {
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" style="margin-bottom: 10px;" data-account="@loginAccount.get.userName">Fork</a>
} else {
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" rel="facebox" style="margin-bottom: 10px;">Fork</a>
}
}
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div>
@@ -65,11 +74,6 @@
@sidemenu("/issues", "issues", "Issues", repository.issueCount)
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
@sidemenu("/wiki" , "wiki" , "Wiki")
@plugin.PluginSystem.repositoryMenus.map { menu =>
@if(menu.condition(context)){
@sidemenuPlugin(menu.url, menu.label, menu.label, menu.icon)
}
}
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@sidemenu("/settings", "settings", "Settings")
}
@@ -175,6 +179,33 @@ $(function(){
$(target).children('img.menu-icon' ).css('display', 'inline');
});
$('a[rel*=facebox]').facebox();
$(document).on("click", ".js-fork-owner-select-target", function() {
if (!$(this).hasClass("disabled")) {
var account = $(this).text().replace("@@", "");
$("#account").val(account);
$("#fork").submit();
}
});
@if(loginAccount.isDefined){
$(document).on("click", "a[data-account]", function(e) {
e.preventDefault();
var form = $('<form/>', {
action: $(this).attr('href'),
method: "post"
});
var account = $('<input/>', {
type: "hidden",
name: "account",
value: $(this).data('account')
});
form.append(account);
form.submit();
});
}
@if(settings.ssh && loginAccount.isDefined){
$('#repository-url-http').click(function(){
$('#repository-url-proto').text('HTTP');

View File

@@ -83,7 +83,7 @@
</table>
} else {
@pulls.html.commits(commits, Some(comments), repository)
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, false, hasWritePermission, false)
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, hasWritePermission, false)
<p>Showing you all comments on commits in this comparison.</p>
@issues.html.commentlist(None, comments, hasWritePermission, repository, None)
}

View File

@@ -62,7 +62,7 @@
<p>
<span class="strong">Step 3:</span> Merge the changes and update the server
</p>
@defining(s"git checkout master\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
@defining(s"git checkout ${pullreq.branch}\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-3", command){
<pre style="width: 500px; float: left;">@command</pre>
}

View File

@@ -77,7 +77,7 @@
@pulls.html.commits(dayByDayCommits, Some(comments), repository)
</div>
<div class="tab-pane" id="files">
@helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, true, hasWritePermission, true)
@helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), hasWritePermission, true)
</div>
</div>
}

View File

@@ -2,7 +2,7 @@
fileName: Option[String] = None,
oldLineNumber: Option[Int] = None,
newLineNumber: Option[Int] = None,
pullRequest: Boolean,
issueId: Option[Int] = None,
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@@ -29,10 +29,10 @@
<input type="submit" class="btn btn-success" formaction="@url(repository)/commit/@commitId/comment/new" value="Comment on this commit"/>
</div>
}
<input type="hidden" name="pullRequest" value="@pullRequest">
@if(fileName.isDefined){<input type="hidden" name="fileName" value="@fileName.get">}
@if(oldLineNumber.isDefined){<input type="hidden" name="oldLineNumber" value="@oldLineNumber.get">}
@if(newLineNumber.isDefined){<input type="hidden" name="newLineNumber" value="@newLineNumber.get">}
@issueId.map { issueId => <input type="hidden" name="issueId" value="@issueId"> }
@fileName.map { fileName => <input type="hidden" name="fileName" value="@fileName"> }
@oldLineNumber.map { oldLineNumber => <input type="hidden" name="oldLineNumber" value="@oldLineNumber"> }
@newLineNumber.map { newLineNumber => <input type="hidden" name="newLineNumber" value="@newLineNumber"> }
</form>
<script>
$('.btn-inline-comment').click(function(e) {
@@ -47,7 +47,17 @@
type: 'POST',
data: param
}).done(function(data) {
$form.closest('tr').removeClass('inline-comment-form').find('td').html('<td colspan="3"></td>').html(data);
var tmp;
if (window.viewType == 0) {
tmp = '@(oldLineNumber, newLineNumber) match {
case (Some(_), None) => {<td colspan="2" class="comment-box-container"></td><td colspan="2"></td>}
case (None, Some(_)) => {<td colspan="2"></td><td colspan="2" class="comment-box-container"></td>}
case _ => {<td colspan="3" class="comment-box-container"></td>}
}'
} else {
tmp = '<td colspan="3" class="comment-box-container"></td>'
}
$form.closest('tr').removeClass('inline-comment-form').html(tmp).find('.comment-box-container').html(data);
$('#comment-list').append(data);
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
$('#comment-list').children('.inline-comment').hide();

View File

@@ -83,14 +83,14 @@
</td>
</tr>
</table>
@helper.html.diff(diffs, repository, Some(commit.id), oldCommitId, true, false, hasWritePermission, true)
@helper.html.diff(diffs, repository, Some(commit.id), oldCommitId, true, None, hasWritePermission, true)
<label class="checkbox">
<input type="checkbox" id="show-notes"> Show line notes below
</label>
<div id="comment-list">
@issues.html.commentlist(None, comments, hasWritePermission, repository, None)
</div>
@commentform(commitId = commitId, pullRequest = false, hasWritePermission = hasWritePermission, repository = repository)
@commentform(commitId = commitId, hasWritePermission = hasWritePermission, repository = repository)
}
}
<script>

View File

@@ -1,6 +1,7 @@
@(branch: String,
repository: service.RepositoryService.RepositoryInfo,
pathList: List[String],
groupNames: List[String],
latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo],
readme: Option[(List[String], String)],
@@ -10,7 +11,7 @@
@import context._
@import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository, Some(branch), pathList.isEmpty, info, error){
@html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){
<div class="head">
@helper.html.branchcontrol(
branch,

View File

@@ -16,20 +16,19 @@
}
</div>
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
<pre>
touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin <span class="live-clone-url">@repository.httpUrl</span>
git push -u origin master
</pre>
@pre {
touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin <span class="live-clone-url">@repository.httpUrl</span>
git push -u origin master
}
<h3 style="margin-top: 30px;">Push an existing repository from the command line</h3>
<pre>
git remote add origin <span class="live-clone-url">@repository.httpUrl</span>
git push -u origin master
</pre>
@pre {
git remote add origin <span class="live-clone-url">@repository.httpUrl</span>
git push -u origin master
}
<script>
$(function(){
$('.git-protocol-selector').click(function(e){

View File

@@ -16,7 +16,7 @@
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
<div>
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
<div class="small muted">Last commited @helper.html.datetimeago(file.lastModified)</div>
<div class="small muted">Last committed @helper.html.datetimeago(file.lastModified)</div>
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
</div>
}

View File

@@ -2,6 +2,12 @@
@import context._
@main("Sign in"){
<div class="signin-form">
@settings.information.map { information =>
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
<button type="button" class="close" data-dismiss="alert">&times;</button>
@Html(information)
</div>
}
@signinform(settings)
</div>
}

View File

@@ -15,7 +15,7 @@
<li>
<h1 class="wiki-title"><span class="muted">Compare Revisions</span></h1>
</li>
<li class="pull-right">
<li class="fill-width pull-right">
<div class="btn-group">
@if(pageName.isDefined){
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
@@ -26,7 +26,9 @@
</div>
</li>
</ul>
@helper.html.diff(diffs, repository, None, None, false, false, false, false)
<div class="pull-left">
@helper.html.diff(diffs, repository, None, None, false, None, false, false)
</div>
@if(hasWritePermission){
<div>
@if(pageName.isDefined){

View File

@@ -53,7 +53,7 @@
</div>
<div style="width: 650px;" class="pull-left">
<div class="markdown-body">
@markdown(page.content, repository, true, false)
@markdown(page.content, repository, true, false, false, false, pages)
</div>
</div>
}

View File

@@ -975,15 +975,16 @@ table.diff .add-comment {
position: absolute;
background: blue;
top: 0;
left: -7px;
color: white;
padding: 2px;
padding: 2px 4px;
border: solid 1px blue;
border-radius: 3px;
z-index: 99;
}
table.diff .add-comment:hover {
padding: 4px;
padding: 4px 6px;
top: -1px;
}
@@ -1003,6 +1004,20 @@ table.diff tbody tr.not-diff:hover td{
background-color: #fff;
}
.not-diff > .comment-box-container {
white-space: initial;
line-height: initial;
padding: 10px;
}
.diff .oldline:before, .diff .newline:before {
content: attr(line-number);
}
.diff .skipline:before {
content: "..."
}
/****************************************************************************/
/* Repository Settings */
/****************************************************************************/
@@ -1082,6 +1097,8 @@ div.markdown-body pre {
div.markdown-body code {
font-size: 12px;
padding: 0 5px;
background-color: rgba(0,0,0,0.04);
rgb(51, 51, 51);
}
div.markdown-body table {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

View File

@@ -0,0 +1,142 @@
#facebox {
position: absolute;
top: 0;
left: 0;
z-index: 100;
text-align: left;
}
#facebox .popup{
position:relative;
border:3px solid rgba(0,0,0,0);
-webkit-border-radius:5px;
-moz-border-radius:5px;
border-radius:5px;
-webkit-box-shadow:0 0 18px rgba(0,0,0,0.4);
-moz-box-shadow:0 0 18px rgba(0,0,0,0.4);
box-shadow:0 0 18px rgba(0,0,0,0.4);
}
#facebox .content {
overflow: hidden;
width: 455px;
padding: 10px;
background: #fff;
-webkit-border-radius:4px;
-moz-border-radius:4px;
border-radius:4px;
}
#facebox .content > p:first-child{
margin-top:0;
}
#facebox .content > p:last-child{
margin-bottom:0;
}
#facebox .close{
position:absolute;
top:5px;
right:5px;
padding:2px;
background:#fff;
}
#facebox .close img{
opacity:0.3;
}
#facebox .close:hover img{
opacity:1.0;
}
#facebox .loading {
text-align: center;
}
#facebox .image {
text-align: center;
}
#facebox img {
border: 0;
margin: 0;
}
#facebox_overlay {
position: fixed;
top: 0px;
left: 0px;
height:100%;
width:100%;
}
.facebox_hide {
z-index:-100;
}
.facebox_overlayBG {
background-color: #000;
z-index: 99;
}
.facebox-header {
margin: -15px -15px 15px;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
font-size: 18px;
font-weight: normal;
}
.owner-select-grid {
margin-left: -8px;
overflow: hidden;
}
.owner-select-target {
float: left;
padding: 10px;
margin: 0 8px 10px;
text-align: center;
background-color: #f2f2f2;
border-radius: 3px;
}
.owner-select-target.enabled {
font-weight: bold;
cursor: pointer;
}
.owner-select-target.disabled {
color: #999;
cursor: not-allowed;
}
.owner-select-grid .avatar {
display: block;
overflow: hidden;
line-height: 1;
vertical-align: middle;
border-radius: 3px;
}
.owner-select-target.enabled .avatar {
opacity: 1;
}
.owner-select-target.disabled .avatar {
margin-bottom: 9px;
opacity: 0.3;
}
.css-truncate {
display: inline-block;
max-width: 125px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: top;
}
.owner-select-target .css-truncate {
max-width: 90px;
}

View File

@@ -0,0 +1,313 @@
/*
* Facebox (for jQuery)
* version: 1.3
* @requires jQuery v1.2 or later
* @homepage https://github.com/defunkt/facebox
*
* Licensed under the MIT:
* http://www.opensource.org/licenses/mit-license.php
*
* Copyright Forever Chris Wanstrath, Kyle Neath
*
* Usage:
*
* jQuery(document).ready(function() {
* jQuery('a[rel*=facebox]').facebox()
* })
*
* <a href="#terms" rel="facebox">Terms</a>
* Loads the #terms div in the box
*
* <a href="terms.html" rel="facebox">Terms</a>
* Loads the terms.html page in the box
*
* <a href="terms.png" rel="facebox">Terms</a>
* Loads the terms.png image in the box
*
*
* You can also use it programmatically:
*
* jQuery.facebox('some html')
* jQuery.facebox('some html', 'my-groovy-style')
*
* The above will open a facebox with "some html" as the content.
*
* jQuery.facebox(function($) {
* $.get('blah.html', function(data) { $.facebox(data) })
* })
*
* The above will show a loading screen before the passed function is called,
* allowing for a better ajaxy experience.
*
* The facebox function can also display an ajax page, an image, or the contents of a div:
*
* jQuery.facebox({ ajax: 'remote.html' })
* jQuery.facebox({ ajax: 'remote.html' }, 'my-groovy-style')
* jQuery.facebox({ image: 'stairs.jpg' })
* jQuery.facebox({ image: 'stairs.jpg' }, 'my-groovy-style')
* jQuery.facebox({ div: '#box' })
* jQuery.facebox({ div: '#box' }, 'my-groovy-style')
*
* Want to close the facebox? Trigger the 'close.facebox' document event:
*
* jQuery(document).trigger('close.facebox')
*
* Facebox also has a bunch of other hooks:
*
* loading.facebox
* beforeReveal.facebox
* reveal.facebox (aliased as 'afterReveal.facebox')
* init.facebox
* afterClose.facebox
*
* Simply bind a function to any of these hooks:
*
* $(document).bind('reveal.facebox', function() { ...stuff to do after the facebox and contents are revealed... })
*
*/
(function($) {
$.facebox = function(data, klass) {
$.facebox.loading(data.settings || [])
if (data.ajax) fillFaceboxFromAjax(data.ajax, klass)
else if (data.image) fillFaceboxFromImage(data.image, klass)
else if (data.div) fillFaceboxFromHref(data.div, klass)
else if ($.isFunction(data)) data.call($)
else $.facebox.reveal(data, klass)
}
/*
* Public, $.facebox methods
*/
$.extend($.facebox, {
settings: {
opacity : 0.2,
overlay : true,
loadingImage : '/assets/vendors/facebox/loading.gif',
closeImage : '/assets/vendors/facebox/closelabel.png',
imageTypes : [ 'png', 'jpg', 'jpeg', 'gif' ],
faceboxHtml : '\
<div id="facebox" style="display:none;"> \
<div class="popup"> \
<div class="content"> \
</div> \
<a href="#" class="close"></a> \
</div> \
</div>'
},
loading: function() {
init()
if ($('#facebox .loading').length == 1) return true
showOverlay()
$('#facebox .content').empty().
append('<div class="loading"><img src="'+$.facebox.settings.loadingImage+'"/></div>')
$('#facebox').show().css({
top: getPageScroll()[1] + (getPageHeight() / 10),
left: $(window).width() / 2 - ($('#facebox .popup').outerWidth() / 2)
})
$(document).bind('keydown.facebox', function(e) {
if (e.keyCode == 27) $.facebox.close()
return true
})
$(document).trigger('loading.facebox')
},
reveal: function(data, klass) {
$(document).trigger('beforeReveal.facebox')
if (klass) $('#facebox .content').addClass(klass)
$('#facebox .content').empty().append(data)
$('#facebox .popup').children().fadeIn('normal')
$('#facebox').css('left', $(window).width() / 2 - ($('#facebox .popup').outerWidth() / 2))
$(document).trigger('reveal.facebox').trigger('afterReveal.facebox')
},
close: function() {
$(document).trigger('close.facebox')
return false
}
})
/*
* Public, $.fn methods
*/
$.fn.facebox = function(settings) {
if ($(this).length == 0) return
init(settings)
function clickHandler() {
$.facebox.loading(true)
// support for rel="facebox.inline_popup" syntax, to add a class
// also supports deprecated "facebox[.inline_popup]" syntax
var klass = this.rel.match(/facebox\[?\.(\w+)\]?/)
if (klass) klass = klass[1]
fillFaceboxFromHref(this.href, klass)
return false
}
return this.bind('click.facebox', clickHandler)
}
/*
* Private methods
*/
// called one time to setup facebox on this page
function init(settings) {
if ($.facebox.settings.inited) return true
else $.facebox.settings.inited = true
$(document).trigger('init.facebox')
makeCompatible()
var imageTypes = $.facebox.settings.imageTypes.join('|')
$.facebox.settings.imageTypesRegexp = new RegExp('\\.(' + imageTypes + ')(\\?.*)?$', 'i')
if (settings) $.extend($.facebox.settings, settings)
$('body').append($.facebox.settings.faceboxHtml)
var preload = [ new Image(), new Image() ]
preload[0].src = $.facebox.settings.closeImage
preload[1].src = $.facebox.settings.loadingImage
$('#facebox').find('.b:first, .bl').each(function() {
preload.push(new Image())
preload.slice(-1).src = $(this).css('background-image').replace(/url\((.+)\)/, '$1')
})
$('#facebox .close')
.click($.facebox.close)
.append('<img src="'
+ $.facebox.settings.closeImage
+ '" class="close_image" title="close">')
}
// getPageScroll() by quirksmode.com
function getPageScroll() {
var xScroll, yScroll;
if (self.pageYOffset) {
yScroll = self.pageYOffset;
xScroll = self.pageXOffset;
} else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict
yScroll = document.documentElement.scrollTop;
xScroll = document.documentElement.scrollLeft;
} else if (document.body) {// all other Explorers
yScroll = document.body.scrollTop;
xScroll = document.body.scrollLeft;
}
return new Array(xScroll,yScroll)
}
// Adapted from getPageSize() by quirksmode.com
function getPageHeight() {
var windowHeight
if (self.innerHeight) { // all except Explorer
windowHeight = self.innerHeight;
} else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
windowHeight = document.documentElement.clientHeight;
} else if (document.body) { // other Explorers
windowHeight = document.body.clientHeight;
}
return windowHeight
}
// Backwards compatibility
function makeCompatible() {
var $s = $.facebox.settings
$s.loadingImage = $s.loading_image || $s.loadingImage
$s.closeImage = $s.close_image || $s.closeImage
$s.imageTypes = $s.image_types || $s.imageTypes
$s.faceboxHtml = $s.facebox_html || $s.faceboxHtml
}
// Figures out what you want to display and displays it
// formats are:
// div: #id
// image: blah.extension
// ajax: anything else
function fillFaceboxFromHref(href, klass) {
// div
if (href.match(/#/)) {
var url = window.location.href.split('#')[0]
var target = href.replace(url,'')
if (target == '#') return
$.facebox.reveal($(target).html(), klass)
// image
} else if (href.match($.facebox.settings.imageTypesRegexp)) {
fillFaceboxFromImage(href, klass)
// ajax
} else {
fillFaceboxFromAjax(href, klass)
}
}
function fillFaceboxFromImage(href, klass) {
var image = new Image()
image.onload = function() {
$.facebox.reveal('<div class="image"><img src="' + image.src + '" /></div>', klass)
}
image.src = href
}
function fillFaceboxFromAjax(href, klass) {
$.facebox.jqxhr = $.get(href, function(data) { $.facebox.reveal(data, klass) })
}
function skipOverlay() {
return $.facebox.settings.overlay == false || $.facebox.settings.opacity === null
}
function showOverlay() {
if (skipOverlay()) return
if ($('#facebox_overlay').length == 0)
$("body").append('<div id="facebox_overlay" class="facebox_hide"></div>')
$('#facebox_overlay').hide().addClass("facebox_overlayBG")
.css('opacity', $.facebox.settings.opacity)
.click(function() { $(document).trigger('close.facebox') })
.fadeIn(200)
return false
}
function hideOverlay() {
if (skipOverlay()) return
$('#facebox_overlay').fadeOut(200, function(){
$("#facebox_overlay").removeClass("facebox_overlayBG")
$("#facebox_overlay").addClass("facebox_hide")
$("#facebox_overlay").remove()
})
return false
}
/*
* Bindings
*/
$(document).bind('close.facebox', function() {
if ($.facebox.jqxhr) {
$.facebox.jqxhr.abort()
$.facebox.jqxhr = null
}
$(document).unbind('keydown.facebox')
$('#facebox').fadeOut(function() {
$('#facebox .content').removeClass().addClass('content')
$('#facebox .loading').remove()
$(document).trigger('afterClose.facebox')
})
hideOverlay()
})
})(jQuery);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -84,7 +84,7 @@ diffview = {
b.appendChild(document.createTextNode("+"));
b.style.display = "none";
b.className = "add-comment";
e.appendChild(b);
e.insertBefore(b, e.firstChild);
e.style.position = "relative";
return e;
}
@@ -116,10 +116,13 @@ diffview = {
* be returned. Otherwise, tidx is returned, and two empty cells are added
* to the given row.
*/
function addCells (row, tidx, tend, textLines, change) {
function addCells (row, tidx, tend, textLines, change, thclass) {
var tmp;
if (tidx < tend) {
row.appendChild(telt("th", (tidx + 1).toString()));
row.appendChild(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
tmp = ctelt("th", thclass, "");
tmp.setAttribute("line-number", (tidx + 1).toString());
row.appendChild(tmp);
row.appendChild(addButton(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))));
return tidx + 1;
} else {
row.appendChild(document.createElement("th"));
@@ -129,9 +132,13 @@ diffview = {
}
function addCellsInline (row, tidx, tidx2, textLines, change) {
row.appendChild(ctelt("th", "oldline", tidx == null ? "" : (tidx + 1).toString()));
row.appendChild(addButton(ctelt("th", "newline", tidx2 == null ? "" : (tidx2 + 1).toString())));
row.appendChild(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
var tmp = ctelt("th", "oldline", "");
tmp.setAttribute("line-number", tidx == null ? "" : (tidx + 1).toString());
row.appendChild(tmp);
tmp = ctelt("th", "newline", "");
tmp.setAttribute("line-number", tidx2 == null ? "" : (tidx2 + 1).toString());
row.appendChild(tmp);
row.appendChild(addButton(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))));
}
for (var idx = 0; idx < opcodes.length; idx++) {
@@ -154,9 +161,9 @@ diffview = {
b += jump;
n += jump;
i += jump - 1;
node.appendChild(telt("th", "..."));
node.appendChild(ctelt("th", "skipline", ""));
if (!inline) node.appendChild(ctelt("td", "skip", ""));
node.appendChild(telt("th", "..."));
node.appendChild(ctelt("th", "skipline", ""));
node.appendChild(ctelt("td", "skip", ""));
// skip last lines if they're all equal
@@ -188,8 +195,8 @@ diffview = {
if (b < be) changeBase = "delete";
if (n < ne) changeNew = "insert";
}
b = addCells(node, b, be, baseTextLines, changeBase);
n = addCells(node, n, ne, newTextLines, changeNew);
b = addCells(node, b, be, baseTextLines, changeBase, "oldline");
n = addCells(node, n, ne, newTextLines, changeNew, "newline");
}
}

View File

@@ -1,22 +0,0 @@
package plugin
import org.specs2.mutable._
class PluginSystemSpec extends Specification {
"isUpdatable" should {
"return true for updattable plugin" in {
PluginSystem.isUpdatable("1.0.0", "1.0.1") must beTrue
PluginSystem.isUpdatable("1.0.0", "1.1.0") must beTrue
PluginSystem.isUpdatable("1.1.1", "1.2.0") must beTrue
PluginSystem.isUpdatable("1.2.1", "2.0.0") must beTrue
}
"return false for not updattable plugin" in {
PluginSystem.isUpdatable("1.0.0", "1.0.0") must beFalse
PluginSystem.isUpdatable("1.0.1", "1.0.0") must beFalse
PluginSystem.isUpdatable("1.1.1", "1.1.0") must beFalse
PluginSystem.isUpdatable("2.0.0", "1.2.1") must beFalse
}
}
}

View File

@@ -95,6 +95,8 @@ class AvatarImageProviderSpec extends Specification with Mockito {
baseUrl = None,
information = None,
allowAccountRegistration = false,
allowAnonymousAccess = true,
isCreateRepoOptionPublic = true,
gravatar = useGravatar,
notification = false,
ssh = false,