diff --git a/README.md b/README.md index fa6718d..07314d8 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ - File editing in repository viewer - Comment for the changeset - Network graph -- Statics +- Statistics - Watch / Star If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki). @@ -42,7 +42,6 @@ - --port=[NUMBER] - --prefix=[CONTEXTPATH] - --host=[HOSTNAME] -- --https=true - --gitbucket.home=[DATA_DIR] To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk. @@ -59,8 +58,12 @@ Release Notes -------- -### 1.11 - End of Feb 2014 -- Base URL for redirect, notification and repository URL box is configurable +### 1.11.1 - 06 Mar 2014 +- Bug fix + +### 1.11 - 01 Mar 2014 +- Base URL for redirection, notification and repository URL box is configurable +- Remove ```--https``` option because it's possible to substitute in the base url - Headline anchor is available for Markdown contents such as Wiki page - Improve H2 connectivity - Label is available for pull requests not only issues diff --git a/contrib/redhat/gitbucket.conf b/contrib/redhat/gitbucket.conf index c3959f3..103778e 100644 --- a/contrib/redhat/gitbucket.conf +++ b/contrib/redhat/gitbucket.conf @@ -4,9 +4,6 @@ # Server port #GITBUCKET_PORT=8080 -# Force HTTPS scheme -#GITBUCKET_HTTPS=false - # Data directory (GITBUCKET_HOME/gitbucket) #GITBUCKET_HOME=/var/lib/gitbucket diff --git a/contrib/redhat/gitbucket.init b/contrib/redhat/gitbucket.init index 3aed802..43e29e3 100644 --- a/contrib/redhat/gitbucket.init +++ b/contrib/redhat/gitbucket.init @@ -39,9 +39,6 @@ if [ $GITBUCKET_HOST ]; then START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}" fi - if [ $GITBUCKET_HTTPS ]; then - START_OPTS="${START_OPTS} --https=true" - fi # Run the Java process GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 & diff --git a/sbt.bat b/sbt.bat index d86d1e0..41a5c11 100644 --- a/sbt.bat +++ b/sbt.bat @@ -1,2 +1,2 @@ set SCRIPT_DIR=%~dp0 -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %* +java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %* diff --git a/sbt.sh b/sbt.sh index 23c721f..cd2266f 100755 --- a/sbt.sh +++ b/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 "$@" diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java index d7b137d..76500f0 100644 --- a/src/main/java/JettyLauncher.java +++ b/src/main/java/JettyLauncher.java @@ -25,8 +25,6 @@ port = Integer.parseInt(dim[1]); } else if(dim[0].equals("--prefix")) { contextPath = dim[1]; - } else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) { - forceHttps = true; } else if(dim[0].equals("--gitbucket.home")){ System.setProperty("gitbucket.home", dim[1]); } @@ -36,7 +34,7 @@ Server server = new Server(); - HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps); + SelectChannelConnector connector = new SelectChannelConnector(); if(host != null) { connector.setHost(host); } @@ -62,19 +60,3 @@ 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); - } - } -} diff --git a/src/main/resources/update/1_12.sql b/src/main/resources/update/1_12.sql new file mode 100644 index 0000000..0030169 --- /dev/null +++ b/src/main/resources/update/1_12.sql @@ -0,0 +1 @@ +ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index de3c40d..21c213d 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -20,7 +20,6 @@ context.mount(new DashboardController, "/*") context.mount(new UserManagementController, "/*") context.mount(new SystemSettingsController, "/*") - context.mount(new CreateRepositoryController, "/*") context.mount(new AccountController, "/*") context.mount(new RepositoryViewerController, "/*") context.mount(new WikiController, "/*") diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index 5bea0b4..d35379b 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -1,18 +1,25 @@ package app import service._ -import util.{FileUtil, OneselfAuthenticator} +import util._ import util.StringUtil._ import util.Directory._ +import util.ControlUtil._ import jp.sf.amateras.scalatra.forms._ -import org.scalatra.FlashMapSupport import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.{FileMode, Constants} +import org.eclipse.jgit.dircache.DirCache +import model.GroupMember class AccountController extends AccountControllerBase - with AccountService with RepositoryService with ActivityService with OneselfAuthenticator + with AccountService with RepositoryService with ActivityService with WikiService with LabelsService + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator -trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport { - self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator => +trait AccountControllerBase extends AccountManagementControllerBase { + self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, url: Option[String], fileId: Option[String]) @@ -38,6 +45,40 @@ "clearImage" -> trim(label("Clear image" , boolean())) )(AccountEditForm.apply) + case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String) + case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) + + val newGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) + )(NewGroupForm.apply) + + val editGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())) + )(EditGroupForm.apply) + + case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) + case class ForkRepositoryForm(owner: String, name: String) + + val newRepositoryForm = mapping( + "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))), + "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))), + "description" -> trim(label("Description" , optional(text()))), + "isPrivate" -> trim(label("Repository Type", boolean())), + "createReadme" -> trim(label("Create README" , boolean())) + )(RepositoryCreationForm.apply) + + val forkRepositoryForm = mapping( + "owner" -> trim(label("Repository owner", text(required))), + "name" -> trim(label("Repository name", text(required))) + )(ForkRepositoryForm.apply) + /** * Displays user information. */ @@ -52,14 +93,20 @@ getActivitiesByUser(userName, true)) // Members - case "members" if(account.isGroupAccount) => - _root_.account.html.members(account, getGroupMembers(account.userName)) + case "members" if(account.isGroupAccount) => { + val members = getGroupMembers(account.userName) + _root_.account.html.members(account, members.map(_.userName), + context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) + } // Repositories - case _ => + case _ => { + val members = getGroupMembers(account.userName) _root_.account.html.repositories(account, if(account.isGroupAccount) Nil else getGroupsByUserName(userName), - getVisibleRepositories(context.loginAccount, baseUrl, Some(userName))) + getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)), + context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) + } } } getOrElse NotFound } @@ -135,4 +182,228 @@ } 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.") + } + } } diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala index bdea042..4168a8d 100644 --- a/src/main/scala/app/ControllerBase.scala +++ b/src/main/scala/app/ControllerBase.scala @@ -20,14 +20,15 @@ * Provides generic features for controller implementations. */ abstract class ControllerBase extends ScalatraFilter - with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations with SystemSettingsService { + with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations + with SystemSettingsService { implicit val jsonFormats = DefaultFormats // Don't set content type via Accept header. override def format(implicit request: HttpServletRequest) = "" - override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try { val httpRequest = request.asInstanceOf[HttpServletRequest] val httpResponse = response.asInstanceOf[HttpServletResponse] val context = request.getServletContext.getContextPath @@ -37,12 +38,15 @@ val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] if(account == null){ // Redirect to login form + // TODO Should use the configured base url. httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path)) } else if(account.isAdmin){ // H2 Console (administrators only) + // TODO Should use the configured base url. chain.doFilter(request, response) } else { // Redirect to dashboard + // TODO Should use the configured base url. httpResponse.sendRedirect(context + "/") } } else if(path.startsWith("/git/")){ @@ -52,12 +56,25 @@ // Scalatra actions super.doFilter(request, response, chain) } + } finally { + contextCache.remove(); } + private val contextCache = new java.lang.ThreadLocal[Context]() + /** * Returns the context object for the request. */ - implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request) + implicit def context: Context = { + contextCache.get match { + case null => { + val context = Context(loadSystemSettings().baseUrl.getOrElse(servletContext.getContextPath), LoginAccount, request) + contextCache.set(context) + context + } + case context => context + } + } private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount) @@ -102,28 +119,32 @@ if(request.getMethod.toUpperCase == "POST"){ org.scalatra.Unauthorized(redirect("/signin")) } else { - val currentUrl = baseUrl + defining(request.getQueryString){ queryString => - request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "") - } - session.setAttribute(Keys.Session.Redirect, currentUrl) - org.scalatra.Unauthorized(redirect("/signin")) + org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode( + defining(request.getQueryString){ queryString => + request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "") + } + ))) } } } - protected def baseUrl = loadSystemSettings().baseUrl.getOrElse { - defining(request.getRequestURL.toString){ url => - url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) - } - }.replaceFirst("/$", "") + override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty, + includeContextPath: Boolean = true, includeServletPath: Boolean = true) + (implicit request: HttpServletRequest, response: HttpServletResponse) = + if (path.startsWith("http")) path + else baseUrl + url(path, params, false, false, false) } /** * Context object for the current request. + * + * @param path the context path */ case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){ + lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length) + /** * Get object from cache. * diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala deleted file mode 100644 index 16cd05c..0000000 --- a/src/main/scala/app/CreateRepositoryController.scala +++ /dev/null @@ -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.") - } - } - -} diff --git a/src/main/scala/app/FileUploadController.scala b/src/main/scala/app/FileUploadController.scala index 6ea5fa2..9950b48 100644 --- a/src/main/scala/app/FileUploadController.scala +++ b/src/main/scala/app/FileUploadController.scala @@ -12,8 +12,7 @@ * This servlet saves uploaded file as temporary file and returns the unique id. * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id. */ -class FileUploadController extends ScalatraServlet - with FileUploadSupport with FlashMapSupport with FileUploadControllerBase { +class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase { configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala index 2ee96bd..0e46b3c 100644 --- a/src/main/scala/app/IndexController.scala +++ b/src/main/scala/app/IndexController.scala @@ -1,7 +1,6 @@ package app import util._ -import util.Implicits._ import service._ import jp.sf.amateras.scalatra.forms._ @@ -31,7 +30,7 @@ get("/signin"){ val redirect = params.get("redirect") if(redirect.isDefined && redirect.get.startsWith("/")){ - session.setAttribute(Keys.Session.Redirect, redirect.get) + flash += Keys.Flash.Redirect -> redirect.get } html.signin(loadSystemSettings()) } @@ -55,7 +54,7 @@ session.setAttribute(Keys.Session.LoginAccount, account) updateLastLoginDate(account.userName) - session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl => + flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){ redirect("/") } else { diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index 1f8453c..47c0614 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -79,7 +79,7 @@ pulls.html.pullreq( issue, pullreq, getComments(owner, name, issueId), - getIssueLabels(owner, name, issueId.toInt), + getIssueLabels(owner, name, issueId), (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, getMilestonesWithIssueCount(owner, name), getLabels(owner, name), @@ -105,9 +105,9 @@ } getOrElse NotFound }) - get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository => + get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository => params("id").toIntOpt.map { issueId => - val branchName = params("branchName") + val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => @@ -183,6 +183,18 @@ } } + // close issue by content of pull request + val defaultBranch = getRepository(owner, name, baseUrl).get.repository.defaultBranch + if(pullreq.branch == defaultBranch){ + commits.flatten.foreach { commit => + closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) + } + issue.content match { + case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name) + case _ => + } + closeIssuesFromMessage(form.message, loginAccount.userName, owner, name) + } // call web hook getWebHookURLs(owner, name) match { case webHookURLs if(webHookURLs.nonEmpty) => @@ -216,16 +228,16 @@ val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2 val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2 - redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") } } getOrElse NotFound } case _ => { using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git => JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) => - redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}") + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}") } getOrElse { - redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}") + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}") } } } diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala index d745623..3f5489a 100644 --- a/src/main/scala/app/RepositorySettingsController.scala +++ b/src/main/scala/app/RepositorySettingsController.scala @@ -5,7 +5,6 @@ import util.{UsersAuthenticator, OwnerAuthenticator} import jp.sf.amateras.scalatra.forms._ import org.apache.commons.io.FileUtils -import org.scalatra.FlashMapSupport import org.scalatra.i18n.Messages import service.WebHookService.WebHookPayload import util.JGitUtil.CommitInfo @@ -16,7 +15,7 @@ with RepositoryService with AccountService with WebHookService with OwnerAuthenticator with UsersAuthenticator -trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport { +trait RepositorySettingsControllerBase extends ControllerBase { self: RepositoryService with AccountService with WebHookService with OwnerAuthenticator with UsersAuthenticator => diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 2726136..d68dc53 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -82,44 +82,45 @@ val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) @scala.annotation.tailrec - def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match { - case true if(walk.getPathString == path) => walk.getObjectId(0) - case true => getPathObjectId(path, walk) + def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { + case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) + case true => getPathObjectId(path, walk) + case false => None } - val objectId = using(new TreeWalk(git.getRepository)){ treeWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => treeWalk.addTree(revCommit.getTree) treeWalk.setRecursive(true) getPathObjectId(path, treeWalk) - } - - if(raw){ - // Download - defining(JGitUtil.getContent(git, objectId, false).get){ bytes => - contentType = FileUtil.getContentType(path, bytes) - bytes - } - } else { - // Viewer - val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) - val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other" - val bytes = if(viewer == "other") JGitUtil.getContent(git, objectId, false) else None - - val content = if(viewer == "other"){ - if(bytes.isDefined && FileUtil.isText(bytes.get)){ - // text - JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray)) - } else { - // binary - JGitUtil.ContentInfo("binary", None) + } map { objectId => + if(raw){ + // Download + defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => + contentType = FileUtil.getContentType(path, bytes) + bytes } } else { - // image or large - JGitUtil.ContentInfo(viewer, None) - } + // Viewer + val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) + val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other" + val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None - repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit)) - } + val content = if(viewer == "other"){ + if(bytes.isDefined && FileUtil.isText(bytes.get)){ + // text + JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray)) + } else { + // binary + JGitUtil.ContentInfo("binary", None) + } + } else { + // image or large + JGitUtil.ContentInfo(viewer, None) + } + + repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit)) + } + } getOrElse NotFound } }) @@ -158,8 +159,8 @@ /** * Deletes branch. */ - get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository => - val branchName = params("branchName") + get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => + val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => @@ -207,7 +208,7 @@ while(walk.next){ val name = walk.getPathString val mode = walk.getFileMode(0) - if(mode != FileMode.TREE){ + if(mode == FileMode.REGULAR_FILE){ walk.getObjectId(objectId, 0) val entry = new ZipEntry(name) val loader = reader.open(objectId) @@ -266,7 +267,7 @@ repo.html.guide(repository) } else { using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) + //val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) // get specified commit JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit => @@ -276,7 +277,8 @@ val readme = files.find { file => readmeFiles.contains(file.name.toLowerCase) }.map { file => - StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) + file -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId( + Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) } repo.html.files(revision, repository, diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala index 08231b6..c6f56fe 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -4,12 +4,11 @@ import SystemSettingsService._ import util.AdminAuthenticator import jp.sf.amateras.scalatra.forms._ -import org.scalatra.FlashMapSupport class SystemSettingsController extends SystemSettingsControllerBase with SystemSettingsService with AccountService with AdminAuthenticator -trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { +trait SystemSettingsControllerBase extends ControllerBase { self: SystemSettingsService with AccountService with AdminAuthenticator => private val form = mapping( diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala index 50ffb3c..dc3543c 100644 --- a/src/main/scala/app/UserManagementController.scala +++ b/src/main/scala/app/UserManagementController.scala @@ -5,6 +5,7 @@ import util.StringUtil._ import util.ControlUtil._ import jp.sf.amateras.scalatra.forms._ +import org.scalatra.i18n.Messages import org.apache.commons.io.FileUtils import util.Directory._ @@ -23,10 +24,10 @@ fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], - memberNames: Option[String]) + members: String) case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], - memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean) + members: String, clearImage: Boolean, isRemoved: Boolean) val newUserForm = mapping( "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), @@ -51,28 +52,28 @@ )(EditUserForm.apply) val newGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "memberNames" -> trim(label("Member Names" ,optional(text()))) + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) )(NewGroupForm.apply) val editGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "memberNames" -> trim(label("Member Names" ,optional(text()))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean())) + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean())) )(EditGroupForm.apply) get("/admin/users")(adminOnly { val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) - val users = getAllUsers(includeRemoved) - - val members = users.collect { case account if(account.isGroupAccount) => - account.userName -> getGroupMembers(account.userName) + val users = getAllUsers(includeRemoved) + val members = users.collect { case account if(account.isGroupAccount) => + account.userName -> getGroupMembers(account.userName).map(_.userName) }.toMap + admin.users.html.list(users, members, includeRemoved) }) @@ -127,7 +128,11 @@ post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => createGroup(form.groupName, form.url) - updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil)) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) updateImage(form.groupName, form.fileId, false) redirect("/admin/users") }) @@ -139,7 +144,11 @@ }) post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => - defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => getAccountByUserName(groupName, true).map { account => updateGroup(groupName, form.url, form.isRemoved) @@ -155,11 +164,11 @@ } } else { // Update GROUP_MEMBER - updateGroupMembers(form.groupName, memberNames) + updateGroupMembers(form.groupName, members) // Update COLLABORATOR for group repositories getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => removeCollaborators(form.groupName, repositoryName) - memberNames.foreach { userName => + members.foreach { case (userName, isManager) => addCollaborator(form.groupName, repositoryName, userName) } } @@ -172,8 +181,17 @@ } }) - post("/admin/users/_usercheck")(adminOnly { + // TODO Move to other generic controller? + post("/admin/users/_usercheck"){ getAccountByUserName(params("userName")).isDefined - }) + } + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } } diff --git a/src/main/scala/app/WikiController.scala b/src/main/scala/app/WikiController.scala index 704ce20..07208a5 100644 --- a/src/main/scala/app/WikiController.scala +++ b/src/main/scala/app/WikiController.scala @@ -6,18 +6,15 @@ import util.ControlUtil._ import jp.sf.amateras.scalatra.forms._ import org.eclipse.jgit.api.Git -import org.scalatra.FlashMapSupport import org.scalatra.i18n.Messages import scala.Some import java.util.ResourceBundle class WikiController extends WikiControllerBase - with WikiService with RepositoryService with AccountService with ActivityService - with CollaboratorsAuthenticator with ReferrerAuthenticator + with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator -trait WikiControllerBase extends ControllerBase with FlashMapSupport { - self: WikiService with RepositoryService with ActivityService - with CollaboratorsAuthenticator with ReferrerAuthenticator => +trait WikiControllerBase extends ControllerBase { + self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator => case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) diff --git a/src/main/scala/model/GroupMembers.scala b/src/main/scala/model/GroupMembers.scala index 0bcd0af..a2a38f3 100644 --- a/src/main/scala/model/GroupMembers.scala +++ b/src/main/scala/model/GroupMembers.scala @@ -5,10 +5,12 @@ object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") { def groupName = column[String]("GROUP_NAME", O PrimaryKey) def userName = column[String]("USER_NAME", O PrimaryKey) - def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _) + def isManager = column[Boolean]("MANAGER") + def * = groupName ~ userName ~ isManager <> (GroupMember, GroupMember.unapply _) } case class GroupMember( groupName: String, - userName: String + userName: String, + isManager: Boolean ) \ No newline at end of file diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala index 84e78a1..c57d0ce 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -59,7 +59,7 @@ Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] = - Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption + Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption def getAllUsers(includeRemoved: Boolean = true): List[Account] = if(includeRemoved){ @@ -122,18 +122,17 @@ def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit = Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed) - def updateGroupMembers(groupName: String, members: List[String]): Unit = { + def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = { Query(GroupMembers).filter(_.groupName is groupName.bind).delete - members.foreach { userName => - GroupMembers insert GroupMember (groupName, userName) + members.foreach { case (userName, isManager) => + GroupMembers insert GroupMember (groupName, userName, isManager) } } - def getGroupMembers(groupName: String): List[String] = + def getGroupMembers(groupName: String): List[GroupMember] = Query(GroupMembers) .filter(_.groupName is groupName.bind) .sortBy(_.userName) - .map(_.userName) .list def getGroupsByUserName(userName: String): List[String] = diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index 54edaf3..590e300 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -119,16 +119,10 @@ // get issues and comment count and labels searchIssueQuery(repos, condition, filterUser, onlyPullRequest) .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } - .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } - .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } - .map { case (((t1, t2), t3), t4) => - (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) - } - .sortBy(_._4) // labelName - .sortBy { case (t1, commentCount, _,_,_) => + .sortBy { case (t1, t2) => (condition.sort match { case "created" => t1.registeredDate - case "comments" => commentCount + case "comments" => t2.commentCount case "updated" => t1.updatedDate }) match { case sort => condition.direction match { @@ -138,6 +132,11 @@ } } .drop(offset).take(limit) + .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } + .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } + .map { case (((t1, t2), t3), t4) => + (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) + } .list .splitWith { (c1, c2) => c1._1.userName == c2._1.userName && @@ -314,6 +313,14 @@ }.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 { diff --git a/src/main/scala/service/RepositorySearchService.scala b/src/main/scala/service/RepositorySearchService.scala index ac4f177..57265c8 100644 --- a/src/main/scala/service/RepositorySearchService.scala +++ b/src/main/scala/service/RepositorySearchService.scala @@ -63,8 +63,8 @@ val list = new ListBuffer[(String, String)] while (treeWalk.next()) { - if(treeWalk.getFileMode(0) != FileMode.TREE){ - JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes => + if(treeWalk.getFileMode(0) == FileMode.REGULAR_FILE){ + JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes => if(FileUtil.isText(bytes)){ val text = StringUtil.convertFromByteArray(bytes) val lowerText = text.toLowerCase diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala index 74bd139..9038ea5 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -147,7 +147,8 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } @@ -162,7 +163,8 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } @@ -195,10 +197,18 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } + private def getRepositoryManagers(userName: String): Seq[String] = + if(getAccountByUserName(userName).exists(_.isGroupAccount)){ + getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } + } else { + Seq(userName) + } + /** * Updates the last activity date of the repository. */ @@ -280,19 +290,19 @@ case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository, issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, - branchList: List[String], tags: List[util.JGitUtil.TagInfo]){ + branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){ /** * Creates instance with issue count and pull request count. */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) = - this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags) + def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = + this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) /** * Creates instance without issue count and pull request count. */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) = - this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags) + def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = + this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) } case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala index de7a966..f15bd37 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -3,9 +3,16 @@ import util.Directory._ import util.ControlUtil._ import SystemSettingsService._ +import javax.servlet.http.HttpServletRequest trait SystemSettingsService { + def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse { + defining(request.getRequestURL.toString){ url => + url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) + } + }.replaceFirst("/$", "") + def saveSystemSettings(settings: SystemSettings): Unit = { defining(new java.util.Properties()){ props => settings.baseUrl.foreach(props.setProperty(BaseURL, _)) diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index 00fbabd..f4e3ac5 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -234,7 +234,7 @@ builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) } else { created = false - updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) + updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) } } } @@ -268,35 +268,35 @@ */ def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, mailAddress: String, message: String): Unit = { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - var removed = false + LockUtil.lock(s"${owner}/${repository}/wiki"){ + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + var removed = false - using(new RevWalk(git.getRepository)){ revWalk => - using(new TreeWalk(git.getRepository)){ treeWalk => - val index = treeWalk.addTree(revWalk.parseTree(headId)) - treeWalk.setRecursive(true) - while(treeWalk.next){ - val path = treeWalk.getPathString - val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) - if(path != pageName + ".md"){ - builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) - } else { - removed = true - } + using(new RevWalk(git.getRepository)){ revWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => + val index = treeWalk.addTree(revWalk.parseTree(headId)) + treeWalk.setRecursive(true) + while(treeWalk.next){ + val path = treeWalk.getPathString + val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) + if(path != pageName + ".md"){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } else { + removed = true } } + } - if(removed){ - builder.finish() - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) - } + if(removed){ + builder.finish() + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) } } } + } } } diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index bfa6992..830ca29 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -50,6 +50,7 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + Version(1, 12), Version(1, 11), Version(1, 10), Version(1, 9), diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala index b909ba1..74b1d34 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -8,7 +8,7 @@ import javax.servlet.ServletConfig import javax.servlet.ServletContext -import javax.servlet.http.HttpServletRequest +import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import util.{StringUtil, Keys, JGitUtil, Directory} import util.ControlUtil._ import util.Implicits._ @@ -23,7 +23,7 @@ * This servlet provides only Git repository functionality. * Authentication is provided by [[servlet.BasicAuthenticationFilter]]. */ -class GitRepositoryServlet extends GitServlet { +class GitRepositoryServlet extends GitServlet with SystemSettingsService { private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet]) @@ -47,13 +47,24 @@ super.init(config) } - + + override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = { + val agent = req.getHeader("USER-AGENT") + if(agent == null || !agent.startsWith("git/")){ + // redirect for browsers + val paths = req.getRequestURI.split("/") + res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last.replaceFirst("\\.git$", "")) + } else { + // response for git client + super.service(req, res) + } + } } -class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] { +class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) - + override def create(request: HttpServletRequest, db: Repository): ReceivePack = { val receivePack = new ReceivePack(db) val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String] @@ -64,13 +75,11 @@ defining(request.paths){ paths => val owner = paths(1) val repository = paths(2).replaceFirst("\\.git$", "") - val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "") // TODO Use base URL in SystemSettings logger.debug("repository:" + owner + "/" + repository) - logger.debug("baseURL:" + baseURL) if(!repository.endsWith(".wiki")){ - receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL)) + receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request))) } receivePack } @@ -79,7 +88,7 @@ import scala.collection.JavaConverters._ -class CommitLogHook(owner: String, repository: String, pusher: String, baseURL: String) extends PostReceiveHook +class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) @@ -143,12 +152,20 @@ } } + // close issues + val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch + if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){ + git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach { commit => + closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository) + } + } + // call web hook getWebHookURLs(owner, repository) match { case webHookURLs if(webHookURLs.nonEmpty) => for(pusherAccount <- getAccountByUserName(pusher); ownerAccount <- getAccountByUserName(owner); - repositoryInfo <- getRepository(owner, repository, baseURL)){ + repositoryInfo <- getRepository(owner, repository, baseUrl)){ callWebHook(owner, repository, webHookURLs, WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)) } @@ -181,7 +198,7 @@ */ private def updatePullRequests(branch: String) = getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => - if(getRepository(pullreq.userName, pullreq.repositoryName, baseURL).isDefined){ + if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){ using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git => git.fetch .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString) diff --git a/src/main/scala/util/Authenticator.scala b/src/main/scala/util/Authenticator.scala index c524713..f40af7b 100644 --- a/src/main/scala/util/Authenticator.scala +++ b/src/main/scala/util/Authenticator.scala @@ -29,7 +29,7 @@ /** * Allows only the repository owner and administrators. */ -trait OwnerAuthenticator { self: ControllerBase with RepositoryService => +trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService => protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } @@ -40,6 +40,9 @@ context.loginAccount match { case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository) + case Some(x) if(getGroupMembers(repository.owner).exists { member => + member.userName == x.userName && member.isManager == true + }) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() @@ -106,7 +109,7 @@ } /** - * Allows only the repository owner and administrators. + * Allows only the repository owner (or manager for group repository) and administrators. */ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } @@ -155,3 +158,24 @@ } } } + +/** + * 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() + } + } + } + } +} diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala index 02d7fd1..0b0f712 100644 --- a/src/main/scala/util/ControlUtil.scala +++ b/src/main/scala/util/ControlUtil.scala @@ -3,7 +3,7 @@ import org.eclipse.jgit.api.Git import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.treewalk.TreeWalk -import org.eclipse.jgit.transport.RefSpec +import scala.util.control.Exception._ import scala.language.reflectiveCalls /** @@ -16,10 +16,8 @@ def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B = try f(resource) finally { if(resource != null){ - try { + ignoring(classOf[Throwable]) { resource.close() - } catch { - case e: Throwable => // ignore } } } diff --git a/src/main/scala/util/Implicits.scala b/src/main/scala/util/Implicits.scala index a9e648c..77c095e 100644 --- a/src/main/scala/util/Implicits.scala +++ b/src/main/scala/util/Implicits.scala @@ -1,6 +1,7 @@ package util import scala.util.matching.Regex +import scala.util.control.Exception._ import javax.servlet.http.{HttpSession, HttpServletRequest} /** @@ -42,10 +43,8 @@ sb.toString } - def toIntOpt: Option[Int] = try { - Option(Integer.parseInt(value)) - } catch { - case e: NumberFormatException => None + def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt { + Integer.parseInt(value) } } diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index 742641e..0e2cb40 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -11,17 +11,20 @@ import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.diff.DiffEntry.ChangeType -import org.eclipse.jgit.errors.MissingObjectException +import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} import java.util.Date import org.eclipse.jgit.api.errors.NoHeadException import service.RepositoryService import org.eclipse.jgit.dircache.DirCacheEntry +import org.slf4j.LoggerFactory /** * Provides complex JGit operations. */ object JGitUtil { + private val logger = LoggerFactory.getLogger(JGitUtil.getClass) + /** * The repository data. * @@ -45,9 +48,10 @@ * @param commitId the last commit id * @param committer the last committer name * @param mailAddress the committer's mail address + * @param linkUrl the url of submodule */ case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, - committer: String, mailAddress: String) + committer: String, mailAddress: String, linkUrl: Option[String]) /** * The commit data. @@ -105,6 +109,15 @@ 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. * * @param git the Git object @@ -128,7 +141,7 @@ using(Git.open(getRepositoryDir(owner, repository))){ git => try { // get commit count - val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum + val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10000).sum RepositoryInfo( owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", @@ -162,7 +175,7 @@ * @return HTML of the file list */ def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { - val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)] + val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] using(new RevWalk(git.getRepository)){ revWalk => val objectId = git.getRepository.resolve(revision) @@ -195,22 +208,28 @@ }) } while (treeWalk.next()) { - list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString)) + // submodule + val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ + getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url) + } else None + + list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) } } } val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) - list.map { case (objectId, fileMode, path, name) => + list.map { case (objectId, fileMode, path, name, linkUrl) => FileInfo( objectId, - fileMode == FileMode.TREE, + fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, name, commits(path).getCommitterIdent.getWhen, commits(path).getShortMessage, commits(path).getName, commits(path).getCommitterIdent.getName, - commits(path).getCommitterIdent.getEmailAddress) + commits(path).getCommitterIdent.getEmailAddress, + linkUrl) }.sortWith { (file1, file2) => (file1.isDirectory, file2.isDirectory) match { case (true , false) => true @@ -326,27 +345,6 @@ } /** - * 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. */ def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = { @@ -377,7 +375,7 @@ DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None) } else { DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, - JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) + JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) })) } (buffer.toList, None) @@ -400,8 +398,8 @@ DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) } else { DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, - JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), - JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) + JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), + JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) } }.toList } @@ -494,4 +492,73 @@ 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 + } + } diff --git a/src/main/scala/util/Keys.scala b/src/main/scala/util/Keys.scala index 4aabe35..920dfca 100644 --- a/src/main/scala/util/Keys.scala +++ b/src/main/scala/util/Keys.scala @@ -13,12 +13,7 @@ /** * Session key for the logged in account information. */ - val LoginAccount = "LOGIN_ACCOUNT" - - /** - * Session key for the redirect URL. - */ - val Redirect = "REDIRECT" + val LoginAccount = "loginAccount" /** * Session key for the issue search condition in dashboard. @@ -47,6 +42,20 @@ } + object Flash { + + /** + * Flash key for the redirect URL. + */ + val Redirect = "redirect" + + /** + * Flash key for the information message. + */ + val Info = "info" + + } + /** * Define request keys. */ diff --git a/src/main/scala/util/StringUtil.scala b/src/main/scala/util/StringUtil.scala index 7de2a62..54da029 100644 --- a/src/main/scala/util/StringUtil.scala +++ b/src/main/scala/util/StringUtil.scala @@ -31,7 +31,7 @@ /** * Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]]. - * And if given bytes contains UTF-8 BOM, it's removed from returned string.. + * And if given bytes contains UTF-8 BOM, it's removed from returned string. */ def convertFromByteArray(content: Array[Byte]): String = IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content)) @@ -47,12 +47,21 @@ } /** - * Extract issue id like ````#issueId``` from the given message. + * Extract issue id like ```#issueId``` from the given message. * *@param message the message which may contains issue id * @return the iterator of issue id */ def extractIssueId(message: String): Iterator[String] = - "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map { matchData => matchData.group(2) } + "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2)) + + /** + * Extract close issue id like ```close #issueId ``` from the given message. + * + * @param message the message which may contains close command + * @return the iterator of issue id + */ + def extractCloseId(message: String): Iterator[String] = + "(?i)(? +
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + @helper.html.uploadavatar(account) +
+
+
+
+ +
    +
