Merge branch 'master' into ssh-access

Conflicts:
	src/main/scala/servlet/GitRepositoryServlet.scala
This commit is contained in:
takezoe
2014-03-08 18:59:13 +09:00
48 changed files with 943 additions and 497 deletions

View File

@@ -23,7 +23,7 @@ Following features are not implemented, but we will make them in the future rele
- File editing in repository viewer
- Comment for the changeset
- Network graph
- Statics
- Statistics
- Watch / Star
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -42,7 +42,6 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
- --port=[NUMBER]
- --prefix=[CONTEXTPATH]
- --host=[HOSTNAME]
- --https=true
- --gitbucket.home=[DATA_DIR]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
@@ -59,8 +58,12 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 1.11 - End of Feb 2014
- Base URL for redirect, notification and repository URL box is configurable
### 1.11.1 - 06 Mar 2014
- Bug fix
### 1.11 - 01 Mar 2014
- Base URL for redirection, notification and repository URL box is configurable
- Remove ```--https``` option because it's possible to substitute in the base url
- Headline anchor is available for Markdown contents such as Wiki page
- Improve H2 connectivity
- Label is available for pull requests not only issues

View File

@@ -4,9 +4,6 @@
# Server port
#GITBUCKET_PORT=8080
# Force HTTPS scheme
#GITBUCKET_HTTPS=false
# Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket

View File

@@ -39,9 +39,6 @@ start() {
if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
if [ $GITBUCKET_HTTPS ]; then
START_OPTS="${START_OPTS} --https=true"
fi
# Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*

2
sbt.sh
View File

@@ -1 +1 @@
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"

View File

@@ -25,8 +25,6 @@ public class JettyLauncher {
port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")) {
contextPath = dim[1];
} else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
forceHttps = true;
} else if(dim[0].equals("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]);
}
@@ -36,7 +34,7 @@ public class JettyLauncher {
Server server = new Server();
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps);
SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) {
connector.setHost(host);
}
@@ -62,19 +60,3 @@ public class JettyLauncher {
server.join();
}
}
class HttpsSupportConnector extends SelectChannelConnector {
private boolean forceHttps;
public HttpsSupportConnector(boolean forceHttps) {
this.forceHttps = forceHttps;
}
@Override
public void customize(final EndPoint endpoint, final Request request) throws IOException {
if (this.forceHttps) {
request.setScheme("https");
super.customize(endpoint, request);
}
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE;

View File

@@ -20,7 +20,6 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*")
context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*")

View File

@@ -1,18 +1,25 @@
package app
import service._
import util.{FileUtil, OneselfAuthenticator}
import util._
import util.StringUtil._
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import model.GroupMember
class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
@@ -38,6 +45,40 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
"clearImage" -> trim(label("Clear image" , boolean()))
)(AccountEditForm.apply)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(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()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean()))
)(EditGroupForm.apply)
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class ForkRepositoryForm(owner: String, name: String)
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkRepositoryForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/**
* Displays user information.
*/
@@ -52,14 +93,20 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
getActivitiesByUser(userName, true))
// Members
case "members" if(account.isGroupAccount) =>
_root_.account.html.members(account, getGroupMembers(account.userName))
case "members" if(account.isGroupAccount) => {
val members = getGroupMembers(account.userName)
_root_.account.html.members(account, members.map(_.userName),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
// Repositories
case _ =>
case _ => {
val members = getGroupMembers(account.userName)
_root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
}
} getOrElse NotFound
}
@@ -135,4 +182,228 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
} else NotFound
}
get("/groups/new")(usersOnly {
account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
})
post("/groups/new", newGroupForm)(usersOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect(s"/${form.groupName}")
})
get("/:groupName/_editgroup")(managersOnly {
defining(params("groupName")){ groupName =>
account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
get("/:groupName/_deletegroup")(managersOnly {
defining(params("groupName")){ groupName =>
// Remove from GROUP_MEMBER
updateGroupMembers(groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
}
}
redirect("/")
})
post("/:groupName/_editgroup", editGroupForm)(managersOnly { form =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, false)
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect(s"/${form.groupName}")
} getOrElse NotFound
}
})
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){
if(getRepository(form.owner, form.name, baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { member =>
addCollaborator(form.owner, form.name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(repository.owner == loginUserName){
// redirect to the repository
redirect(s"/${repository.owner}/${repository.name}")
} else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) =>
// redirect to the repository
redirect(s"/${owner}/${name}")
} getOrElse {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id)
}
}
case Left(_) => ???
}
}
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
}
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
private def uniqueRepository: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
}

