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 - File editing in repository viewer
- Comment for the changeset - Comment for the changeset
- Network graph - Network graph
- Statics - Statistics
- Watch / Star - 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). 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] - --port=[NUMBER]
- --prefix=[CONTEXTPATH] - --prefix=[CONTEXTPATH]
- --host=[HOSTNAME] - --host=[HOSTNAME]
- --https=true
- --gitbucket.home=[DATA_DIR] - --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. 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 Release Notes
-------- --------
### 1.11 - End of Feb 2014 ### 1.11.1 - 06 Mar 2014
- Base URL for redirect, notification and repository URL box is configurable - 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 - Headline anchor is available for Markdown contents such as Wiki page
- Improve H2 connectivity - Improve H2 connectivity
- Label is available for pull requests not only issues - Label is available for pull requests not only issues

View File

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

View File

@@ -39,9 +39,6 @@ start() {
if [ $GITBUCKET_HOST ]; then if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}" START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi fi
if [ $GITBUCKET_HTTPS ]; then
START_OPTS="${START_OPTS} --https=true"
fi
# Run the Java process # Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 & 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 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]); port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")) { } else if(dim[0].equals("--prefix")) {
contextPath = dim[1]; 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")){ } else if(dim[0].equals("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]); System.setProperty("gitbucket.home", dim[1]);
} }
@@ -36,7 +34,7 @@ public class JettyLauncher {
Server server = new Server(); Server server = new Server();
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps); SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) { if(host != null) {
connector.setHost(host); connector.setHost(host);
} }
@@ -62,19 +60,3 @@ public class JettyLauncher {
server.join(); 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 DashboardController, "/*")
context.mount(new UserManagementController, "/*") context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*") context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*")
context.mount(new AccountController, "/*") context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*") context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*") context.mount(new WikiController, "/*")

View File

@@ -1,18 +1,25 @@
package app package app
import service._ import service._
import util.{FileUtil, OneselfAuthenticator} import util._
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
import org.apache.commons.io.FileUtils 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 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 { trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator => 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, case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String]) url: Option[String], fileId: Option[String])
@@ -38,6 +45,40 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
"clearImage" -> trim(label("Clear image" , boolean())) "clearImage" -> trim(label("Clear image" , boolean()))
)(AccountEditForm.apply) )(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. * Displays user information.
*/ */
@@ -52,14 +93,20 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
getActivitiesByUser(userName, true)) getActivitiesByUser(userName, true))
// Members // Members
case "members" if(account.isGroupAccount) => case "members" if(account.isGroupAccount) => {
_root_.account.html.members(account, getGroupMembers(account.userName)) 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 // Repositories
case _ => case _ => {
val members = getGroupMembers(account.userName)
_root_.account.html.repositories(account, _root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName), 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 } getOrElse NotFound
} }
@@ -135,4 +182,228 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
} else NotFound } 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. * Provides generic features for controller implementations.
*/ */
abstract class ControllerBase extends ScalatraFilter 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 implicit val jsonFormats = DefaultFormats
// Don't set content type via Accept header. // Don't set content type via Accept header.
override def format(implicit request: HttpServletRequest) = "" 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 httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse] val httpResponse = response.asInstanceOf[HttpServletResponse]
val context = request.getServletContext.getContextPath val context = request.getServletContext.getContextPath
@@ -37,12 +38,15 @@ abstract class ControllerBase extends ScalatraFilter
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
if(account == null){ if(account == null){
// Redirect to login form // Redirect to login form
// TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path)) httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
} else if(account.isAdmin){ } else if(account.isAdmin){
// H2 Console (administrators only) // H2 Console (administrators only)
// TODO Should use the configured base url.
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
// Redirect to dashboard // Redirect to dashboard
// TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/") httpResponse.sendRedirect(context + "/")
} }
} else if(path.startsWith("/git/")){ } else if(path.startsWith("/git/")){
@@ -52,12 +56,25 @@ abstract class ControllerBase extends ScalatraFilter
// Scalatra actions // Scalatra actions
super.doFilter(request, response, chain) super.doFilter(request, response, chain)
} }
} finally {
contextCache.remove();
} }
private val contextCache = new java.lang.ThreadLocal[Context]()
/** /**
* Returns the context object for the request. * 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) 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"){ if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin")) org.scalatra.Unauthorized(redirect("/signin"))
} else { } else {
val currentUrl = baseUrl + defining(request.getQueryString){ queryString => org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "") 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")) )))
} }
} }
} }
protected def baseUrl = loadSystemSettings().baseUrl.getOrElse { override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
defining(request.getRequestURL.toString){ url => includeContextPath: Boolean = true, includeServletPath: Boolean = true)
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) (implicit request: HttpServletRequest, response: HttpServletResponse) =
} if (path.startsWith("http")) path
}.replaceFirst("/$", "") else baseUrl + url(path, params, false, false, false)
} }
/** /**
* Context object for the current request. * Context object for the current request.
*
* @param path the context path
*/ */
case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){ case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length)
/** /**
* Get object from cache. * 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. * 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. * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/ */
class FileUploadController extends ScalatraServlet class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))

