Compare commits

...

98 Commits
1.3 ... 1.4

Author SHA1 Message Date
takezoe
fd84b3f1c4 Fix link path in dashborad. 2013-07-31 12:04:09 +09:00
Naoki Takezoe
9d4a052ecc Update for 1.4 release. 2013-07-31 00:49:27 +09:00
takezoe
f93c8965be Bug fix. 2013-07-30 22:03:48 +09:00
takezoe
beef86ce8c Extend session timeout to 24 hours. 2013-07-30 21:41:47 +09:00
shimamoto
03b75d5379 (refs #26) Fix splitWith condition. 2013-07-30 21:08:38 +09:00
shimamoto
66855e65bb (refs #26) Implements repository filter. 2013-07-30 19:36:20 +09:00
takezoe
b8da93912f Fix query in RepositoryService#getVisibleRepositories fluently :-) 2013-07-30 12:42:42 +09:00
takezoe
d675115615 Remove unnecessary <hr>. 2013-07-30 11:59:47 +09:00
shimamoto
0296a0bde6 Merge branch 'master' of https://github.com/takezoe/gitbucket.git
Conflicts:
	src/main/scala/app/DashboardController.scala
2013-07-30 10:51:44 +09:00
shimamoto
8409384232 (refs #26) Implements the dashboard issue display. 2013-07-30 10:47:46 +09:00
Naoki Takezoe
cfaee56a08 Merge pull request #55 from tanacasino/fix/improve-repository-viewer
Make more fast and github-like repository viewer
2013-07-29 15:20:00 -07:00
takezoe
7d65717784 The method of RepositoryService was cleaned up. 2013-07-30 03:41:47 +09:00
Tomofumi Tanaka
7079d50fdf Make more fast and github-like repository viewer 2013-07-30 03:26:36 +09:00
takezoe
41a613e151 Move private method. 2013-07-30 02:45:14 +09:00
takezoe
1f2b6a0acc Adjust whitespaces. 2013-07-29 22:58:30 +09:00
takezoe
25d402c9d1 (refs #53)Fix path extraction for branch which contains '/'. 2013-07-29 22:57:57 +09:00
takezoe
045b7cf019 Fix avatar problem. 2013-07-29 17:10:45 +09:00
takezoe
57109dd72e Add init-param 'webAllowOthers' to web.xml. 2013-07-29 13:24:01 +09:00
takezoe
1b878b59b8 Use ISSUE_OUTLINE_VIEW to retrieve comment count. 2013-07-26 02:59:51 +09:00
Naoki Takezoe
80452ab4cd Merge pull request #52 from tanacasino/fix/set-initial-commiter
Initial commiter should be repository creator
2013-07-25 10:36:50 -07:00
Tomofumi Tanaka
4d9c8e8d3e Initial commiter should be repository creator
like wiki repository.
Now it seems that set $HOME/.gitconfig user.name,user.email or
machine username and username@hostname.
2013-07-26 00:29:00 +09:00
shimamoto
a5f12a50e6 Add view ISSUE_OUTLINE_VIEW. 2013-07-25 20:28:19 +09:00
takezoe
07ef06ad95 Improve authentication for H2 console. 2013-07-25 03:16:34 +09:00
takezoe
34e2663492 Use JGitUtil.isEmpty() to check whether repository is empty. 2013-07-24 23:10:55 +09:00
Naoki Takezoe
8b90f87589 Merge pull request #51 from tanacasino/fix/search-in-empty-repository
(refs #50)Fix search logic in empty repository
2013-07-24 06:14:42 -07:00
takezoe
8c1e45da6c Set initial value of 'owner' parameter in the repository creation page. 2013-07-24 22:09:10 +09:00
Tomofumi Tanaka
62a6d74393 (refs #50)Fix search logic in empty repository 2013-07-24 16:46:46 +09:00
shimamoto
cb94447290 (refs #26) Add Dashboard controller. Uses a common design at issue. 2013-07-24 14:10:17 +09:00
shimamoto
e4cf509d0f Add tab at dashboard. 2013-07-24 14:01:10 +09:00
takezoe
3a7391fbb3 (refs #8)Some fix for group management. 2013-07-24 03:36:42 +09:00
takezoe
2155734e23 (refs #8)Add Members tab to account information page for group account. 2013-07-24 02:12:35 +09:00
takezoe
6806e66d64 (refs #8)Change create/edit user template name and path. 2013-07-24 02:04:08 +09:00
takezoe
db8305b5e9 (refs #8)Change create/edit user template name and path. 2013-07-24 02:03:42 +09:00
takezoe
e8330eedc3 (refs #8)Group repository creation is completed. 2013-07-24 02:00:52 +09:00
takezoe
c01c4a860c (refs #8)Set initial value for editing group. 2013-07-24 01:54:33 +09:00
takezoe
6e778f209d (refs #8)Fix error message position. 2013-07-24 01:42:40 +09:00
takezoe
b760361184 (refs #8)Implementing repository creation for group. 2013-07-23 22:05:30 +09:00
takezoe
7150befa54 (refs #8)Group register/edit form is completed. 2013-07-23 18:52:36 +09:00
takezoe
5bf0b275cb (refs #8)Remove unused code. 2013-07-23 15:39:47 +09:00
takezoe
c86bf1d68b (refs #8)Merge user name proposal API to IndexController. 2013-07-23 15:37:59 +09:00
takezoe
e61bde1415 (refs #8)Implementing group register/edit form. 2013-07-23 13:02:30 +09:00
takezoe
e4b3f0ddef (refs #8)Implementing group register/edit form. 2013-07-23 11:59:49 +09:00
takezoe
ec73294900 (refs #8)Add model for GROUP_MEMBER. 2013-07-23 11:08:36 +09:00
takezoe
30eb949ce1 (refs #8)Start to implement group management. 2013-07-22 22:22:49 +09:00
takezoe
f5d69a3df6 Merge branch 'master' into group-management 2013-07-22 21:22:36 +09:00
takezoe
3cc39489bd (refs #40)Enable H2 Console. 2013-07-22 21:12:22 +09:00
takezoe
ace5d7de9e (refs #3)Separate search actions to SearchController. 2013-07-22 17:28:13 +09:00
takezoe
1682eb3915 (refs #8)Add DDL to add new table and columns for group management. 2013-07-22 17:15:12 +09:00
takezoe
6fd1a990ae (refs #44)Add milestone progress bar to the issue detail page. 2013-07-22 17:01:00 +09:00
Naoki Takezoe
cfa36a21b5 Merge pull request #47 from tomykaira/set_icon_on_assignee_change
Set icon when assignee is changed in page (via JS)
2013-07-21 20:29:09 -07:00
takezoe
95163d4864 Add link to the account info page for the assigned user icon at the issue list. 2013-07-22 12:27:18 +09:00
takezoe
5a9645829d (refs #33)Small fix for pull request #45. 2013-07-22 12:25:28 +09:00
takezoe
be78d93c1f Fix avatar tooltip. 2013-07-22 12:22:18 +09:00
Naoki Takezoe
ac63558645 Merge pull request #45 from tomykaira/feature/participants
Add participants to issue detail
2013-07-21 20:10:57 -07:00
tomykaira
88fb2e49dc Set icon when assignee is changed in page
The javascript code did not set icon, whereas the view script does.
2013-07-21 20:06:24 +09:00
tomykaira
6e96ad0f17 (refs #37)Add participants to issue detail
The design is inherited from Github.
2013-07-21 18:25:02 +09:00
takezoe
e54754d04f (refs #25)Display due date in milestone dropdown chooser. 2013-07-21 01:39:38 +09:00
takezoe
e4b2ebe2a4 (refs #25)Alert if due date passed. 2013-07-20 19:34:58 +09:00
takezoe
0028431dde Exclude some actions from comment count at the repository search result. 2013-07-20 03:06:33 +09:00
takezoe
91d94de1d2 Merge branch '#3_repository-search'
Conflicts:
	src/main/scala/app/UserManagementController.scala
	src/main/scala/service/IssuesService.scala
	src/main/twirl/issues/issue.scala.html
2013-07-20 03:00:16 +09:00
takezoe
0c131ec990 Move FileUploadUtil to FileUploadControllerBase. 2013-07-19 20:33:40 +09:00
takezoe
54280d5572 Add paginator and separate search code in controller to service. 2013-07-19 20:24:31 +09:00
Naoki Takezoe
6d3640a8b0 Merge pull request #43 from rabitarochan/fix/wiki-resourceleak
Fix resource leak.
2013-07-19 04:23:57 -07:00
rabitarochan
8226073506 Fix resource leak. 2013-07-19 18:18:44 +09:00
takezoe
f4a5e18c69 Merge branch 'repository-search-cache' into #3_repository-search 2013-07-19 18:04:28 +09:00
takezoe
133af93548 Don't use cache library immediately. 2013-07-19 18:03:48 +09:00
takezoe
3546a5d392 Ignore error in activity timeline caused by invalid data. 2013-07-19 14:11:13 +09:00
takezoe
fb921e951e Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-07-19 14:08:05 +09:00
shimamoto
22685d8e3b Add reference link to wiki. 2013-07-19 13:25:23 +09:00
takezoe
b2d050d136 Remove unused import statement. 2013-07-19 12:17:40 +09:00
takezoe
e3ff1dcd96 Check state of repository type radio button had not been applied. 2013-07-19 03:48:58 +09:00
takezoe
897890e1b4 (refs #42)Requires BASIC authentication for /info/refs?service=git-receive-pack. 2013-07-19 03:23:19 +09:00
Naoki Takezoe
2c95ea00e8 GitBucket 1.3 released. 2013-07-18 20:59:59 +09:00
takezoe
00d6ed7dbb Cleanup search field and global header styles. 2013-07-18 20:50:42 +09:00
shimamoto
b23133c79c Move Implicit && to model package object. 2013-07-18 20:21:24 +09:00
takezoe
eef2f26707 Change highlight color. 2013-07-18 20:12:08 +09:00
takezoe
7483ad1732 Pagination for repository search results. 2013-07-18 20:02:12 +09:00
takezoe
134624967b Store search results into singleton cache. 2013-07-18 19:09:21 +09:00
takezoe
a1b8d1cd84 Add Guava to use CacheBuilder. 2013-07-18 19:08:03 +09:00
takezoe
e7b9293f3b Merge branch 'master' into #3_repository-search 2013-07-18 17:07:22 +09:00
takezoe
93e4a8931d (refs #3)Add search form to all repository related pages. 2013-07-18 17:06:28 +09:00
takezoe
dedf5094c1 Small fix and add TODO. 2013-07-18 03:58:39 +09:00
takezoe
e4d97e4059 (refs #3)Hide result count if count is zero. 2013-07-18 01:34:06 +09:00
takezoe
ba567d81cb (refs #3)Add result count to the menu. 2013-07-18 01:12:12 +09:00
takezoe
4fb6005f44 (refs #3)Apply likeEncode to search keyword. 2013-07-18 00:47:55 +09:00
takezoe
69ec4175eb (refs #3)Fix issue search condition. 2013-07-17 21:18:09 +09:00
takezoe
d46e90dcdb (refs #3)Improve presentation for code search results. 2013-07-17 21:13:53 +09:00
takezoe
900e91e101 Bugfix 2013-07-17 19:00:35 +09:00
takezoe
05d7e33d86 (refs #3)Add search form at the top of search result. 2013-07-17 18:32:59 +09:00
takezoe
7f0aff8c03 (refs #3)Cleanup 2013-07-17 16:59:38 +09:00
takezoe
512e59193d (refs #3)Issue search is temporary available. 2013-07-17 16:47:43 +09:00
takezoe
d06a986293 Remove unused import statement. 2013-07-17 13:57:45 +09:00
takezoe
83472bc354 Remove unused import statement. 2013-07-17 13:55:26 +09:00
takezoe
ce8168d97a Fix typo. 2013-07-17 11:54:10 +09:00
takezoe
27670525a3 (refs #3)Search by AND if query words are separated by whitespace. 2013-07-17 11:52:28 +09:00
takezoe
4796d7f450 (refs #3)Search git repository without cloning to the file system. 2013-07-17 06:36:21 +09:00
takezoe
79ec96343f (refs #3)Start work for repository search. 2013-07-17 03:24:47 +09:00
takezoe
cb591925ea (refs #3)Add search field to header area. 2013-07-16 21:58:09 +09:00
83 changed files with 1842 additions and 823 deletions

View File

@@ -36,7 +36,17 @@ To upgrade GitBucket, only replace gitbucket.war.
Release Notes
--------
### 1.3 - xx Jul 2013
### 1.4 - 31 Jul 2013
- Group management.
- Repository search for code and issues.
- Display user related issues on the dashboard.
- Display participants avatar of issues on the issue page.
- Performance improvement for repository viewer.
- Alert by milestone due date.
- H2 database administration console.
- Fixed some bugs.
### 1.3 - 18 Jul 2013
- Batch updating for issues.
- Display assigned user on issue list.
- User icon and Gravatar support.

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
java -Dhttp.proxyHost=proxy.intellilink.co.jp -Dhttp.proxyPort=8080 -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*

View File

@@ -0,0 +1,24 @@
CREATE TABLE GROUP_MEMBER(
GROUP_NAME VARCHAR(100) NOT NULL,
USER_NAME VARCHAR(100) NOT NULL
);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID);

View File

@@ -5,8 +5,10 @@ import javax.servlet._
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
context.mount(new SignInController, "/*")
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*")

View File

@@ -1,11 +1,10 @@
package app
import service._
import util.{FileUtil, FileUploadUtil, OneselfAuthenticator}
import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._
import util.Directory._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
class AccountController extends AccountControllerBase
@@ -43,12 +42,23 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
*/
get("/:userName") {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
getAccountByUserName(userName).map { account =>
params.getOrElse("tab", "repositories") match {
// Public Activity
case "activity" => account.html.activity(x, getActivitiesByUser(userName, true))
case "activity" =>
_root_.account.html.activity(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getActivitiesByUser(userName, true))
// Members
case "members" if(account.isGroupAccount) =>
_root_.account.html.members(account, getGroupMembers(account.userName))
// Repositories
case _ => account.html.repositories(x, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
case _ =>
_root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
}
} getOrElse NotFound
}

View File

@@ -1,7 +1,7 @@
package app
import _root_.util.Directory._
import _root_.util.{FileUploadUtil, FileUtil, Validations}
import _root_.util.{StringUtil, FileUtil, Validations}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
@@ -10,7 +10,9 @@ import org.apache.commons.io.FileUtils
import model.Account
import scala.Some
import service.AccountService
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
/**
* Provides generic features for controller implementations.
@@ -20,6 +22,33 @@ abstract class ControllerBase extends ScalatraFilter
implicit val jsonFormats = DefaultFormats
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse]
val context = request.getServletContext.getContextPath
val path = httpRequest.getRequestURI.substring(context.length)
if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account]
if(account == null){
// Redirect to login form
httpResponse.sendRedirect(context + "/signin?" + path)
} else if(account.isAdmin){
// H2 Console (administrators only)
chain.doFilter(request, response)
} else {
// Redirect to dashboard
httpResponse.sendRedirect(context + "/")
}
} else if(path.startsWith("/git/")){
// Git repository
chain.doFilter(request, response)
} else {
// Scalatra actions
super.doFilter(request, response, chain)
}
}
/**
* Returns the context object for the request.
*/
@@ -116,7 +145,8 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
/**
* Base trait for controllers which manages account information.
*/
trait AccountManagementControllerBase extends ControllerBase { self: AccountService =>
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = {
if(clearImage){
@@ -126,9 +156,9 @@ trait AccountManagementControllerBase extends ControllerBase { self: AccountServ
}
} else {
fileId.map { fileId =>
val filename = "avatar." + FileUtil.getExtension(FileUploadUtil.getUploadedFilename(fileId).get)
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
FileUtils.moveFile(
FileUploadUtil.getTemporaryFile(fileId),
getTemporaryFile(fileId),
new java.io.File(getUserUploadDir(userName), filename)
)
updateAvatarImage(userName, Some(filename))
@@ -148,4 +178,34 @@ trait AccountManagementControllerBase extends ControllerBase { self: AccountServ
.map { _ => "Mail address is already registered." }
}
}
/**
* Base trait for controllers which needs file uploading feature.
*/
trait FileUploadControllerBase {
def generateFileId: String =
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
def TemporaryDir(implicit session: HttpSession): java.io.File =
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
new java.io.File(TemporaryDir, fileId)
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
// getTemporaryFile(fileId).delete()
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
if(filename.isDefined){
session.removeAttribute("upload_" + fileId)
}
filename
}
}

View File

@@ -5,7 +5,6 @@ import util.{JGitUtil, UsersAuthenticator}
import service._
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._
@@ -17,12 +16,13 @@ class CreateRepositoryController extends CreateRepositoryControllerBase
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with WikiService with LabelsService with ActivityService
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator =>
case class RepositoryCreationForm(name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
val form = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
@@ -33,33 +33,41 @@ trait CreateRepositoryControllerBase extends ControllerBase {
* Show the new repository form.
*/
get("/new")(usersOnly {
html.newrepo()
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", form)(usersOnly { form =>
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, loginUserName, form.description, form.isPrivate)
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
}
}
// Insert default labels
createLabel(loginUserName, form.name, "bug", "fc2929")
createLabel(loginUserName, form.name, "duplicate", "cccccc")
createLabel(loginUserName, form.name, "enhancement", "84b6eb")
createLabel(loginUserName, form.name, "invalid", "e6e6e6")
createLabel(loginUserName, form.name, "question", "cc317c")
createLabel(loginUserName, form.name, "wontfix", "ffffff")
createLabel(form.owner, form.name, "bug", "fc2929")
createLabel(form.owner, form.name, "duplicate", "cccccc")
createLabel(form.owner, form.name, "enhancement", "84b6eb")
createLabel(form.owner, form.name, "invalid", "e6e6e6")
createLabel(form.owner, form.name, "question", "cc317c")
createLabel(form.owner, form.name, "wontfix", "ffffff")
// Create the actual repository
val gitdir = getRepositoryDir(loginUserName, form.name)
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
val tmpdir = getInitRepositoryDir(loginUserName, form.name)
val tmpdir = getInitRepositoryDir(form.owner, form.name)
try {
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
@@ -78,7 +86,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
val git = Git.open(tmpdir)
git.add.addFilepattern("README.md").call
git.commit.setMessage("Initial commit").call
git.commit.setCommitter(loginAccount.userName, loginAccount.mailAddress).setMessage("Initial commit").call
git.push.call
} finally {
@@ -87,21 +95,29 @@ trait CreateRepositoryControllerBase extends ControllerBase {
}
// Create Wiki repository
createWikiRepository(loginAccount, form.name)
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(loginUserName, form.name, loginUserName)
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${form.name}")
redirect(s"/${form.owner}/${form.name}")
})
private def existsAccount: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
/**
* Duplicate check for the repository name.
*/
private def unique: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.")
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}
}

View File

@@ -0,0 +1,57 @@
package app
import service._
import util.UsersAuthenticator
class DashboardController extends DashboardControllerBase
with IssuesService with RepositoryService with AccountService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly {
searchIssues("all")
})
get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned")
})
get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by")
})
private def searchIssues(filter: String) = {
import IssuesService._
// condition
val sessionKey = "dashboard/issues"
val condition = if(request.getQueryString == null)
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
else IssueSearchCondition(request)
session.put(sessionKey, condition)
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
//
dashboard.html.issues(
issues.html.listparts(
searchIssue(condition, filterUser, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
page,
countIssue(condition.copy(state = "open"), filterUser, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, repositories: _*),
condition),
countIssue(condition, Map.empty, repositories: _*),
countIssue(condition, Map("assigned" -> userName), repositories: _*),
countIssue(condition, Map("created_by" -> userName), repositories: _*),
countIssueGroupByRepository(condition, filterUser, repositories: _*),
condition,
filter)
}
}

View File

@@ -1,6 +1,6 @@
package app
import util.{FileUtil, FileUploadUtil}
import util.{FileUtil}
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils
@@ -9,17 +9,18 @@ import org.apache.commons.io.FileUtils
* Provides Ajax based file upload functionality.
*
* This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[util.FileUploadUtil#getTemporaryFile()]] with this id.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/
// TODO Remove temporary files at session timeout by session listener.
class FileUploadController extends ScalatraServlet with FileUploadSupport with FlashMapSupport {
class FileUploadController extends ScalatraServlet
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){
fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => {
val fileId = FileUploadUtil.generateFileId
FileUtils.writeByteArrayToFile(FileUploadUtil.getTemporaryFile(fileId), file.get)
val fileId = generateFileId
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
session += "upload_" + fileId -> file.name
Ok(fileId)
}

View File

@@ -1,21 +1,38 @@
package app
import util._
import service._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService
with RepositoryService with SystemSettingsService with ActivityService with AccountService
with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with SystemSettingsService with ActivityService with AccountService
with UsersAuthenticator =>
trait IndexControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService =>
get("/"){
val loginAccount = context.loginAccount
html.index(getRecentActivities(),
getAccessibleRepositories(loginAccount, baseUrl),
getVisibleRepositories(loginAccount, baseUrl),
loadSystemSettings(),
loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil)
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
)
}
}
/**
* JSON API for collaborator completion.
*
* TODO Move to other controller?
*/
get("/_user/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
)
})
}

View File

@@ -67,7 +67,7 @@ trait IssuesControllerBase extends ControllerBase {
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
getMilestones(owner, name),
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
@@ -207,7 +207,12 @@ trait IssuesControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
Ok("updated")
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
} getOrElse NotFound
} getOrElse Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
@@ -256,7 +261,7 @@ trait IssuesControllerBase extends ControllerBase {
}
/**
* @see
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] =
@@ -296,16 +301,10 @@ trait IssuesControllerBase extends ControllerBase {
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner
val repoName = repository.name
val userName = if(filter != "all") Some(params("userName")) else None
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = s"${owner}/${repoName}/issues"
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
// retrieve search condition
val condition = if(request.getQueryString == null){
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
@@ -314,17 +313,17 @@ trait IssuesControllerBase extends ControllerBase {
session.put(sessionKey, condition)
issues.html.list(
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
searchIssue(condition, filterUser, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
countIssue(owner, repoName, condition, "all", None),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "assigned", Some(x.userName))),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "created_by", Some(x.userName))),
countIssueGroupByLabels(owner, repoName, condition, filter, userName),
countIssue(condition.copy(state = "open"), filterUser, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, owner -> repoName),
countIssue(condition, Map.empty, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition,
filter,
repository,

View File

@@ -3,7 +3,7 @@ package app
import jp.sf.amateras.scalatra.forms._
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService

View File

@@ -54,22 +54,19 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
* Display the Collaborators page.
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository)
})
/**
* JSON API for collaborator completion.
*/
get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
settings.html.collaborators(
getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
repository)
})
/**
* Add the collaborator.
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
addCollaborator(repository.owner, repository.name, form.userName)
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
addCollaborator(repository.owner, repository.name, form.userName)
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
@@ -77,7 +74,9 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
* Add the collaborator.
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
removeCollaborator(repository.owner, repository.name, params("name"))
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
removeCollaborator(repository.owner, repository.name, params("name"))
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})

View File

@@ -37,49 +37,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(_)
})
/**
* Displays the file list of the repository root and the specified branch.
*/
get("/:owner/:repository/tree/:id")(referrersOnly {
fileList(_, params("id"))
})
/**
* Displays the file list of the specified path and branch.
*/
get("/:owner/:repository/tree/:id/*")(referrersOnly {
fileList(_, params("id"), multiParams("splat").head)
})
/**
* Displays the commit list of the specified branch.
*/
get("/:owner/:repository/commits/:branch")(referrersOnly { repository =>
val branchName = params("branch")
val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30) match {
case Right((logs, hasNext)) =>
repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
case Left(_) => NotFound
}
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
if(path.isEmpty){
fileList(repository, id)
} else {
fileList(repository, id, path)
}
})
/**
* Displays the commit list of the specified resource.
*/
get("/:owner/:repository/commits/:branch/*")(referrersOnly { repository =>
val branchName = params("branch")
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
val page = params.getOrElse("page", "1").toInt
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
repo.html.commits(path.split("/").toList, branchName, repository,
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
@@ -91,10 +71,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Displays the file content of the specified branch or commit.
*/
get("/:owner/:repository/blob/:id/*")(referrersOnly { repository =>
val id = params("id") // branch name or commit id
val raw = params.get("raw").getOrElse("false").toBoolean
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@@ -202,7 +181,17 @@ trait RepositoryViewerControllerBase extends ControllerBase {
BadRequest
}
})
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} orElse Some(path) get
(id, path.substring(id.length).replaceFirst("^/", ""))
}
/**
* Provides HTML of the file list.
*

View File

@@ -0,0 +1,51 @@
package app
import util._
import service._
import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService
with RepositorySearchService with IssuesService
with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService with RepositorySearchService
with ReferrerAuthenticator =>
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
post("/search", searchForm){ form =>
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
}
get("/:owner/:repository/search")(referrersOnly { repository =>
val query = params("q").trim
val target = params.getOrElse("type", "code")
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
target.toLowerCase match {
case "issue" => search.html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => search.html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
}
})
}

View File

@@ -24,20 +24,19 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
}
post("/signin", form){ form =>
val account = getAccountByUserName(form.userName)
if(account.isEmpty || account.get.password != sha1(form.password)){
redirect("/signin")
} else {
session.setAttribute("LOGIN_ACCOUNT", account.get)
updateLastLoginDate(account.get.userName)
getAccountByUserName(form.userName).collect {
case account if(!account.isGroupAccount && account.password == sha1(form.password)) => {
session.setAttribute("LOGIN_ACCOUNT", account)
updateLastLoginDate(account.userName)
session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String])
}.getOrElse {
redirect("/")
session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String])
}.getOrElse {
redirect("/")
}
}
}
} getOrElse redirect("/signin")
}
get("/signout"){

View File

@@ -1,34 +1,38 @@
package app
import service._
import util.{FileUploadUtil, FileUtil, AdminAuthenticator}
import util.AdminAuthenticator
import util.StringUtil._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import util.Directory._
import scala.Some
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator
trait UserManagementControllerBase extends AccountManagementControllerBase {
self: AccountService with AdminAuthenticator =>
self: AccountService with RepositoryService with AdminAuthenticator =>
case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping(
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String])
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String], clearImage: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
)(UserNewForm.apply)
)(NewUserForm.apply)
val editForm = mapping(
val editUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
@@ -36,28 +40,47 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(UserEditForm.apply)
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text())))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(EditGroupForm.apply)
get("/admin/users")(adminOnly {
admin.users.html.list(getAllUsers())
val users = getAllUsers()
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName)
}.toMap
admin.users.html.list(users, members)
})
get("/admin/users/_new")(adminOnly {
admin.users.html.edit(None)
get("/admin/users/_newuser")(adminOnly {
admin.users.html.user(None)
})
post("/admin/users/_new", newForm)(adminOnly { form =>
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:userName/_edit")(adminOnly {
get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
admin.users.html.edit(getAccountByUserName(userName))
admin.users.html.user(getAccountByUserName(userName))
})
post("/admin/users/:name/_edit", editForm)(adminOnly { form =>
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy(
@@ -71,5 +94,46 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} getOrElse NotFound
})
get("/admin/users/_newgroup")(adminOnly {
admin.users.html.group(None, Nil)
})
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
val groupName = params("groupName")
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
val groupName = params("groupName")
getAccountByUserName(groupName).map { account =>
updateGroup(groupName, form.url)
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil)
updateGroupMembers(form.groupName, memberNames)
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
})
post("/admin/users/_usercheck")(adminOnly {
getAccountByUserName(params("userName")).isDefined
})
}

View File

@@ -12,7 +12,8 @@ object Accounts extends Table[Account]("ACCOUNT") {
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
def image = column[String]("IMAGE")
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? <> (Account, Account.unapply _)
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
}
case class Account(
@@ -24,5 +25,6 @@ case class Account(
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date],
image: Option[String]
image: Option[String],
isGroupAccount: Boolean
)

View File

@@ -0,0 +1,14 @@
package model
import scala.slick.driver.H2Driver.simple._
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
def userName = column[String]("USER_NAME", O PrimaryKey)
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
}
case class GroupMember(
groupName: String,
userName: String
)

View File

@@ -7,6 +7,11 @@ object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTempla
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate {
def commentCount = column[Int]("COMMENT_COUNT")
def * = userName ~ repositoryName ~ issueId ~ commentCount
}
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
def openedUserName = column[String]("OPENED_USER_NAME")
def assignedUserName = column[String]("ASSIGNED_USER_NAME")

View File

@@ -1,5 +1,6 @@
package object model {
import scala.slick.lifted.MappedTypeMapper
import scala.slick.driver.BasicDriver.Implicit._
import scala.slick.lifted.{Column, MappedTypeMapper}
// java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
@@ -7,6 +8,10 @@ package object model {
t => new java.util.Date(t.getTime)
)
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
/**
* Returns system date.
*/

View File

@@ -24,7 +24,8 @@ trait AccountService {
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None,
image = None)
image = None,
isGroupAccount = false)
def updateAccount(account: Account): Unit =
Accounts
@@ -44,5 +45,42 @@ trait AccountService {
def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
def createGroup(groupName: String, url: Option[String]): Unit =
Accounts insert Account(
userName = groupName,
password = "",
mailAddress = groupName + "@devnull",
isAdmin = false,
url = url,
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None,
image = None,
isGroupAccount = true)
def updateGroup(groupName: String, url: Option[String]): Unit =
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName)
}
}
def getGroupMembers(groupName: String): List[String] =
Query(GroupMembers)
.filter(_.groupName is groupName.bind)
.sortBy(_.userName)
.map(_.userName)
.list
def getGroupsByUserName(userName: String): List[String] =
Query(GroupMembers)
.filter(_.userName is userName.bind)
.sortBy(_.groupName)
.map(_.groupName)
.list
}

View File

@@ -6,8 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import model._
import util.StringUtil._
import util.Implicits._
import util.StringUtil._
trait IssuesService {
import IssuesService._
@@ -42,18 +42,16 @@ trait IssuesService {
/**
* Returns the count of the search result against issues.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param repos Tuple of the repository owner and the repository name
* @return the count of the search result
*/
def countIssue(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]): Int = {
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], repos: (String, String)*): Int = {
// TODO It must be _.length instead of map (_.issueId) list).length.
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
(searchIssueQuery(owner, repository, condition, filter, userName) map (_.issueId) list).length
(searchIssueQuery(repos, condition, filterUser) map (_.issueId) list).length
}
/**
* Returns the Map which contains issue count for each labels.
@@ -61,14 +59,13 @@ trait IssuesService {
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @return the Map which contains issue count for each labels (key is label name, value is issue count),
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filter: String, userName: Option[String]): Map[String, Int] = {
filterUser: Map[String, String]): Map[String, Int] = {
searchIssueQuery(owner, repository, condition.copy(labels = Set.empty), filter, userName)
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser)
.innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -83,76 +80,93 @@ trait IssuesService {
}
.toMap
}
/**
* Returns list which contains issue count for each repository.
* If the issue does not exist, its repository is not included in the result.
*
* @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param repos Tuple of the repository owner and the repository name
* @return list which contains issue count for each repository
*/
def countIssueGroupByRepository(condition: IssueSearchCondition, filterUser: Map[String, String],
repos: (String, String)*): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser)
.groupBy { t =>
t.userName ~ t.repositoryName
}
.map { case (repo, t) =>
repo ~ t.length
}
.filter (_._3 > 0.bind)
.list
}
/**
* Returns the search result against issues.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param offset the offset for pagination
* @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count)
*/
def searchIssue(owner: String, repository: String, condition: IssueSearchCondition,
filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = {
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String],
offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
// get issues and comment count
val issues = searchIssueQuery(owner, repository, condition, filter, userName)
.leftJoin(Query(IssueComments)
.filter { t =>
(t.byRepository(owner, repository)) &&
(t.action inSetBind Seq("comment", "close_comment", "reopen_comment"))
// get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.map { case (((t1, t2), t3), t4) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
}
.groupBy { _.issueId }
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
.sortBy { case (t1, t2) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => t2._2
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
.sortBy(_._4) // labelName
.sortBy { case (t1, commentCount, _,_,_) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => commentCount
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
}
}
}
}
.map { case (t1, t2) => (t1, t2._2.ifNull(0)) }
.drop(offset).take(limit)
.list
// get labels
val labels = Query(IssueLabels)
.innerJoin(Labels).on { (t1, t2) =>
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
}
.filter { case (t1, t2) =>
(t1.byRepository(owner, repository)) &&
(t1.issueId inSetBind (issues.map(_._1.issueId)))
}
.sortBy { case (t1, t2) => t1.issueId ~ t2.labelName }
.map { case (t1, t2) => (t1.issueId, t2) }
.list
issues.map { case (issue, commentCount) =>
(issue, labels.collect { case (issueId, labels) if(issueId == issue.issueId) => labels }, commentCount)
}
.drop(offset).take(limit)
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
.map { issues => issues.head match {
case (issue, commentCount, _,_,_) =>
(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
commentCount)
}} toList
}
/**
* Assembles query for conditional issue searching.
*/
private def searchIssueQuery(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]) =
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, filterUser: Map[String, String]) =
Query(Issues) filter { t1 =>
(t1.byRepository(owner, repository)) &&
(condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) } reduceLeft ( _ || _ ) ) &&
(t1.closed is (condition.state == "closed").bind) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.assignedUserName is userName.get.bind, filter == "assigned") &&
(t1.openedUserName is userName.get.bind, filter == "created_by") &&
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -237,6 +251,60 @@ trait IssuesService {
}
.update (closed, currentDate)
/**
* Search issues by keyword.
*
* @param owner the repository owner
* @param repository the repository name
* @param query the keywords separated by whitespace.
* @return issues with comment count and matched content of issue or comment
*/
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
import scala.slick.driver.H2Driver.likeEncode
val keywords = splitWords(query.toLowerCase)
// Search Issue
val issues = Issues
.innerJoin(IssueOutline).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
.filter { case (t1, t2) =>
keywords.map { keyword =>
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
} .reduceLeft(_ && _)
}
.map { case (t1, t2) =>
(t1, 0, t1.content.?, t2.commentCount)
}
// Search IssueComment
val comments = IssueComments
.innerJoin(Issues).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
.innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
}
.filter { case ((t1, t2), t3) =>
keywords.map { query =>
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
}.reduceLeft(_ && _)
}
.map { case ((t1, t2), t3) =>
(t2, t1.commentId, t1.content.?, t3.commentCount)
}
issues.union(comments).sortBy { case (issue, commentId, _, _) =>
issue.issueId ~ commentId
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
issue1.issueId == issue2.issueId
}.map { _.head match {
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
}
}.toList
}
}
object IssuesService {
@@ -247,6 +315,7 @@ object IssuesService {
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None,
repo: Option[String] = None,
state: String = "open",
sort: String = "created",
direction: String = "desc"){
@@ -258,6 +327,7 @@ object IssuesService {
case Some(x) => x.toString
case None => "none"
})},
repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
@@ -278,8 +348,17 @@ object IssuesService {
case "none" => None
case x => Some(x.toInt)
}),
param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
}
}

View File

@@ -0,0 +1,125 @@
package service
import model.Issue
import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._
import model.Issue
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git
trait RepositorySearchService { self: IssuesService =>
import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int =
searchIssuesByKeyword(owner, repository, query).length
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult(
issue.issueId,
issue.title,
issue.openedUserName,
issue.registeredDate,
commentCount,
getHighlightText(content, query)._1)
}
def countFiles(owner: String, repository: String, query: String): Int =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
}
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
if(JGitUtil.isEmpty(git)){
Nil
} else {
val files = searchRepositoryFiles(git, query)
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult(
path,
commits(path).getCommitterIdent.getWhen,
highlightText,
lineNumber)
}
}
}
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve("HEAD")
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.setRecursive(true)
treeWalk.addTree(revCommit.getTree)
val keywords = StringUtil.splitWords(query.toLowerCase)
val list = new ListBuffer[(String, String)]
while (treeWalk.next()) {
if(treeWalk.getFileMode(0) != FileMode.TREE){
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){
val text = new String(bytes, "UTF-8")
val lowerText = text.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
list.append((treeWalk.getPathString, text))
}
}
}
}
}
treeWalk.release
revWalk.release
list.toList
}
}
object RepositorySearchService {
val CodeLimit = 10
val IssueLimit = 10
def getHighlightText(content: String, query: String): (String, Int) = {
val keywords = StringUtil.splitWords(query.toLowerCase)
val lowerText = content.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
val lineNumber = content.substring(0, indices.min).split("\n").size - 1
val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
.replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
"<span style=\"background-color: #ffff88;;\">$1</span>")
(highlightText, lineNumber + 1)
} else {
(content.split("\n").take(5).mkString("\n"), 1)
}
}
case class SearchResult(
files : List[(String, String)],
issues: List[(Issue, Int, String)])
case class IssueSearchResult(
issueId: Int,
title: String,
openedUserName: String,
registeredDate: java.util.Date,
commentCount: Int,
highlightText: String)
case class FileSearchResult(
path: String,
lastModified: java.util.Date,
highlightText: String,
highlightLineNumber: Int)
}

View File

@@ -53,39 +53,6 @@ trait RepositoryService { self: AccountService =>
def getRepositoryNamesOfUser(userName: String): List[String] =
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
/**
* Returns the list of specified user's repositories information.
*
* @param userName the user name
* @param baseUrl the base url of this application
* @param loginUserName the logged in user name
* @return the list of repository information which is sorted in descending order of lastActivityDate.
*/
def getVisibleRepositories(userName: String, baseUrl: String, loginUserName: Option[String]): List[RepositoryInfo] = {
val q1 = Repositories
.filter { t => t.userName is userName.bind }
.map { r => r }
val q2 = Collaborators
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter{ case (t1, t2) => t1.collaboratorName is userName.bind}
.map { case (t1, t2) => t2 }
def visibleFor(t: Repositories.type, loginUserName: Option[String]) = {
loginUserName match {
case Some(x) => (t.isPrivate is false.bind) || (
(t.isPrivate is true.bind) && ((t.userName is x.bind) || (Collaborators.filter { c =>
c.byRepository(t.userName, t.repositoryName) && (c.collaboratorName is x.bind)
}.exists)))
case None => (t.isPrivate is false.bind)
}
}
q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
}
/**
* Returns the specified repository information.
*
@@ -101,29 +68,43 @@ trait RepositoryService { self: AccountService =>
}
/**
* Returns the list of accessible repositories information for the specified account user.
*
* @param account the account
* @param baseUrl the base url of this application
* @return the repository informations which is sorted in descending order of lastActivityDate.
* Returns the list of specified user's repositories.
* It contains own repositories and collaboration repositories.
*/
def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = {
def newRepositoryInfo(repository: Repository): RepositoryInfo = {
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
Query(Repositories).filter { t1 =>
(t1.userName is userName.bind) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
}
(account match {
/**
* Returns the list of visible repositories for the specified user.
* If repositoryUserName is given then filters results by repository owner.
*
* @param loginAccount the logged in account
* @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @return the repository information which is sorted in descending order of lastActivityDate.
*/
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = {
(loginAccount match {
// for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories)
// for Normal Users
case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
(Query(Collaborators).filter(t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)) exists)
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
}
// for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind)
}).sortBy(_.lastActivityDate desc).list.map(newRepositoryInfo _)
}).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
}
/**
@@ -161,6 +142,15 @@ trait RepositoryService { self: AccountService =>
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/**
* Remove all collaborators from the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
*/
def removeCollaborators(userName: String, repositoryName: String): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/**
* Returns the list of collaborators name which is sorted with ascending order.
*

View File

@@ -6,7 +6,6 @@ import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils
import util.JGitUtil.DiffInfo
import util.{Directory, JGitUtil}
import org.eclipse.jgit.lib.RepositoryBuilder
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import java.util.concurrent.ConcurrentHashMap
@@ -64,16 +63,16 @@ object WikiService {
trait WikiService {
import WikiService._
def createWikiRepository(owner: model.Account, repository: String): Unit = {
lock(owner.userName, repository){
val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = {
lock(owner, repository){
val dir = Directory.getWikiRepositoryDir(owner, repository)
if(!dir.exists){
try {
JGitUtil.initRepository(dir)
saveWikiPage(owner.userName, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", owner, "Initial Commit")
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
} finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository))
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
}
}
}
@@ -84,14 +83,11 @@ trait WikiService {
*/
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
try {
if(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
}
} catch {
// TODO no commit, but it should not judge by exception.
case e: NullPointerException => None
}
} else None
}
}
@@ -100,7 +96,7 @@ trait WikiService {
*/
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
try {
if(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index)
val fileName = if(index < 0) path else path.substring(index + 1)
@@ -108,10 +104,7 @@ trait WikiService {
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes
}
} catch {
// TODO no commit, but it should not judge by exception.
case e: NullPointerException => None
}
} else None
}
}
@@ -119,10 +112,12 @@ trait WikiService {
* Returns the list of wiki page names.
*/
def getWikiPageList(owner: String, repository: String): List[String] = {
JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".")
.filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", ""))
.sortBy(x => x)
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", ""))
.sortBy(x => x)
}
}
/**
@@ -210,12 +205,16 @@ trait WikiService {
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
if(!workDir.exists){
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
val git =
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
git.getRepository.close // close .git resources.
} else {
Git.open(workDir).pull.call
JGitUtil.withGit(workDir){ git =>
git.pull.call
}
}
}

View File

@@ -49,6 +49,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection): Unit = {
super.update(conn)
@@ -128,6 +129,7 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
}

View File

@@ -32,7 +32,8 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){
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 {

View File

@@ -1,17 +1,15 @@
package servlet
import util.FileUploadUtil
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
import app.FileUploadControllerBase
/**
* Removes session associated temporary files when session is destroyed.
*/
class SessionCleanupListener extends HttpSessionListener {
class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
def sessionCreated(se: HttpSessionEvent): Unit = {}
def sessionDestroyed(se: HttpSessionEvent): Unit = {
FileUploadUtil.removeTemporaryFiles()(se.getSession)
}
def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
}

View File

@@ -1,8 +1,6 @@
package util
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
/**
* Provides directories used by GitBucket.

View File

@@ -1,33 +0,0 @@
package util
import java.text.SimpleDateFormat
import javax.servlet.http.HttpSession
import util.Directory._
import org.apache.commons.io.FileUtils
object FileUploadUtil {
def generateFileId: String =
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
def TemporaryDir(implicit session: HttpSession): java.io.File =
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
new java.io.File(TemporaryDir, fileId)
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
// getTemporaryFile(fileId).delete()
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
if(filename.isDefined){
session.removeAttribute("upload_" + fileId)
}
filename
}
}

View File

@@ -1,6 +1,6 @@
package util
import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
import org.apache.commons.io.{IOUtils, FileUtils}
import java.net.URLConnection
import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}

View File

@@ -1,6 +1,5 @@
package util
import scala.slick.driver.H2Driver.simple._
import scala.util.matching.Regex
/**
@@ -25,11 +24,6 @@ object Implicits {
}
}
// TODO Should this implicit conversion move to model.Functions?
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
implicit class RichString(value: String){
def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = {
val sb = new StringBuilder()

View File

@@ -348,51 +348,11 @@ object JGitUtil {
* @return the list of latest commit
*/
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val map = new scala.collection.mutable.HashMap[String, RevCommit]
val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision)))
//revWalk.sort(RevSort.REVERSE);
val i = revWalk.iterator
while(i.hasNext && map.size != paths.length){
val commit = i.next
if(commit.getParentCount == 0){
// Initial commit
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.reset()
treeWalk.setRecursive(true)
treeWalk.addTree(commit.getTree)
while (treeWalk.next) {
paths.foreach { path =>
if(treeWalk.getPathString.startsWith(path) && !map.contains(path)){
map.put(path, commit)
}
}
}
treeWalk.release
} else {
(0 to commit.getParentCount - 1).foreach { i =>
val parent = revWalk.parseCommit(commit.getParent(i).getId())
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
df.setRepository(git.getRepository)
df.setDiffComparator(RawTextComparator.DEFAULT)
df.setDetectRenames(true)
val diffs = df.scan(parent.getTree(), commit.getTree)
diffs.asScala.foreach { diff =>
paths.foreach { path =>
if(diff.getChangeType != ChangeType.DELETE && diff.getNewPath.startsWith(path) && !map.contains(path)){
map.put(path, commit)
}
}
}
}
}
revWalk.release
}
map.toMap
val start = git.getRepository.resolve(revision)
paths.map { path =>
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
(path, commit)
}.toMap
}
/**
@@ -524,10 +484,12 @@ object JGitUtil {
}
}
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
val config = repository.getConfig
config.setBoolean("http", null, "receivepack", true)
config.save
}
}
}

View File

@@ -20,4 +20,9 @@ object StringUtil {
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
}

View File

@@ -1,7 +1,6 @@
package util
import jp.sf.amateras.scalatra.forms._
import scala.Some
trait Validations {

View File

@@ -12,8 +12,13 @@ trait AvatarImageProvider { self: RequestCache =>
*/
protected def getAvatarImageHtml(userName: String, size: Int,
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
val src = getAccountByUserName(userName).collect { case account if(account.image.isEmpty) =>
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
val src = getAccountByUserName(userName).map { account =>
if(account.image.isEmpty){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
} else {
s"""${context.path}/${userName}/_avatar"""
}
} getOrElse {
if(mailAddress.nonEmpty){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
@@ -22,7 +27,7 @@ trait AvatarImageProvider { self: RequestCache =>
}
}
if(tooltip){
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title=${userName}/>""")
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else {
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
}

View File

@@ -82,6 +82,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def assets(implicit context: app.Context): String =
s"${context.path}/assets"
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/**
* Implicit conversion to add mkHtml() to Seq[Html].

View File

@@ -1,23 +1,6 @@
@(account: model.Account, activities: List[model.Activity])(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "activity")
@helper.html.activities(activities)
</div>
</div>
</div>
@main(account, groupNames, "activity"){
@helper.html.activities(activities)
}

View File

@@ -0,0 +1,48 @@
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
</div>
<div class="block">
@if(account.url.isDefined){
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
}
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
@if(groupNames.nonEmpty){
<div>
<div>Groups</div>
@groupNames.map { groupName =>
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
}
</div>
}
</div>
<div class="span8">
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
} else {
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
}
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>
@body
</div>
</div>
</div>
}

View File

@@ -0,0 +1,16 @@
@(account: model.Account, members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, Nil, "members"){
@if(members.isEmpty){
No members
} else {
@members.map { userName =>
<div class="block">
<div class="block-header">
@avatar(userName, 20) <a href="@url(userName)">@userName</a>
</div>
</div>
}
}
}

View File

@@ -1,42 +1,25 @@
@(account: model.Account, repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "repositories")
@if(repositories.isEmpty){
No repositories
} else {
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
@main(account, groupNames, "repositories"){
@if(repositories.isEmpty){
No repositories
} else {
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
</div>
</div>
}
}
}

View File

@@ -1,14 +0,0 @@
@(account: model.Account, active: String)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>

View File

@@ -10,6 +10,9 @@
<li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a>
</li>
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,121 @@
@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(account.isEmpty) "New Group" else "Update Group"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span7">
<fieldset>
<label for="groupName"><strong>Group name</strong></label>
<span id="error-groupName" class="error"></span>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span5">
<fieldset>
<label><strong>Members</strong></label>
<ul id="members" class="collaborator">
@members.map { userName =>
<li data-name="@userName">
<a href="@path/@url(userName)">@userName</a>
<a href="#" class="remove">(remove)</a>
</li>
}
</ul>
<input type="text" id="memberName" style="width: 200px; margin-bottom: 0px;"/>
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<div>
<span class="error" id="error-memberName"></span>
</div>
</fieldset>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
</fieldset>
</form>
}
}
<script>
$(function(){
$('#memberName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#members li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-memberName').text('User has been already added.');
return false;
}
// check existence
$.post('@path/admin/users/_usercheck', {
'userName': userName
}, function(data, status){
if(data == 'true'){
// add member
$('#members').append($('<li>')
.data('name', userName)
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a>').attr('href', '#').addClass('remove').text('(remove)')));
$('#memberName').val('');
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
} else {
$('#error-memberName').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
// remove member
$(this).parent().remove();
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
console.log(e.keyCode);
return !(e.keyCode == 13);
});
});
</script>

View File

@@ -1,30 +1,46 @@
@(users: List[model.Account])(implicit context: app.Context)
@(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Manage Users"){
@admin.html.menu("users"){
<div style="text-align: right; margin-bottom: 4px;">
<a href="@path/admin/users/_new" class="btn">New User</a>
<a href="@path/admin/users/_newuser" class="btn">New User</a>
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
</div>
<table class="table table-bordered table-hover">
@users.map { account =>
<tr>
<td>
<div class="pull-right">
<a href="@path/admin/users/@account.userName/_edit">Edit</a>
@if(account.isGroupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
} else {
<a href="@path/admin/users/@account.userName/_edituser">Edit</a>
}
</div>
<div class="strong">
@avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a>
@if(account.isAdmin){
(Administrator)
@if(account.isGroupAccount){
(Group)
} else {
(Normal)
@if(account.isAdmin){
(Administrator)
} else {
(Normal)
}
}
@if(account.isGroupAccount){
@members(account.userName).map { userName =>
@avatar(userName, 20, tooltip = true)
}
}
</div>
<div>
<hr>
<i class="icon-envelope"></i> @account.mailAddress
@if(!account.isGroupAccount){
<i class="icon-envelope"></i> @account.mailAddress
}
@account.url.map { url =>
<i class="icon-home"></i> @url
}
@@ -32,7 +48,9 @@
<div>
<span class="muted">Registered:</span> @datetime(account.registeredDate)
<span class="muted">Updated:</span> @datetime(account.updatedDate)
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
@if(!account.isGroupAccount){
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
}
</div>
</td>
</tr>

View File

@@ -2,27 +2,28 @@
@import context._
@html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_new} else {@path/admin/users/@account.get.userName/_edit}" validate="true">
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
<div class="row-fluid">
<div class="span6">
<fieldset>
<label for="userName"><strong>Username</strong></label>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
<span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label for="password"><strong>Password</strong>
<label for="password">
<strong>Password</strong>
@if(account.isDefined){
(Input to change password)
}
</label>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset>
<fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
</fieldset>
<fieldset>
<label><strong>User Type</strong></label>
@@ -35,8 +36,8 @@
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
</fieldset>
</div>
<div class="span6">

View File

@@ -0,0 +1,48 @@
@(listparts: twirl.api.Html,
allCount: Int,
assignedCount: Int,
createdByCount: Int,
repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Your Issues"){
@dashboard.html.tab("issues")
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "all"){ class="active"}>
<a href="@path/dashboard/issues/repos@condition.toURL">
<span class="count-right">@allCount</span>
In your repositories
</a>
</li>
<li@if(filter == "assigned"){ class="active"}>
<a href="@path/dashboard/issues/assigned@condition.toURL">
<span class="count-right">@assignedCount</span>
Assigned to you
</a>
</li>
<li@if(filter == "created_by"){ class="active"}>
<a href="@path/dashboard/issues/created_by@condition.toURL">
<span class="count-right">@createdByCount</span>
Created by you
</a>
</li>
</ul>
<hr/>
<ul class="nav nav-pills nav-stacked small">
@repositories.map { case (owner, name, count) =>
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
<span class="count-right">@count</span>
@owner/@name
</a>
</li>
}
</ul>
</div>
@listparts
</div>
}

View File

@@ -0,0 +1,8 @@
@(active: String = "")(implicit context: app.Context)
@import context._
<ul class="nav nav-tabs">
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
@if(loginAccount.isDefined){
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
}
</ul>

View File

@@ -12,31 +12,33 @@
@activityMessage(activity.message)
</div>
@activity.additionalInfo.map { additionalInfo =>
@(activity.activityType match {
case "create_wiki" => {
<div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "edit_wiki" => {
<div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "push" => {
<div class="small activity-message">
{additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
<div>...</div>
} else {
<div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span>
</div>
}
}}
</div>
}
case _ => {
<div class=" activity-message">{additionalInfo}</div>
}
})
@if(additionalInfo.nonEmpty){
@(activity.activityType match {
case "create_wiki" => {
<div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "edit_wiki" => {
<div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "push" => {
<div class="small activity-message">
{additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
<div>...</div>
} else {
<div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span>
</div>
}
}}
</div>
}
case _ => {
<div class=" activity-message">{additionalInfo}</div>
}
})
}
}
</div>
}

View File

@@ -1,32 +1,32 @@
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String)
@defining(view.Pagination(page, count, service.IssuesService.IssueLimit, width)){ p =>
@defining(view.Pagination(page, count, limit, width)){ p =>
@if(p.count > p.limit){
<div class="pagination">
<ul>
@if(page == 1){
<li class="disabled"><span>&#9664;</span></li>
<li class="disabled"><span>&#9664;</span></li>
} else {
<li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li>
<li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li>
}
@for(i <- 1 to p.max){
@if(i == p.max && p.omitRight){
<li><span>&hellip;</span></li>
}
@if(i == page){
<li class="active"><span>@i</span></li>
} else {
@if(p.visibleFor(i)){
<li><a href="@baseURL&page=@i">@i</a></li>
}
}
@if(i == 1 && p.omitLeft){
<li><span>&hellip;</span></li>
}
@if(i == p.max && p.omitRight){
<li><span>&hellip;</span></li>
}
@if(i == page){
<li class="active"><span>@i</span></li>
} else {
@if(p.visibleFor(i)){
<li><a href="@baseURL&page=@i">@i</a></li>
}
}
@if(i == 1 && p.omitLeft){
<li><span>&hellip;</span></li>
}
}
@if(page == p.max){
<li class="disabled"><span>&#9654;</span></li>
<li class="disabled"><span>&#9654;</span></li>
} else {
<li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li>
<li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li>
}
</ul>
</div>

View File

@@ -1,13 +1,13 @@
@(activities: List[model.Activity],
repositories: List[service.RepositoryService.RepositoryInfo],
recentRepositories: List[service.RepositoryService.RepositoryInfo],
systemSettings: service.SystemSettingsService.SystemSettings,
userRepositories: List[String])(implicit context: app.Context)
userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@main("GitBucket"){
@dashboard.html.tab()
<div class="row-fluid">
<div class="span8">
<h3>News Feed</h3>
@helper.html.activities(activities)
</div>
<div class="span4">
@@ -28,9 +28,15 @@
<td>No repositories</td>
</tr>
} else {
@userRepositories.map { repositoryName =>
@userRepositories.map { repository =>
<tr>
<td><a href="@path/@loginAccount.get.userName/@repositoryName"><strong>@repositoryName</strong></a></td>
<td>
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><strong>@repository.name</strong></a>
} else {
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
}
</td>
</tr>
}
}
@@ -43,12 +49,12 @@
Recent updated repositories
</th>
</tr>
@if(repositories.isEmpty){
@if(recentRepositories.isEmpty){
<tr>
<td>No repositories</td>
</tr>
} else {
@repositories.map { repository =>
@recentRepositories.map { repository =>
<tr>
<td>
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>

View File

@@ -5,7 +5,7 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("New Issue - " + repository.owner + "/" + repository.name){
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("", repository)
<form action="@url(repository)/issues/new" method="POST" validate="true">
@@ -22,7 +22,6 @@
<input type="hidden" name="assignedUserName" value=""/>
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
@@ -34,9 +33,23 @@
<input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId"><i class="icon-while"></i> @milestone.title</a></li>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-while"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
@@ -91,7 +104,7 @@ $(function(){
});
$('a.milestone').click(function(){
var title = $(this).text();
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$('a.milestone i.icon-ok').attr('class', 'icon-white');

View File

@@ -2,13 +2,13 @@
comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[model.Milestone],
milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}"){
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("issues", repository)
<ul class="nav nav-tabs">
@@ -38,7 +38,6 @@
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
@@ -47,17 +46,38 @@
<div class="pull-right">
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.find(_.milestoneId == milestoneId).map { milestone =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <strong>@milestone.title</strong>
}
}.getOrElse("No milestone")
</span>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}
}
</div>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId"><i class="icon-white"></i> @milestone.title</a></li>
@milestones.map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
@@ -68,6 +88,12 @@
</div>
</div>
</div>
<div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<strong>@participants.size</strong> @plural(participants.size, "participant")
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
}
</div>
@comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
@@ -175,7 +201,8 @@ $(function(){
});
$('a.assign').click(function(){
var userName = $(this).data('name');
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
@@ -185,27 +212,31 @@ $(function(){
if(userName == ''){
$('#label-assigned').text('No one is assigned');
} else {
$('#label-assigned').html($('<span>')
$('#label-assigned').empty()
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned'));
.append(' is assigned');
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
}
});
});
$('a.milestone').click(function(){
var title = $(this).text();
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(){
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title)));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});

View File

@@ -15,7 +15,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Issues - " + repository.owner + "/" + repository.name){
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("issues", repository)
<div class="row-fluid">
@@ -65,21 +65,45 @@
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
</a>
</li>
@milestones.map { milestone =>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
</a>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
<div class="milestone-progress" style="margin-top: 8px;">
@if(closedCount > 0){
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span>
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
<div style="margin-top: 4px;">
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
</div>
<span class="muted small">@openCount open issues</span>
@if(milestone.closedDate.isDefined){
@milestone.closedDate.map { closedDate =>
<span class="small">Closed in @date(closedDate)</span>
}
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="small">Due in @date(dueDate)</span>
}
}
}
</div>
<span class="muted small">@openCount open issues</span>
}
}
<hr/>
<strong>Labels</strong>
@@ -107,145 +131,8 @@
@_root_.issues.labels.html.edit(None, repository)
}
</div>
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</div>
<div class="btn-group">
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
<div class="btn-group">
<button class="btn dropdown-toggle" data-toggle="dropdown">
Sort:
<strong>
@if(condition.sort == "created" && condition.direction == "desc"){ Newest }
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest }
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented }
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented }
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated }
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated }
</strong>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
</ul>
</div>
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
<a href="@url(repository)/issues/new">Create a new issue.</a>
}
</td>
</tr>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini" id="state"><strong>@{if(condition.state == "open") "Close" else "Reopen"}</strong></button>
</div>
@helper.html.dropdown("Label") {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Assignee") {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
@helper.html.dropdown("Milestone") {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId"><i class="icon-white"></i> @milestone.title</a></li>
}
}
</td>
</tr>
}
}
@issues.map { case (issue, labels, commentCount) =>
<tr>
<td>
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
<a href="@url(repository)/issues/@issue.issueId" class="issue-title">@issue.title</a>
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
#@issue.issueId
</span>
<div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@url(repository)/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
</label>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>
</div>
@***** show issue list *****@
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
</div>
@if(hasWritePermission){
<form id="batcheditForm" method="POST">

View File

@@ -0,0 +1,176 @@
@(issues: List[(model.Issue, List[model.Label], Int)],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
collaborators: List[String] = Nil,
milestones: List[model.Milestone] = Nil,
labels: List[model.Label] = Nil,
repository: Option[service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
</a>
}
@if(condition.repo.isDefined){
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</div>
<div class="btn-group">
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
<div class="btn-group">
<button class="btn dropdown-toggle" data-toggle="dropdown">
Sort:
<strong>
@if(condition.sort == "created" && condition.direction == "desc"){ Newest }
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest }
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented }
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented }
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated }
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated }
</strong>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
</ul>
</div>
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini" id="state"><strong>@{if(condition.state == "open") "Close" else "Reopen"}</strong></button>
</div>
@helper.html.dropdown("Label") {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Assignee") {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
@helper.html.dropdown("Milestone") {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.map { milestone =>
<li>
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</td>
</tr>
}
}
@issues.map { case (issue, labels, commentCount) =>
<tr>
<td>
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
#@issue.issueId
</span>
<div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @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>
</label>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>
</div>

View File

@@ -1,7 +1,7 @@
@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Milestones - " + repository.owner + "/" + repository.name){
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository)
@issues.html.tab("milestones", repository)
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">

View File

@@ -4,7 +4,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Milestones - " + repository.owner + "/" + repository.name){
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository)
@issues.html.tab("milestones", repository)
<div class="row-fluid">
@@ -42,9 +42,13 @@
@if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span>
} else {
@if(milestone.dueDate.isDefined){
<span class="muted">Due in @date(milestone.dueDate.get)</span>
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
}
@@ -65,18 +69,7 @@
</div>
<span class="muted">@closedCount closed - @openCount open</span>
</div>
<div class="milestone-progress">
@if(closedCount > 0){
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span>
}
<span class="milestone-percentage">
@if(closedCount == 0){
0%
} else {
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
}
</span>
</div>
@progress(openCount + closedCount, closedCount, true)
</div>
</div>
@if(milestone.description.isDefined){

View File

@@ -0,0 +1,15 @@
@(total: Int, progress: Int, showPercentage: Boolean)
<div class="milestone-progress">
@if(progress > 0){
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
}
@if(showPercentage){
<span class="milestone-percentage">
@if(progress == 0){
0%
} else {
@((progress.toDouble / total.toDouble * 100).toInt)%
}
</span>
}
</div>

View File

@@ -1,4 +1,4 @@
@(title: String)(body: Html)(implicit context: app.Context)
@(title: String, repository: Option[service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
<!DOCTYPE html>
@@ -29,30 +29,37 @@
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
</head>
<body>
<div class="navbar">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">GitBucket</a>
<div class="nav-collapse collapse pull-right">
@if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></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>
<form id="search" action="@path/search" method="POST">
<div class="navbar">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">GitBucket</a>
<div class="nav-collapse collapse pull-right header-menu">
@repository.map { repository =>
<input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</a>
}
</div><!--/.nav-collapse -->
@if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></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 {
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</a>
}
</div><!--/.nav-collapse -->
</div>
</div>
</div>
</form>
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
}
@@ -60,5 +67,12 @@
<div class="container body">
@body
</div>
<script>
$(function(){
$('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != '';
});
});
</script>
</body>
</html>

View File

@@ -1,47 +1,73 @@
@()(implicit context: app.Context)
@(groupNames: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name"><strong>Repository name</strong></label>
<input type="text" name="name" id="name" />
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong> (optional)</label>
<input type="text" name="description" id="description" style="width: 95%;"/>
</fieldset>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name"><strong>Repository name</strong></label>
<div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
<button class="btn dropdown-toggle" data-toggle="dropdown">
<strong>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</strong>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="icon-ok"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
@groupNames.map { groupName =>
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
}
</ul>
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<label for="createReadme">
<input type="checkbox" name="createReadme" id="createReadme"/>
<strong>Initialize this repository with a README</strong>
<div>
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset>
</form>
/
<input type="text" name="name" id="name" />
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong> (optional)</label>
<input type="text" name="description" id="description" style="width: 95%;"/>
</fieldset>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<label for="createReadme">
<input type="checkbox" name="createReadme" id="createReadme"/>
<strong>Initialize this repository with a README</strong>
<div>
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset>
</form>
</div>
}
<script>
$('#owner-dropdown a').click(function(){
var userName = $(this).data('name');
$('#owner').val(userName);
$('#owner-dropdown i').attr('class', 'icon-white');
$(this).find('i').attr('class', 'icon-ok');
$('#owner-dropdown strong').html($(this).find('span').html());
});
</script>

View File

@@ -5,7 +5,7 @@
latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner+"/"+repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, "files")
<div class="head">

View File

@@ -8,7 +8,7 @@
@import view.helpers._
@import util.Implicits._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@html.main(commit.shortMessage){
@html.main(commit.shortMessage, Some(repository)){
@html.header("code", repository)
@tab(commitId, repository, "commits")
<table class="table table-bordered">

View File

@@ -6,7 +6,7 @@
hasNext: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner+"/"+repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, if(pathList.isEmpty) "commits" else "files")
<div class="head">

View File

@@ -6,7 +6,7 @@
readme: Option[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, "files")
<div class="head">

View File

@@ -1,7 +1,7 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
<pre>

View File

@@ -26,9 +26,9 @@
</div>
</li>
}
<li@if(active=="files"){ class="active"}><a href="@url(repository)/tree/@id">Files</a></li>
<li@if(active=="files" ){ class="active"}><a href="@url(repository)/tree/@id">Files</a></li>
<li@if(active=="commits"){ class="active"}><a href="@url(repository)/commits/@id">Commits</a></li>
<li@if(active=="tags"){ class="active"}><a href="@url(repository)/tags">Tags@if(repository.tags.length > 0){ <span class="badge">@repository.tags.length</span>}</a></li>
<li@if(active=="tags" ){ class="active"}><a href="@url(repository)/tags">Tags@if(repository.tags.length > 0){ <span class="badge">@repository.tags.length</span>}</a></li>
<li class="pull-right">
<div class="input-append">
<input type="text" value="@repository.url" id="repository-url" readonly>

View File

@@ -1,7 +1,7 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(repository.repository.defaultBranch, repository, "tags", true)
<h1>Tags</h1>

View File

@@ -0,0 +1,26 @@
@(files: List[service.RepositorySearchService.FileSearchResult],
issueCount: Int,
query: String,
page: Int,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.RepositorySearchService._
@html.main("Search Results", Some(repository)){
@menu("code", files.size, issueCount, query, repository){
@if(files.isEmpty){
<h4>We couldn't find any code matching '@query'</h4>
} else {
<h4>We've found @files.size code @plural(files.size, "result")</h4>
}
@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">Latest commit at @datetime(file.lastModified)</div>
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
</div>
}
@helper.html.paginator(page, files.size, CodeLimit, 10,
s"${url(repository)}/search?q=${urlEncode(query)}&type=code")
}
}

View File

@@ -0,0 +1,35 @@
@(issues: List[service.RepositorySearchService.IssueSearchResult],
fileCount: Int,
query: String,
page: Int,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.RepositorySearchService._
@html.main("Search Results", Some(repository)){
@menu("issue", fileCount, issues.size, query, repository){
@if(issues.isEmpty){
<h4>We couldn't find any code matching '@query'</h4>
} else {
<h4>We've found @issues.size code @plural(issues.size, "result")</h4>
}
@issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue =>
<div class="block">
<div class="pull-right muted">#@issue.issueId</div>
<h4 style="margin-top: 0px;"><a href="@url(repository)/issues/@issue.issueId">@issue.title</a></h4>
@if(issue.highlightText.nonEmpty){
<pre>@Html(issue.highlightText)</pre>
}
<div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
at @datetime(issue.registeredDate)
@if(issue.commentCount > 0){
&nbsp;&nbsp;<i class="icon-comment"></i><strong>@issue.commentCount</strong> @plural(issue.commentCount, "comment")
}
</div>
</div>
}
@helper.html.paginator(page, issues.size, IssueLimit, 10,
s"${url(repository)}/search?q=${urlEncode(query)}&type=issue")
}
}

View File

@@ -0,0 +1,37 @@
@(active: String, fileCount: Int, issueCount: Int, query: String,
repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.header("", repository)
<div class="row-fluid">
<div class="span3">
<div class="box">
<ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="code"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=code">
@if(fileCount != 0){
<span class="badge pull-right">@fileCount</span>
}
Code
</a>
</li>
<li@if(active=="issue"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=issue">
@if(issueCount != 0){
<span class="badge pull-right">@issueCount</span>
}
Issue
</a>
</li>
</ul>
</div>
</div>
<div class="span9">
<form action="@url(repository)/search" method="GET">
<input type="text" name="q" value="@query" style="width: 80%; margin-bottom: 0px;"/>
<input type="submit" value="Search" class="btn" style="width: 15%;"/>
<input type="hidden" name="type" value="@active"/>
</form>
@body
</div>
</div>

View File

@@ -1,7 +1,9 @@
@(collaborators: List[String], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@(collaborators: List[String],
isGroupRepository: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Settings"){
@html.main("Settings", Some(repository)){
@html.header("settings", repository)
@menu("collaborators", repository){
<h3>Manage Collaborators</h3>
@@ -9,26 +11,32 @@
@collaborators.map { collaboratorName =>
<li>
<a href="@url(collaboratorName)">@collaboratorName</a>
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
@if(!isGroupRepository){
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
}
</li>
}
</ul>
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true">
<div>
<span class="error" id="error-userName"></span>
</div>
<input type="text" name="userName" id="userName" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/>
</form>
@if(!isGroupRepository){
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true">
<div>
<span class="error" id="error-userName"></span>
</div>
<input type="text" name="userName" id="userName" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/>
</form>
}
}
}
<script>
$('#userName').typeahead({
source: function (query, process) {
return $.get('@url(repository)/settings/collaborators/proposals', { query: query },
function (data) {
return process(data.options);
});
}
$(function(){
$('#userName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
});
</script>

View File

@@ -1,7 +1,7 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Delete Repository"){
@html.main("Delete Repository", Some(repository)){
@html.header("settings", repository)
@menu("delete", repository){
<form id="form" method="post" action="@url(repository)/settings/delete">

View File

@@ -1,7 +1,7 @@
@(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Settings"){
@html.main("Settings", Some(repository)){
@html.header("settings", repository)
@menu("options", repository){
@helper.html.information(info)
@@ -13,8 +13,7 @@
<label for="description"><strong>Description</strong></label>
<input type="text" name="description" id="description" style="width: 600px;" value="@repository.repository.description"/>
</fieldset>
<hr>
<fieldset>
<fieldset class="margin">
<label for="defaultBranch"><strong>Default Branch</strong></label>
<select name="defaultBranch" id="defaultBranch">
@repository.branchList.map { branch =>
@@ -22,25 +21,24 @@
}
</select>
</fieldset>
<hr>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false"@if(!repository.repository.isPrivate){ checked}>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true"@if(repository.repository.isPrivate){ checked}>
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
</div>
</div>
@*

View File

@@ -4,7 +4,7 @@
@import context._
@import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@html.main("Compare Revisions - " + repository.owner + "/" + repository.name){
@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab("history", repository)
<ul class="nav nav-tabs">

View File

@@ -3,7 +3,7 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main((if(pageName == "") "New Page" else pageName) + " - " + repository.owner + "/" + repository.name){
@html.main(s"${if(pageName == "") "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab("", repository)
<ul class="nav nav-tabs">

View File

@@ -3,7 +3,7 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("History - " + repository.owner + "/" + repository.name){
@html.main(s"History - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab(if(pageName.isEmpty) "history" else "", repository)
<ul class="nav nav-tabs">

View File

@@ -4,7 +4,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(pageName + " - " + repository.owner + "/" + repository.name){
@html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab((if(pageName == "Home") "home" else ""), repository)
<ul class="nav nav-tabs">

View File

@@ -1,7 +1,7 @@
@(pages: List[String], repository: service.RepositoryService.RepositoryInfo, hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Pages - " + repository.owner + "/" + repository.name){
@html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab("pages", repository)
<ul class="nav nav-tabs">

View File

@@ -81,22 +81,29 @@
<servlet>
<servlet-name>H2Console</servlet-name>
<servlet-class>org.h2.server.web.WebServlet</servlet-class>
<!--
<init-param>
<param-name>webAllowOthers</param-name>
<param-value></param-value>
</init-param>
<init-param>
<param-name>trace</param-name>
<param-value></param-value>
</init-param>
-->
<!--
<init-param>
<param-name>trace</param-name>
<param-value></param-value>
</init-param>
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>H2Console</servlet-name>
<url-pattern>/console/*</url-pattern>
</servlet-mapping>
</servlet-mapping>
<!-- ===================================================================== -->
<!-- Session timeout -->
<!-- ===================================================================== -->
<session-config>
<session-timeout>1440</session-timeout>
</session-config>
</web-app>

View File

@@ -5,6 +5,9 @@ body {
color: #333;
}
/* ======================================================================== */
/* Global Header */
/* ======================================================================== */
div.navbar-inner {
border-radius: 0px;
-webkit-border-radius: 0px;
@@ -16,18 +19,23 @@ div.navbar-inner {
padding-right: 0px;
}
div.header-menu {
line-height: 40px;
}
div.header-menu input,
div.header-menu a.btn {
margin-top: 0px;
margin-bottom: 0px;
}
div.nav-collapse a.menu {
margin-right: 12px;
line-height: 40px;
}
div.nav-collapse a.btn-last {
margin-right: 30px;
}
div.nav-collapse a.btn-last,
div.nav-collapse a.menu-last {
margin-right: 30px;
line-height: 40px;
}
div.gitbucket-version {
@@ -37,6 +45,9 @@ div.gitbucket-version {
margin-right: 10px;
}
/* ======================================================================== */
/* Repository Header */
/* ======================================================================== */
table.global-nav {
width: 920px;
margin-bottom: 10px;
@@ -59,6 +70,9 @@ table.global-nav th a:link, table.global-nav th a:hover, table.global-nav th a:v
text-decoration: none;
}
/* ======================================================================== */
/* General Styles */
/* ======================================================================== */
div.head {
font-size: large;
margin-bottom: 10px;
@@ -243,6 +257,15 @@ div.account-image {
margin-bottom: 8px;
}
ul.dropdown-menu li {
border-bottom: 1px solid #eee;
}
ul.dropdown-menu :last-child {
border-bottom: none;
}
/****************************************************************************/
/* Sign-in form */
/****************************************************************************/
@@ -427,10 +450,16 @@ a#clear-filter {
margin-bottom: 10px;
}
span.milestone-alert {
font-weight: bold;
color: #bd2c00;
}
a.milestone-title {
font-size: 120%;
font-weight: bold;
}
div.milestone-description {
border-top: 1px solid #eee;
color: #666;
@@ -449,6 +478,15 @@ div.milestone-menu a.delete {
color: #b00;
}
div#milestone-progress-area {
display: inline-block;
}
div#milestone-progress-area div.milestone-progress {
width: 150px;
margin-bottom: -6px;
}
div.milestone-progress {
position: relative;
height: 20px;
@@ -511,6 +549,11 @@ div.issue-avatar-image {
}
div.issue-box {
margin-bottom: 5px;
margin-left: 50px;
}
div.issue-participants {
margin-bottom: 15px;
margin-left: 50px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B