View File

@@ -20,14 +20,15 @@ import org.scalatra.i18n._
* Provides generic features for controller implementations.
*/
abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations with SystemSettingsService {
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
implicit val jsonFormats = DefaultFormats
// Don't set content type via Accept header.
override def format(implicit request: HttpServletRequest) = ""
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
val httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse]
val context = request.getServletContext.getContextPath
@@ -37,12 +38,15 @@ abstract class ControllerBase extends ScalatraFilter
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
if(account == null){
// Redirect to login form
// TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
} else if(account.isAdmin){
// H2 Console (administrators only)
// TODO Should use the configured base url.
chain.doFilter(request, response)
} else {
// Redirect to dashboard
// TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/")
}
} else if(path.startsWith("/git/")){
@@ -52,12 +56,25 @@ abstract class ControllerBase extends ScalatraFilter
// Scalatra actions
super.doFilter(request, response, chain)
}
} finally {
contextCache.remove();
}
private val contextCache = new java.lang.ThreadLocal[Context]()
/**
* Returns the context object for the request.
*/
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request)
implicit def context: Context = {
contextCache.get match {
case null => {
val context = Context(loadSystemSettings().baseUrl.getOrElse(servletContext.getContextPath), LoginAccount, request)
contextCache.set(context)
context
}
case context => context
}
}
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
@@ -102,28 +119,32 @@ abstract class ControllerBase extends ScalatraFilter
if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin"))
} else {
val currentUrl = baseUrl + defining(request.getQueryString){ queryString =>
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
}
session.setAttribute(Keys.Session.Redirect, currentUrl)
org.scalatra.Unauthorized(redirect("/signin"))
org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
defining(request.getQueryString){ queryString =>
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
}
)))
}
}
}
protected def baseUrl = loadSystemSettings().baseUrl.getOrElse {
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
}
}.replaceFirst("/$", "")
override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
includeContextPath: Boolean = true, includeServletPath: Boolean = true)
(implicit request: HttpServletRequest, response: HttpServletResponse) =
if (path.startsWith("http")) path
else baseUrl + url(path, params, false, false, false)
}
/**
* Context object for the current request.
*
* @param path the context path
*/
case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length)
/**
* Get object from cache.
*

View File

@@ -1,199 +0,0 @@
package app
import util.Directory._
import util.ControlUtil._
import util._
import service._
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import org.scalatra.i18n.Messages
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator
/**
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator =>
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class ForkRepositoryForm(owner: String, name: String)
val newForm = 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())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){
if(getRepository(form.owner, form.name, baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(repository.owner == loginUserName){
// redirect to the repository
redirect(s"/${repository.owner}/${repository.name}")
} else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) =>
// redirect to the repository
redirect(s"/${owner}/${name}")
} getOrElse {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id)
}
}
case Left(_) => ???
}
}
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
}
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): 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(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}

View File

@@ -12,8 +12,7 @@ import org.apache.commons.io.FileUtils
* This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/
class FileUploadController extends ScalatraServlet
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))

View File

@@ -1,7 +1,6 @@
package app
import util._
import util.Implicits._
import service._
import jp.sf.amateras.scalatra.forms._
@@ -31,7 +30,7 @@ trait IndexControllerBase extends ControllerBase {
get("/signin"){
val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute(Keys.Session.Redirect, redirect.get)
flash += Keys.Flash.Redirect -> redirect.get
}
html.signin(loadSystemSettings())
}
@@ -55,7 +54,7 @@ trait IndexControllerBase extends ControllerBase {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
} else {

View File

@@ -79,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
getIssueLabels(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
@@ -105,9 +105,9 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound
})
get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository =>
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = params("branchName")
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
@@ -183,6 +183,18 @@ trait PullRequestsControllerBase extends ControllerBase {
}
}
// close issue by content of pull request
val defaultBranch = getRepository(owner, name, baseUrl).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
issue.content match {
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
case _ =>
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
// call web hook
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
@@ -216,16 +228,16 @@ trait PullRequestsControllerBase extends ControllerBase {
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
} getOrElse NotFound
}
case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
} getOrElse {
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
}
}
}

View File

@@ -5,7 +5,6 @@ import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages
import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
@@ -16,7 +15,7 @@ class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator =>

View File

@@ -82,44 +82,45 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@scala.annotation.tailrec
def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match {
case true if(walk.getPathString == path) => walk.getObjectId(0)
case true => getPathObjectId(path, walk)
def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => getPathObjectId(path, walk)
case false => None
}
val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
}
if(raw){
// Download
defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContent(git, objectId, false) else None
val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else {
// binary
JGitUtil.ContentInfo("binary", None)
} map { objectId =>
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else {
// image or large
JGitUtil.ContentInfo(viewer, None)
}
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
}
val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else {
// binary
JGitUtil.ContentInfo("binary", None)
}
} else {
// image or large
JGitUtil.ContentInfo(viewer, None)
}
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
}
} getOrElse NotFound
}
})
@@ -158,8 +159,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository =>
val branchName = params("branchName")
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
@@ -207,7 +208,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
while(walk.next){
val name = walk.getPathString
val mode = walk.getFileMode(0)
if(mode != FileMode.TREE){
if(mode == FileMode.REGULAR_FILE){
walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name)
val loader = reader.open(objectId)
@@ -266,7 +267,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.guide(repository)
} else {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
//val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
@@ -276,7 +277,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase)
}.map { file =>
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
file -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
repo.html.files(revision, repository,

View File

@@ -4,12 +4,11 @@ import service.{AccountService, SystemSettingsService}
import SystemSettingsService._
import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
class SystemSettingsController extends SystemSettingsControllerBase
with SystemSettingsService with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
trait SystemSettingsControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with AdminAuthenticator =>
private val form = mapping(

View File

@@ -5,6 +5,7 @@ import util.AdminAuthenticator
import util.StringUtil._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils
import util.Directory._
@@ -23,10 +24,10 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String])
members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
members: String, clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
@@ -51,28 +52,28 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text())))
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(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())),
"removed" -> trim(label("Disable" ,boolean()))
"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()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply)
get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap
admin.users.html.list(users, members, includeRemoved)
})
@@ -127,7 +128,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
@@ -139,7 +144,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved)
@@ -155,11 +164,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}
} else {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, memberNames)
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName =>
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
@@ -172,8 +181,17 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}
})
post("/admin/users/_usercheck")(adminOnly {
// TODO Move to other generic controller?
post("/admin/users/_usercheck"){
getAccountByUserName(params("userName")).isDefined
})
}
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
}

View File

@@ -6,18 +6,15 @@ import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages
import scala.Some
import java.util.ResourceBundle
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase with FlashMapSupport {
self: WikiService with RepositoryService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)

View File

@@ -5,10 +5,12 @@ 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 _)
def isManager = column[Boolean]("MANAGER")
def * = groupName ~ userName ~ isManager <> (GroupMember, GroupMember.unapply _)
}
case class GroupMember(
groupName: String,
userName: String
userName: String,
isManager: Boolean
)

View File

@@ -59,7 +59,7 @@ trait AccountService {
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){
@@ -122,18 +122,17 @@ trait AccountService {
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName)
members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName, isManager)
}
}
def getGroupMembers(groupName: String): List[String] =
def getGroupMembers(groupName: String): List[GroupMember] =
Query(GroupMembers)
.filter(_.groupName is groupName.bind)
.sortBy(_.userName)
.map(_.userName)
.list
def getGroupsByUserName(userName: String): List[String] =

View File

@@ -119,16 +119,10 @@ trait IssuesService {
// get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
.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.?)
}
.sortBy(_._4) // labelName
.sortBy { case (t1, commentCount, _,_,_) =>
.sortBy { case (t1, t2) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => commentCount
case "comments" => t2.commentCount
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
@@ -138,6 +132,11 @@ trait IssuesService {
}
}
.drop(offset).take(limit)
.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.?)
}
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
@@ -314,6 +313,14 @@ trait IssuesService {
}.toList
}
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = {
extractCloseId(message).foreach { issueId =>
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
createComment(owner, repository, userName, issue.issueId, "Close", "close")
updateClosed(owner, repository, issue.issueId, true)
}
}
}
}
object IssuesService {

View File

@@ -63,8 +63,8 @@ RepositorySearchService { self: IssuesService =>
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(treeWalk.getFileMode(0) == FileMode.REGULAR_FILE){
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){
val text = StringUtil.convertFromByteArray(bytes)
val lowerText = text.toLowerCase

View File

@@ -147,7 +147,8 @@ trait RepositoryService { self: AccountService =>
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
),
getRepositoryManagers(repository.userName))
}
}
@@ -162,7 +163,8 @@ trait RepositoryService { self: AccountService =>
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
),
getRepositoryManagers(repository.userName))
}
}
@@ -195,10 +197,18 @@ trait RepositoryService { self: AccountService =>
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
),
getRepositoryManagers(repository.userName))
}
}
private def getRepositoryManagers(userName: String): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
} else {
Seq(userName)
}
/**
* Updates the last activity date of the repository.
*/
@@ -280,19 +290,19 @@ object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){
/**
* Creates instance with issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
/**
* Creates instance without issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
}
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])

View File

@@ -3,9 +3,16 @@ package service
import util.Directory._
import util.ControlUtil._
import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
trait SystemSettingsService {
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse {
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
}
}.replaceFirst("/$", "")
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(props.setProperty(BaseURL, _))

View File

@@ -234,7 +234,7 @@ trait WikiService {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
created = false
updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
}
}
}
@@ -268,35 +268,35 @@ trait WikiService {
*/
def deleteWikiPage(owner: String, repository: String, pageName: String,
committer: String, mailAddress: String, message: String): Unit = {
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path != pageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
removed = true
}
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path != pageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
removed = true
}
}
}
if(removed){
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
}
if(removed){
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
}
}
}
}
}
}