View File

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

View File

@@ -79,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq( pulls.html.pullreq(
issue, pullreq, issue, pullreq,
getComments(owner, name, issueId), 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, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
@@ -105,9 +105,9 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository => get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId => params("id").toIntOpt.map { issueId =>
val branchName = params("branchName") val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){ if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => 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 // call web hook
getWebHookURLs(owner, name) match { getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>
@@ -216,16 +228,16 @@ trait PullRequestsControllerBase extends ControllerBase {
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2 val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).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 } getOrElse NotFound
} }
case _ => { case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git => using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) => 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 { } 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 util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import service.WebHookService.WebHookPayload import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
@@ -16,7 +15,7 @@ class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport { trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WebHookService self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator => with OwnerAuthenticator with UsersAuthenticator =>

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.Directory._ import util.Directory._
@@ -23,10 +24,10 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], 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], 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( val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
@@ -51,28 +52,28 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text()))) "members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply) )(NewGroupForm.apply)
val editGroupForm = mapping( val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text()))), "members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean())), "clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean())) "removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply) )(EditGroupForm.apply)
get("/admin/users")(adminOnly { get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved) val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
val members = users.collect { case account if(account.isGroupAccount) => account.userName -> getGroupMembers(account.userName).map(_.userName)
account.userName -> getGroupMembers(account.userName)
}.toMap }.toMap
admin.users.html.list(users, members, includeRemoved) admin.users.html.list(users, members, includeRemoved)
}) })
@@ -127,7 +128,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url) 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) updateImage(form.groupName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
@@ -139,7 +144,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}) })
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => 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 => getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved) updateGroup(groupName, form.url, form.isRemoved)
@@ -155,11 +164,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
} else { } else {
// Update GROUP_MEMBER // Update GROUP_MEMBER
updateGroupMembers(form.groupName, memberNames) updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories // Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName) removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName => members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName) 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 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 util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import scala.Some import scala.Some
import java.util.ResourceBundle import java.util.ResourceBundle
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase with FlashMapSupport { trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with ActivityService self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) 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") { object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
def groupName = column[String]("GROUP_NAME", O PrimaryKey) def groupName = column[String]("GROUP_NAME", O PrimaryKey)
def userName = column[String]("USER_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( case class GroupMember(
groupName: String, 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 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] = 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] = def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){ if(includeRemoved){
@@ -122,18 +122,17 @@ trait AccountService {
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit = 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) 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 Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { userName => members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName) GroupMembers insert GroupMember (groupName, userName, isManager)
} }
} }
def getGroupMembers(groupName: String): List[String] = def getGroupMembers(groupName: String): List[GroupMember] =
Query(GroupMembers) Query(GroupMembers)
.filter(_.groupName is groupName.bind) .filter(_.groupName is groupName.bind)
.sortBy(_.userName) .sortBy(_.userName)
.map(_.userName)
.list .list
def getGroupsByUserName(userName: String): List[String] = def getGroupsByUserName(userName: String): List[String] =

