mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-09 15:05:50 +01:00
Merge branch 'master' into ssh-access
Conflicts: src/main/scala/servlet/GitRepositoryServlet.scala
This commit is contained in:
11
README.md
11
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 &
|
||||||
|
|||||||
2
sbt.bat
2
sbt.bat
@@ -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
2
sbt.sh
@@ -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 "$@"
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
1
src/main/resources/update/1_12.sql
Normal file
1
src/main/resources/update/1_12.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE;
|
||||||
@@ -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, "/*")
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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)))
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
@@ -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] =
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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, _))
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|
||||||
@@ -47,13 +47,24 @@ 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])
|
||||||
|
|
||||||
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
||||||
val receivePack = new ReceivePack(db)
|
val receivePack = new ReceivePack(db)
|
||||||
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
140
src/main/twirl/account/group.scala.html
Normal file
140
src/main/twirl/account/group.scala.html
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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._
|
||||||
|
|||||||
@@ -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>
|
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 10000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>
|
||||||
}
|
}
|
||||||
</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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/main/webapp/assets/common/images/folder_link.png
Normal file
BIN
src/main/webapp/assets/common/images/folder_link.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 785 B |
@@ -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)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user