View File

@@ -50,6 +50,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 12),
Version(1, 11),
Version(1, 10),
Version(1, 9),

View File

@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
@@ -23,7 +23,7 @@ import util.JGitUtil.CommitInfo
* This servlet provides only Git repository functionality.
* Authentication is provided by [[servlet.BasicAuthenticationFilter]].
*/
class GitRepositoryServlet extends GitServlet {
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
@@ -47,13 +47,24 @@ class GitRepositoryServlet extends GitServlet {
super.init(config)
}
override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = {
val agent = req.getHeader("USER-AGENT")
if(agent == null || !agent.startsWith("git/")){
// redirect for browsers
val paths = req.getRequestURI.split("/")
res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last.replaceFirst("\\.git$", ""))
} else {
// response for git client
super.service(req, res)
}
}
}
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] {
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db)
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
@@ -64,13 +75,11 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths =>
val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "")
val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "") // TODO Use base URL in SystemSettings
logger.debug("repository:" + owner + "/" + repository)
logger.debug("baseURL:" + baseURL)
if(!repository.endsWith(".wiki")){
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL))
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
}
receivePack
}
@@ -79,7 +88,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseURL: String) extends PostReceiveHook
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -143,12 +152,20 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
}
}
// close issues
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach { commit =>
closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
}
}
// call web hook
getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner);
repositoryInfo <- getRepository(owner, repository, baseURL)){
repositoryInfo <- getRepository(owner, repository, baseUrl)){
callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
}
@@ -181,7 +198,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
*/
private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseURL).isDefined){
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
git.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)