View File

@@ -119,16 +119,10 @@ trait IssuesService {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser, onlyPullRequest) searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .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) } .sortBy { case (t1, t2) =>
.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, _,_,_) =>
(condition.sort match { (condition.sort match {
case "created" => t1.registeredDate case "created" => t1.registeredDate
case "comments" => commentCount case "comments" => t2.commentCount
case "updated" => t1.updatedDate case "updated" => t1.updatedDate
}) match { }) match {
case sort => condition.direction match { case sort => condition.direction match {
@@ -138,6 +132,11 @@ trait IssuesService {
} }
} }
.drop(offset).take(limit) .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 .list
.splitWith { (c1, c2) => .splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName && c1._1.userName == c2._1.userName &&
@@ -314,6 +313,14 @@ trait IssuesService {
}.toList }.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 { object IssuesService {

View File

@@ -63,8 +63,8 @@ RepositorySearchService { self: IssuesService =>
val list = new ListBuffer[(String, String)] val list = new ListBuffer[(String, String)]
while (treeWalk.next()) { while (treeWalk.next()) {
if(treeWalk.getFileMode(0) != FileMode.TREE){ if(treeWalk.getFileMode(0) == FileMode.REGULAR_FILE){
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes => JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){ if(FileUtil.isText(bytes)){
val text = StringUtil.convertFromByteArray(bytes) val text = StringUtil.convertFromByteArray(bytes)
val lowerText = text.toLowerCase val lowerText = text.toLowerCase

View File

@@ -147,7 +147,8 @@ trait RepositoryService { self: AccountService =>
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
)) ),
getRepositoryManagers(repository.userName))
} }
} }
@@ -162,7 +163,8 @@ trait RepositoryService { self: AccountService =>
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
)) ),
getRepositoryManagers(repository.userName))
} }
} }
@@ -195,10 +197,18 @@ trait RepositoryService { self: AccountService =>
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) 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. * 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, case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, 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. * Creates instance with issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) = 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) 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. * Creates instance without issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) = 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) 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]) case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])

View File

@@ -3,9 +3,16 @@ package service
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
trait SystemSettingsService { 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 = { def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props => defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(props.setProperty(BaseURL, _)) settings.baseUrl.foreach(props.setProperty(BaseURL, _))

View File

@@ -234,7 +234,7 @@ trait WikiService {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else { } else {
created = false 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, def deleteWikiPage(owner: String, repository: String, pageName: String,
committer: String, mailAddress: String, message: String): Unit = { committer: String, mailAddress: String, message: String): Unit = {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder() val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter() val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false var removed = false
using(new RevWalk(git.getRepository)){ revWalk => using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk => using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId)) val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true) treeWalk.setRecursive(true)
while(treeWalk.next){ while(treeWalk.next){
val path = treeWalk.getPathString val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path != pageName + ".md"){ if(path != pageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else { } else {
removed = true removed = true
}
} }
} }
}
if(removed){ if(removed){
builder.finish() builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) 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. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
Version(1, 12),
Version(1, 11), Version(1, 11),
Version(1, 10), Version(1, 10),
Version(1, 9), Version(1, 9),

View File

@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig import javax.servlet.ServletConfig
import javax.servlet.ServletContext import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import util.{StringUtil, Keys, JGitUtil, Directory} import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._ import util.Implicits._
@@ -23,7 +23,7 @@ import util.JGitUtil.CommitInfo
* This servlet provides only Git repository functionality. * This servlet provides only Git repository functionality.
* Authentication is provided by [[servlet.BasicAuthenticationFilter]]. * Authentication is provided by [[servlet.BasicAuthenticationFilter]].
*/ */
class GitRepositoryServlet extends GitServlet { class GitRepositoryServlet extends GitServlet with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet]) private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
@@ -48,9 +48,20 @@ class GitRepositoryServlet extends GitServlet {
super.init(config) 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]) private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
@@ -64,13 +75,11 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "") 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("repository:" + owner + "/" + repository)
logger.debug("baseURL:" + baseURL)
if(!repository.endsWith(".wiki")){ if(!repository.endsWith(".wiki")){
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL)) receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
} }
receivePack receivePack
} }
@@ -79,7 +88,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ 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 { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) 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 // call web hook
getWebHookURLs(owner, repository) match { getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher); for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner); ownerAccount <- getAccountByUserName(owner);
repositoryInfo <- getRepository(owner, repository, baseURL)){ repositoryInfo <- getRepository(owner, repository, baseUrl)){
callWebHook(owner, repository, webHookURLs, callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)) 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) = private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => 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 => using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
git.fetch git.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString) .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)