+ @helper.html.account("memberName", 200) + + +
+ +
+
+
+
+
+ @if(account.isDefined){ + + } + + @if(account.isDefined){ + Cancel + } +
+
+ +} + \ No newline at end of file diff --git a/src/main/twirl/account/main.scala.html b/src/main/twirl/account/main.scala.html index 0b6cc6e..17e05c0 100644 --- a/src/main/twirl/account/main.scala.html +++ b/src/main/twirl/account/main.scala.html @@ -1,4 +1,5 @@ -@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context) +@(account: model.Account, groupNames: List[String], active: String, + isGroupManager: Boolean = false)(body: Html)(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(account.userName){ @@ -41,6 +42,13 @@ } + @if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){ +
  • +
    + Edit Group +
    +
  • + } @body diff --git a/src/main/twirl/account/members.scala.html b/src/main/twirl/account/members.scala.html index 14d7c77..0e21d01 100644 --- a/src/main/twirl/account/members.scala.html +++ b/src/main/twirl/account/members.scala.html @@ -1,7 +1,7 @@ -@(account: model.Account, members: List[String])(implicit context: app.Context) +@(account: model.Account, members: List[String], isGroupManager: Boolean)(implicit context: app.Context) @import context._ @import view.helpers._ -@main(account, Nil, "members"){ +@main(account, Nil, "members", isGroupManager){ @if(members.isEmpty){ No members } else { diff --git a/src/main/twirl/account/newrepo.scala.html b/src/main/twirl/account/newrepo.scala.html new file mode 100644 index 0000000..b5f6c6a --- /dev/null +++ b/src/main/twirl/account/newrepo.scala.html @@ -0,0 +1,73 @@ +@(groupNames: List[String])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("Create a New Repository"){ +
    +
    +
    + +
    + + + +
    + / + + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +} + diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html index f9037f5..100c2b4 100644 --- a/src/main/twirl/account/repositories.scala.html +++ b/src/main/twirl/account/repositories.scala.html @@ -1,7 +1,9 @@ -@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) +@(account: model.Account, groupNames: List[String], + repositories: List[service.RepositoryService.RepositoryInfo], + isGroupManager: Boolean)(implicit context: app.Context) @import context._ @import view.helpers._ -@main(account, groupNames, "repositories"){ +@main(account, groupNames, "repositories", isGroupManager){ @if(repositories.isEmpty){ No repositories } else { diff --git a/src/main/twirl/admin/users/group.scala.html b/src/main/twirl/admin/users/group.scala.html index abf64cb..37369aa 100644 --- a/src/main/twirl/admin/users/group.scala.html +++ b/src/main/twirl/admin/users/group.scala.html @@ -1,11 +1,11 @@ -@(account: Option[model.Account], members: List[String])(implicit context: app.Context) +@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(if(account.isEmpty) "New Group" else "Update Group"){ @admin.html.menu("users"){
    -
    +
    @@ -24,29 +24,23 @@
    - +
    @helper.html.uploadavatar(account)
    -
    +
    -
      - @members.map { userName => -
    • - @userName - (remove) -
    • - } +
      @helper.html.account("memberName", 200) - +
      - +
    @@ -60,6 +54,10 @@ } \ No newline at end of file diff --git a/src/main/twirl/header.scala.html b/src/main/twirl/header.scala.html index bb2be97..d7591ba 100644 --- a/src/main/twirl/header.scala.html +++ b/src/main/twirl/header.scala.html @@ -30,6 +30,9 @@ } }
    +@repository.repository.description.map { description => +

    @description

    +} - @if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){ + @if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){ diff --git a/src/main/twirl/main.scala.html b/src/main/twirl/main.scala.html index 7ec9842..51ba07b 100644 --- a/src/main/twirl/main.scala.html +++ b/src/main/twirl/main.scala.html @@ -54,14 +54,18 @@ } @if(loginAccount.isDefined){ @avatar(loginAccount.get.userName, 20) @loginAccount.get.userName - + + @if(loginAccount.get.isAdmin){ } } else { - Sign in + Sign in } @@ -76,7 +80,6 @@ $('#search').submit(function(){ return $.trim($(this).find('input[name=query]').val()) != ''; }); - $('#signin').attr('href', '@path/signin?redirect=' + encodeURIComponent(location.pathname + location.search + location.hash)); }); diff --git a/src/main/twirl/newrepo.scala.html b/src/main/twirl/newrepo.scala.html deleted file mode 100644 index 05fefbc..0000000 --- a/src/main/twirl/newrepo.scala.html +++ /dev/null @@ -1,73 +0,0 @@ -@(groupNames: List[String])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@main("Create a New Repository"){ -
    - -
    - -
    - - - -
    - / - - -
    -
    - - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - -
    -} - diff --git a/src/main/twirl/repo/branches.scala.html b/src/main/twirl/repo/branches.scala.html index 219c7c1..bc26459 100644 --- a/src/main/twirl/repo/branches.scala.html +++ b/src/main/twirl/repo/branches.scala.html @@ -1,4 +1,4 @@ -@(branchInfo: List[(String, java.util.Date)], +@(branchInfo: Seq[(String, java.util.Date)], hasWritePermission: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @import context._ diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html index 94f6dac..6b4739c 100644 --- a/src/main/twirl/repo/files.scala.html +++ b/src/main/twirl/repo/files.scala.html @@ -3,7 +3,7 @@ pathList: List[String], latestCommit: util.JGitUtil.CommitInfo, files: List[util.JGitUtil.FileInfo], - readme: Option[String])(implicit context: app.Context) + readme: Option[(util.JGitUtil.FileInfo, String)])(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @@ -12,7 +12,7 @@
    - @readme.map { content => + @readme.map { case(file, content) =>
    -
    README.md
    +
    @file.name
    @markdown(content, repository, false, false)
    } diff --git a/src/main/twirl/settings/collaborators.scala.html b/src/main/twirl/settings/collaborators.scala.html index 8264764..cf894a4 100644 --- a/src/main/twirl/settings/collaborators.scala.html +++ b/src/main/twirl/settings/collaborators.scala.html @@ -13,6 +13,10 @@ @collaboratorName @if(!isGroupRepository){ (remove) + } else { + @if(repository.managers.contains(collaboratorName)){ + (Manager) + } } } diff --git a/src/main/webapp/assets/common/images/folder_link.png b/src/main/webapp/assets/common/images/folder_link.png new file mode 100644 index 0000000..b9b75f6 --- /dev/null +++ b/src/main/webapp/assets/common/images/folder_link.png Binary files differ diff --git a/src/test/scala/service/AccountServiceServiceSpec.scala b/src/test/scala/service/AccountServiceServiceSpec.scala deleted file mode 100644 index ba53327..0000000 --- a/src/test/scala/service/AccountServiceServiceSpec.scala +++ /dev/null @@ -1,78 +0,0 @@ -package service - -import org.specs2.mutable.Specification -import java.util.Date - -class AccountServiceServiceSpec extends Specification with ServiceSpecBase { - - "AccountService" should { - val RootMailAddress = "root@localhost" - - "getAllUsers" in { withTestDB{ - AccountService.getAllUsers() must be like{ - case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok - } - }} - - "getAccountByUserName" in { withTestDB{ - AccountService.getAccountByUserName("root") must beSome.like{ - case user => user.userName must_== "root" - } - - AccountService.getAccountByUserName("invalid user name") must beNone - }} - - "getAccountByMailAddress" in { withTestDB{ - AccountService.getAccountByMailAddress(RootMailAddress) must beSome - }} - - "updateLastLoginDate" in { withTestDB{ - val root = "root" - def user() = - AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) - - user().lastLoginDate must beNone - val date1 = new Date - AccountService.updateLastLoginDate(root) - user().lastLoginDate must beSome.like{ case date => - date must be_>(date1) - } - val date2 = new Date - Thread.sleep(1000) - AccountService.updateLastLoginDate(root) - user().lastLoginDate must beSome.like{ case date => - date must be_>(date2) - } - }} - - "updateAccount" in { withTestDB{ - val root = "root" - def user() = - AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) - - val newAddress = "new mail address" - AccountService.updateAccount(user().copy(mailAddress = newAddress)) - user().mailAddress must_== newAddress - }} - - "group" in { withTestDB { - val group1 = "group1" - val user1 = "root" - AccountService.createGroup(group1, None) - - AccountService.getGroupMembers(group1) must_== Nil - AccountService.getGroupsByUserName(user1) must_== Nil - - AccountService.updateGroupMembers(group1, List(user1)) - - AccountService.getGroupMembers(group1) must_== List(user1) - AccountService.getGroupsByUserName(user1) must_== List(group1) - - AccountService.updateGroupMembers(group1, Nil) - - AccountService.getGroupMembers(group1) must_== Nil - AccountService.getGroupsByUserName(user1) must_== Nil - }} - } -} - diff --git a/src/test/scala/service/AccountServiceSpec.scala b/src/test/scala/service/AccountServiceSpec.scala new file mode 100644 index 0000000..344f8e3 --- /dev/null +++ b/src/test/scala/service/AccountServiceSpec.scala @@ -0,0 +1,79 @@ +package service + +import org.specs2.mutable.Specification +import java.util.Date +import model.GroupMember + +class AccountServiceSpec extends Specification with ServiceSpecBase { + + "AccountService" should { + val RootMailAddress = "root@localhost" + + "getAllUsers" in { withTestDB{ + AccountService.getAllUsers() must be like{ + case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok + } + }} + + "getAccountByUserName" in { withTestDB{ + AccountService.getAccountByUserName("root") must beSome.like{ + case user => user.userName must_== "root" + } + + AccountService.getAccountByUserName("invalid user name") must beNone + }} + + "getAccountByMailAddress" in { withTestDB{ + AccountService.getAccountByMailAddress(RootMailAddress) must beSome + }} + + "updateLastLoginDate" in { withTestDB{ + val root = "root" + def user() = + AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) + + user().lastLoginDate must beNone + val date1 = new Date + AccountService.updateLastLoginDate(root) + user().lastLoginDate must beSome.like{ case date => + date must be_>(date1) + } + val date2 = new Date + Thread.sleep(1000) + AccountService.updateLastLoginDate(root) + user().lastLoginDate must beSome.like{ case date => + date must be_>(date2) + } + }} + + "updateAccount" in { withTestDB{ + val root = "root" + def user() = + AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) + + val newAddress = "new mail address" + AccountService.updateAccount(user().copy(mailAddress = newAddress)) + user().mailAddress must_== newAddress + }} + + "group" in { withTestDB { + val group1 = "group1" + val user1 = "root" + AccountService.createGroup(group1, None) + + AccountService.getGroupMembers(group1) must_== Nil + AccountService.getGroupsByUserName(user1) must_== Nil + + AccountService.updateGroupMembers(group1, List((user1, true))) + + AccountService.getGroupMembers(group1) must_== List(GroupMember(group1, user1, true)) + AccountService.getGroupsByUserName(user1) must_== List(group1) + + AccountService.updateGroupMembers(group1, Nil) + + AccountService.getGroupMembers(group1) must_== Nil + AccountService.getGroupsByUserName(user1) must_== Nil + }} + } +} + diff --git a/src/test/scala/util/StringUtilSpec.scala b/src/test/scala/util/StringUtilSpec.scala index 26056a5..caaa2dc 100644 --- a/src/test/scala/util/StringUtilSpec.scala +++ b/src/test/scala/util/StringUtilSpec.scala @@ -35,4 +35,22 @@ 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 + } + } }