View File

@@ -29,7 +29,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
/**
* Allows only the repository owner and administrators.
*/
trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -40,6 +40,9 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists { member =>
member.userName == x.userName && member.isManager == true
}) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
@@ -106,7 +109,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
}
/**
* Allows only the repository owner and administrators.
* Allows only the repository owner (or manager for group repository) and administrators.
*/
trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
@@ -155,3 +158,24 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
}
}
}
/**
* Allows only the group managers.
*/
trait GroupManagerAuthenticator { self: ControllerBase with AccountService =>
protected def managersOnly(action: => Any) = { authenticate(action) }
protected def managersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) }
private def authenticate(action: => Any) = {
{
defining(request.paths){ paths =>
context.loginAccount match {
case Some(x) if(getGroupMembers(paths(0)).exists { member =>
member.userName == x.userName && member.isManager
}) => action
case _ => Unauthorized()
}
}
}
}
}

View File

@@ -3,7 +3,7 @@ package util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import org.eclipse.jgit.transport.RefSpec
import scala.util.control.Exception._
import scala.language.reflectiveCalls
/**
@@ -16,10 +16,8 @@ object ControlUtil {
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally {
if(resource != null){
try {
ignoring(classOf[Throwable]) {
resource.close()
} catch {
case e: Throwable => // ignore
}
}
}

View File

@@ -1,6 +1,7 @@
package util
import scala.util.matching.Regex
import scala.util.control.Exception._
import javax.servlet.http.{HttpSession, HttpServletRequest}
/**
@@ -42,10 +43,8 @@ object Implicits {
sb.toString
}
def toIntOpt: Option[Int] = try {
Option(Integer.parseInt(value))
} catch {
case e: NumberFormatException => None
def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt {
Integer.parseInt(value)
}
}

View File

@@ -11,17 +11,20 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.MissingObjectException
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
/**
* Provides complex JGit operations.
*/
object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
/**
* The repository data.
*
@@ -45,9 +48,10 @@ object JGitUtil {
* @param commitId the last commit id
* @param committer the last committer name
* @param mailAddress the committer's mail address
* @param linkUrl the url of submodule
*/
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
committer: String, mailAddress: String)
committer: String, mailAddress: String, linkUrl: Option[String])
/**
* The commit data.
@@ -104,6 +108,15 @@ object JGitUtil {
*/
case class TagInfo(name: String, time: Date, id: String)
/**
* The submodule data
*
* @param name the module name
* @param path the path in the repository
* @param url the repository url of this module
*/
case class SubmoduleInfo(name: String, path: String, url: String)
/**
* Returns RevCommit from the commit or tag id.
*
@@ -128,7 +141,7 @@ object JGitUtil {
using(Git.open(getRepositoryDir(owner, repository))){ git =>
try {
// get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10000).sum
RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -162,7 +175,7 @@ object JGitUtil {
* @return HTML of the file list
*/
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
@@ -195,22 +208,28 @@ object JGitUtil {
})
}
while (treeWalk.next()) {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
// submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
} else None
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
}
}
}
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name) =>
list.map { case (objectId, fileMode, path, name, linkUrl) =>
FileInfo(
objectId,
fileMode == FileMode.TREE,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name,
commits(path).getCommitterIdent.getWhen,
commits(path).getShortMessage,
commits(path).getName,
commits(path).getCommitterIdent.getName,
commits(path).getCommitterIdent.getEmailAddress)
commits(path).getCommitterIdent.getEmailAddress,
linkUrl)
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
@@ -325,27 +344,6 @@ object JGitUtil {
}.toMap
}
/**
* Get object content of the given id as String from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param large if false then returns None for the large file
* @return the object or None if object does not exist
*/
def getContent(git: Git, id: ObjectId, large: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(large == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
}
}
} catch {
case e: MissingObjectException => None
}
/**
* Returns the tuple of diff of the given commit and the previous commit id.
*/
@@ -377,7 +375,7 @@ object JGitUtil {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
} else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
}))
}
(buffer.toList, None)
@@ -400,8 +398,8 @@ object JGitUtil {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
}
}.toList
}
@@ -494,4 +492,73 @@ object JGitUtil {
newHeadId.getName
}
/**
* Read submodule information from .gitmodules
*/
def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = {
val repository = git.getRepository
getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
(try {
val config = new BlobBasedConfig(repository.getConfig(), bytes)
config.getSubsections("submodule").asScala.map { module =>
val path = config.getString("submodule", module, "path")
val url = config.getString("submodule", module, "url")
SubmoduleInfo(module, path, url)
}
} catch {
case e: ConfigInvalidException => {
logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e)
Nil
}
}).toList
} getOrElse Nil
}
/**
* Get object content of the given path as byte array from the Git repository.
*
* @param git the Git object
* @param revTree the rev tree
* @param path the path
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = {
@scala.annotation.tailrec
def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => getPathObjectId(path, walk)
case false => None
}
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
} flatMap { objectId =>
getContentFromId(git, objectId, fetchLargeFile)
}
}
/**
* Get object content of the given object id as byte array from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
}
}
} catch {
case e: MissingObjectException => None
}
}

View File

@@ -13,12 +13,7 @@ object Keys {
/**
* Session key for the logged in account information.
*/
val LoginAccount = "LOGIN_ACCOUNT"
/**
* Session key for the redirect URL.
*/
val Redirect = "REDIRECT"
val LoginAccount = "loginAccount"
/**
* Session key for the issue search condition in dashboard.
@@ -47,6 +42,20 @@ object Keys {
}
object Flash {
/**
* Flash key for the redirect URL.
*/
val Redirect = "redirect"
/**
* Flash key for the information message.
*/
val Info = "info"
}
/**
* Define request keys.
*/