View File

@@ -29,7 +29,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
/** /**
* Allows only the repository owner and administrators. * 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(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } 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 { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => 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() case _ => Unauthorized()
} }
} getOrElse NotFound() } 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 => trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } 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.api.Git
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk import org.eclipse.jgit.treewalk.TreeWalk
import org.eclipse.jgit.transport.RefSpec import scala.util.control.Exception._
import scala.language.reflectiveCalls import scala.language.reflectiveCalls
/** /**
@@ -16,10 +16,8 @@ object ControlUtil {
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B = def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally { try f(resource) finally {
if(resource != null){ if(resource != null){
try { ignoring(classOf[Throwable]) {
resource.close() resource.close()
} catch {
case e: Throwable => // ignore
} }
} }
} }

View File

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

View File

@@ -11,17 +11,20 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType 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 java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
/** /**
* Provides complex JGit operations. * Provides complex JGit operations.
*/ */
object JGitUtil { object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
/** /**
* The repository data. * The repository data.
* *
@@ -45,9 +48,10 @@ object JGitUtil {
* @param commitId the last commit id * @param commitId the last commit id
* @param committer the last committer name * @param committer the last committer name
* @param mailAddress the committer's mail address * @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, 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. * The commit data.
@@ -104,6 +108,15 @@ object JGitUtil {
*/ */
case class TagInfo(name: String, time: Date, id: String) 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. * Returns RevCommit from the commit or tag id.
* *
@@ -128,7 +141,7 @@ object JGitUtil {
using(Git.open(getRepositoryDir(owner, repository))){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // 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( RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -162,7 +175,7 @@ object JGitUtil {
* @return HTML of the file list * @return HTML of the file list
*/ */
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { 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 => using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision) val objectId = git.getRepository.resolve(revision)
@@ -195,22 +208,28 @@ object JGitUtil {
}) })
} }
while (treeWalk.next()) { 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) 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( FileInfo(
objectId, objectId,
fileMode == FileMode.TREE, fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name, name,
commits(path).getCommitterIdent.getWhen, commits(path).getCommitterIdent.getWhen,
commits(path).getShortMessage, commits(path).getShortMessage,
commits(path).getName, commits(path).getName,
commits(path).getCommitterIdent.getName, commits(path).getCommitterIdent.getName,
commits(path).getCommitterIdent.getEmailAddress) commits(path).getCommitterIdent.getEmailAddress,
linkUrl)
}.sortWith { (file1, file2) => }.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match { (file1.isDirectory, file2.isDirectory) match {
case (true , false) => true case (true , false) => true
@@ -325,27 +344,6 @@ object JGitUtil {
}.toMap }.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. * 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) DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
} else { } else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, 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) (buffer.toList, None)
@@ -400,8 +398,8 @@ object JGitUtil {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else { } else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), JGitUtil.getContentFromId(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.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
} }
}.toList }.toList
} }
@@ -494,4 +492,73 @@ object JGitUtil {
newHeadId.getName 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. * Session key for the logged in account information.
*/ */
val LoginAccount = "LOGIN_ACCOUNT" val LoginAccount = "loginAccount"
/**
* Session key for the redirect URL.
*/
val Redirect = "REDIRECT"
/** /**
* Session key for the issue search condition in dashboard. * 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. * 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]]. * 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 = def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content)) 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 *@param message the message which may contains issue id
* @return the iterator of issue id * @return the iterator of issue id
*/ */
def extractIssueId(message: String): Iterator[String] = 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 context._
@import view.helpers._ @import view.helpers._
@html.main(account.userName){ @html.main(account.userName){
@@ -41,6 +42,13 @@
</div> </div>
</li> </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> </ul>
@body @body
</div> </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 context._
@import view.helpers._ @import view.helpers._
@main(account, Nil, "members"){ @main(account, Nil, "members", isGroupManager){
@if(members.isEmpty){ @if(members.isEmpty){
No members No members
} else { } else {

View File

@@ -1,7 +1,7 @@
@(groupNames: List[String])(implicit context: app.Context) @(groupNames: List[String])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@main("Create a New Repository"){ @html.main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;"> <div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true"> <form id="form" method="post" action="@path/new" validate="true">
<fieldset> <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 context._
@import view.helpers._ @import view.helpers._
@main(account, groupNames, "repositories"){ @main(account, groupNames, "repositories", isGroupManager){
@if(repositories.isEmpty){ @if(repositories.isEmpty){
No repositories No repositories
} else { } 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 context._
@import view.helpers._ @import view.helpers._
@html.main(if(account.isEmpty) "New Group" else "Update Group"){ @html.main(if(account.isEmpty) "New Group" else "Update Group"){
@admin.html.menu("users"){ @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"> <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="row-fluid">
<div class="span7"> <div class="span5">
<fieldset> <fieldset>
<label for="groupName" class="strong">Group name</label> <label for="groupName" class="strong">Group name</label>
<div> <div>
@@ -24,29 +24,23 @@
<div> <div>
<span id="error-url" class="error"></span> <span id="error-url" class="error"></span>
</div> </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>
<fieldset> <fieldset>
<label for="avatar" class="strong">Image (Optional)</label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @helper.html.uploadavatar(account)
</fieldset> </fieldset>
</div> </div>
<div class="span5"> <div class="span7">
<fieldset> <fieldset>
<label class="strong">Members</label> <label class="strong">Members</label>
<ul id="members" class="collaborator"> <ul id="member-list" class="collaborator">
@members.map { userName =>
<li data-name="@userName">
<a href="@path/@url(userName)">@userName</a>
<a href="#" class="remove">(remove)</a>
</li>
}
</ul> </ul>
@helper.html.account("memberName", 200) @helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/> <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> <div>
<span class="error" id="error-memberName"></span> <span class="error" id="error-members"></span>
</div> </div>
</fieldset> </fieldset>
</div> </div>
@@ -60,6 +54,10 @@
} }
<script> <script>
$(function(){ $(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){ $('#addMember').click(function(){
$('#error-memberName').text(''); $('#error-memberName').text('');
var userName = $('#memberName').val(); var userName = $('#memberName').val();
@@ -70,7 +68,7 @@ $(function(){
} }
// check duplication // check duplication
var exists = $('#members li').filter(function(){ var exists = $('#member-list li').filter(function(){
return $(this).data('name') == userName; return $(this).data('name') == userName;
}).length > 0; }).length > 0;
if(exists){ if(exists){
@@ -83,19 +81,7 @@ $(function(){
'userName': userName 'userName': userName
}, function(data, status){ }, function(data, status){
if(data == 'true'){ if(data == 'true'){
// add member addMemberHTML(userName, false);
$('#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 { } else {
$('#error-memberName').text('User does not exist.'); $('#error-memberName').text('User does not exist.');
} }
@@ -103,20 +89,47 @@ $(function(){
}); });
$(document).on('click', '.remove', function(){ $(document).on('click', '.remove', function(){
// remove member
$(this).parent().remove(); $(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 // Don't submit form by ENTER key
$('#memberName').keypress(function(e){ $('#memberName').keypress(function(e){
console.log(e.keyCode);
return !(e.keyCode == 13); 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> </script>

View File

@@ -30,6 +30,9 @@
} }
} }
</div> </div>
@repository.repository.description.map { description =>
<p>@description</p>
}
<table class="global-nav box-header"> <table class="global-nav box-header">
<tr> <tr>
<th class="box-header@if(active=="code"){ active}"> <th class="box-header@if(active=="code"){ active}">
@@ -53,7 +56,7 @@
<th class="box-header@if(active=="network"){ active}"> <th class="box-header@if(active=="network"){ active}">
<a href="@url(repository)/network/members">Network</a> <a href="@url(repository)/network/members">Network</a>
</th> </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}"> <th class="box-header@if(active=="settings"){ active}">
<a href="@url(repository)/settings">Settings</a> <a href="@url(repository)/settings">Settings</a>
</th> </th>

View File

@@ -54,14 +54,18 @@
} }
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a> <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> <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){ @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/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> <a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else { } 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><!--/.nav-collapse -->
</div> </div>
@@ -76,7 +80,6 @@
$('#search').submit(function(){ $('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != ''; return $.trim($(this).find('input[name=query]').val()) != '';
}); });
$('#signin').attr('href', '@path/signin?redirect=' + encodeURIComponent(location.pathname + location.search + location.hash));
}); });
</script> </script>
</body> </body>

View File

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

View File

@@ -3,7 +3,7 @@
pathList: List[String], pathList: List[String],
latestCommit: util.JGitUtil.CommitInfo, latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo], 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 context._
@import view.helpers._ @import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@@ -12,7 +12,7 @@
<div class="head"> <div class="head">
<div class="pull-right"> <div class="pull-right">
@defining(repository.commitCount){ commitCount => @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> </div>
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> / <a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@@ -55,14 +55,22 @@
<tr> <tr>
<td width="16"> <td width="16">
@if(file.isDirectory){ @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 { } else {
<img src="@assets/common/images/file.png"/> <img src="@assets/common/images/file.png"/>
} }
</td> </td>
<td> <td>
@if(file.isDirectory){ @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 { } else {
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a> <a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
} }
@@ -77,9 +85,9 @@
</table> </table>
</div> </div>
@readme.map { content => @readme.map { case(file, content) =>
<div id="readme" class="box"> <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 class="box-content markdown-body">@markdown(content, repository, false, false)</div>
</div> </div>
} }

View File

@@ -13,6 +13,10 @@
<a href="@url(collaboratorName)">@collaboratorName</a> <a href="@url(collaboratorName)">@collaboratorName</a>
@if(!isGroupRepository){ @if(!isGroupRepository){
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a> <a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
} else {
@if(repository.managers.contains(collaboratorName)){
(Manager)
}
} }
</li> </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 org.specs2.mutable.Specification
import java.util.Date import java.util.Date
import model.GroupMember
class AccountServiceServiceSpec extends Specification with ServiceSpecBase { class AccountServiceSpec extends Specification with ServiceSpecBase {
"AccountService" should { "AccountService" should {
val RootMailAddress = "root@localhost" val RootMailAddress = "root@localhost"
@@ -63,9 +64,9 @@ class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
AccountService.getGroupMembers(group1) must_== Nil AccountService.getGroupMembers(group1) must_== Nil
AccountService.getGroupsByUserName(user1) 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.getGroupsByUserName(user1) must_== List(group1)
AccountService.updateGroupMembers(group1, Nil) AccountService.updateGroupMembers(group1, Nil)

View File

@@ -35,4 +35,22 @@ class StringUtilSpec extends Specification {
StringUtil.sha1("abc") mustEqual "a9993e364706816aba3e25717850c26c9cd0d89d" 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
}
}
} }