View File

@@ -31,7 +31,7 @@ object StringUtil {
/**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
* And if given bytes contains UTF-8 BOM, it's removed from returned string..
* And if given bytes contains UTF-8 BOM, it's removed from returned string.
*/
def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
@@ -47,12 +47,21 @@ object StringUtil {
}
/**
* Extract issue id like ````#issueId``` from the given message.
* Extract issue id like ```#issueId``` from the given message.
*
*@param message the message which may contains issue id
* @return the iterator of issue id
*/
def extractIssueId(message: String): Iterator[String] =
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map { matchData => matchData.group(2) }
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2))
/**
* Extract close issue id like ```close #issueId ``` from the given message.
*
* @param message the message which may contains close command
* @return the iterator of issue id
*/
def extractCloseId(message: String): Iterator[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
}

View File

@@ -0,0 +1,140 @@
@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div>
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span5">
<fieldset>
<label for="groupName" class="strong">Group name</label>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label class="strong">URL (Optional)</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span7">
<fieldset>
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
</div>
</fieldset>
</div>
</div>
<fieldset class="margin">
@if(account.isDefined){
<div class="pull-right">
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
</div>
}
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
@if(account.isDefined){
<a href="@url(account.get.userName)" class="btn">Cancel</a>
}
</fieldset>
</form>
</div>
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#member-list 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'){
addMemberHTML(userName, false);
} else {
$('#error-memberName').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
$(this).parent().remove();
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
return !(e.keyCode == 13);
});
$('#delete').click(function(){
return confirm('Once you delete this group, there is no going back.\nAre you sure?');
});
@members.map { member =>
addMemberHTML('@member.userName', @member.isManager);
}
function addMemberHTML(userName, isManager){
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
if(!isManager){
memberButton.addClass('active');
}
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
if(isManager){
managerButton.addClass('active');
}
$('#member-list').append($('<li>')
.data('name', userName)
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
.append(memberButton)
.append(managerButton))
.append(' ')
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateMembers(){
var members = $('#member-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $('button.active').filter(function(i, e){
return $(e).data('name') == userName;
}).attr('value');
}).get().join(',');
$('#members').val(members);
}
});
</script>

View File

@@ -1,4 +1,5 @@
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], active: String,
isGroupManager: Boolean = false)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
@@ -41,6 +42,13 @@
</div>
</li>
}
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_editgroup" class="btn">Edit Group</a>
</div>
</li>
}
</ul>
@body
</div>

View File

@@ -1,7 +1,7 @@
@(account: model.Account, members: List[String])(implicit context: app.Context)
@(account: model.Account, members: List[String], isGroupManager: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, Nil, "members"){
@main(account, Nil, "members", isGroupManager){
@if(members.isEmpty){
No members
} else {

View File

@@ -1,7 +1,7 @@
@(groupNames: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main("Create a New Repository"){
@html.main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>

View File

@@ -1,7 +1,9 @@
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@(account: model.Account, groupNames: List[String],
repositories: List[service.RepositoryService.RepositoryInfo],
isGroupManager: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, groupNames, "repositories"){
@main(account, groupNames, "repositories", isGroupManager){
@if(repositories.isEmpty){
No repositories
} else {

View File

@@ -1,11 +1,11 @@
@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
@(account: Option[model.Account], members: List[model.GroupMember])(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">
<div class="span5">
<fieldset>
<label for="groupName" class="strong">Group name</label>
<div>
@@ -24,29 +24,23 @@
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
<input type="text" name="url" id="url" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span5">
<div class="span7">
<fieldset>
<label class="strong">Members</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 id="member-list" class="collaborator">
</ul>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-memberName"></span>
<span class="error" id="error-members"></span>
</div>
</fieldset>
</div>
@@ -60,6 +54,10 @@
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
@@ -70,7 +68,7 @@ $(function(){
}
// check duplication
var exists = $('#members li').filter(function(){
var exists = $('#member-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
@@ -83,19 +81,7 @@ $(function(){
'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);
addMemberHTML(userName, false);
} else {
$('#error-memberName').text('User does not exist.');
}
@@ -103,20 +89,47 @@ $(function(){
});
$(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);
});
@members.map { member =>
addMemberHTML('@member.userName', @member.isManager);
}
function addMemberHTML(userName, isManager){
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
if(!isManager){
memberButton.addClass('active');
}
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
if(isManager){
managerButton.addClass('active');
}
$('#member-list').append($('<li>')
.data('name', userName)
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
.append(memberButton)
.append(managerButton))
.append(' ')
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateMembers(){
var members = $('#member-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $('button.active').filter(function(i, e){
return $(e).data('name') == userName;
}).attr('value');
}).get().join(',');
$('#members').val(members);
}
});
</script>

View File

@@ -30,6 +30,9 @@
}
}
</div>
@repository.repository.description.map { description =>
<p>@description</p>
}
<table class="global-nav box-header">
<tr>
<th class="box-header@if(active=="code"){ active}">
@@ -53,7 +56,7 @@
<th class="box-header@if(active=="network"){ active}">
<a href="@url(repository)/network/members">Network</a>
</th>
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
<th class="box-header@if(active=="settings"){ active}">
<a href="@url(repository)/settings">Settings</a>
</th>

View File

@@ -54,14 +54,18 @@
}
@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 class="dropdown-toggle menu" data-toggle="dropdown" href="#"><i class="icon-plus"></i><span class="caret" style="vertical-align: middle;"></span></a>
<ul class="dropdown-menu">
<li><a href="@path/new">New repository</a></li>
<li><a href="@path/groups/new">New group</a></li>
</ul>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@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" class="btn btn-last" id="signin">Sign in</a>
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
}
</div><!--/.nav-collapse -->
</div>
@@ -76,7 +80,6 @@
$('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != '';
});
$('#signin').attr('href', '@path/signin?redirect=' + encodeURIComponent(location.pathname + location.search + location.hash));
});
</script>
</body>

View File

@@ -1,4 +1,4 @@
@(branchInfo: List[(String, java.util.Date)],
@(branchInfo: Seq[(String, java.util.Date)],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._

View File

@@ -3,7 +3,7 @@
pathList: List[String],
latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo],
readme: Option[String])(implicit context: app.Context)
readme: Option[(util.JGitUtil.FileInfo, String)])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@@ -12,7 +12,7 @@
<div class="head">
<div class="pull-right">
@defining(repository.commitCount){ commitCount =>
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 10000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
}
</div>
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@@ -55,14 +55,22 @@
<tr>
<td width="16">
@if(file.isDirectory){
<img src="@assets/common/images/folder.png"/>
@if(file.linkUrl.isDefined){
<img src="@assets/common/images/folder_link.png"/>
} else {
<img src="@assets/common/images/folder.png"/>
}
} else {
<img src="@assets/common/images/file.png"/>
}
</td>
<td>
@if(file.isDirectory){
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
@if(file.linkUrl.isDefined){
<a href="@file.linkUrl">@file.name</a>
} else {
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}
} else {
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}
@@ -77,9 +85,9 @@
</table>
</div>
@readme.map { content =>
@readme.map { case(file, content) =>
<div id="readme" class="box">
<div class="box-header">README.md</div>
<div class="box-header">@file.name</div>
<div class="box-content markdown-body">@markdown(content, repository, false, false)</div>
</div>
}

View File

@@ -13,6 +13,10 @@
<a href="@url(collaboratorName)">@collaboratorName</a>
@if(!isGroupRepository){
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
} else {
@if(repository.managers.contains(collaboratorName)){
(Manager)
}
}
</li>
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

@@ -2,8 +2,9 @@ package service
import org.specs2.mutable.Specification
import java.util.Date
import model.GroupMember
class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
class AccountServiceSpec extends Specification with ServiceSpecBase {
"AccountService" should {
val RootMailAddress = "root@localhost"
@@ -63,9 +64,9 @@ class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
AccountService.getGroupMembers(group1) must_== Nil
AccountService.getGroupsByUserName(user1) must_== Nil
AccountService.updateGroupMembers(group1, List(user1))
AccountService.updateGroupMembers(group1, List((user1, true)))
AccountService.getGroupMembers(group1) must_== List(user1)
AccountService.getGroupMembers(group1) must_== List(GroupMember(group1, user1, true))
AccountService.getGroupsByUserName(user1) must_== List(group1)
AccountService.updateGroupMembers(group1, Nil)

View File

@@ -35,4 +35,22 @@ class StringUtilSpec extends Specification {
StringUtil.sha1("abc") mustEqual "a9993e364706816aba3e25717850c26c9cd0d89d"
}
}
"extractIssueId" should {
"extract '#xxx' and return extracted id" in {
StringUtil.extractIssueId("(refs #123)").toSeq mustEqual Seq("123")
}
"returns Nil from message which does not contain #xxx" in {
StringUtil.extractIssueId("this is test!").toSeq mustEqual Nil
}
}
"extractCloseId" should {
"extract 'close #xxx' and return extracted id" in {
StringUtil.extractCloseId("(close #123)").toSeq mustEqual Seq("123")
}
"returns Nil from message which does not contain close command" in {
StringUtil.extractCloseId("(refs #123)").toSeq mustEqual Nil
}
}
}