diff --git a/.gitignore b/.gitignore
index e6c4d47..51bd3f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@
.classpath
.project
.cache
+.settings
# IntelliJ specific
.idea/
diff --git a/README.md b/README.md
index c6311fd..fa6718d 100644
--- a/README.md
+++ b/README.md
@@ -37,23 +37,64 @@
The default administrator account is **root** and password is **root**.
-or you can start GitBucket by ```java -jar gitbucket.war``` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
+or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
- --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.
+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.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
+### Mac OS X
+On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
+
+Run the following commands in `Terminal` to
+
+- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
+- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
+
Release Notes
--------
+### 1.11 - End of Feb 2014
+- Base URL for redirect, notification and repository URL box is configurable
+- Headline anchor is available for Markdown contents such as Wiki page
+- Improve H2 connectivity
+- Label is available for pull requests not only issues
+- Delete branch button is added
+- Repository icons are updated
+- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
+- Display reference to issue from others in comment list
+- Fix some bugs
+
+### 1.10 - 01 Feb 2014
+- Rename repository
+- Transfer repository owner
+- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
+- Add LDAP display name attribute
+- Response performance improvement
+- Fix some bugs
+
+### 1.9 - 28 Dec 2013
+- Display GITBUCKET_HOME on the system settings page
+- Fix some bugs
+
+### 1.8 - 30 Nov 2013
+- Add user and group deletion
+- Improve pull request performance
+- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
+- LDAP StartTLS support
+- Enable hard wrapping in Markdown
+- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
+- Fix some bugs
+
### 1.7 - 26 Oct 2013
- Support working on Java6 in embedded Jetty mode
-- Add ```--host``` option to bind specified host name in embedded Jetty mode
-- Add ```--https=true``` option to use https in embedded Jetty mode
+- Add `--host` option to bind specified host name in embedded Jetty mode
+- Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy
- Add full name as user property
- Change link color for absent Wiki pages
- Add ZIP download button to the repository viewer tab
diff --git a/contrib/macosx/gitbucket.plist b/contrib/macosx/gitbucket.plist
new file mode 100644
index 0000000..827c15a
--- /dev/null
+++ b/contrib/macosx/gitbucket.plist
@@ -0,0 +1,20 @@
+
+
+
+
+ Label
+ gitbucket
+ ProgramArguments
+
+ /usr/bin/java
+ -Dmail.smtp.starttls.enable=true
+ -jar
+ gitbucket.war
+ --host=127.0.0.1
+ --port=8080
+ --https=true
+
+ RunAtLoad
+
+
+
diff --git a/etc/icons.svg b/etc/icons.svg
index 6943304..c2efe0e 100644
--- a/etc/icons.svg
+++ b/etc/icons.svg
@@ -25,17 +25,17 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
- inkscape:cx="629.30023"
- inkscape:cy="281.44758"
+ inkscape:cx="450.21999"
+ inkscape:cy="97.51519"
inkscape:document-units="px"
inkscape:current-layer="layer1-9"
showgrid="false"
inkscape:window-width="1366"
- inkscape:window-height="705"
- inkscape:window-x="-8"
+ inkscape:window-height="706"
+ inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
- inkscape:snap-global="true"
+ inkscape:snap-global="false"
inkscape:snap-grids="false"
inkscape:snap-page="false"
inkscape:snap-bbox="true"
@@ -746,6 +746,238 @@
d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z"
id="rect2995-0-2-7-7"
inkscape:connector-curvature="0" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/project/build.scala b/project/build.scala
index df7b368..ce6d4d9 100644
--- a/project/build.scala
+++ b/project/build.scala
@@ -1,8 +1,6 @@
import sbt._
import Keys._
import org.scalatra.sbt._
-import org.scalatra.sbt.PluginKeys._
-import sbt.ScalaVersion
import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
@@ -25,14 +23,14 @@
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
),
- scalacOptions := Seq("-deprecation"),
+ scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.5",
- "jp.sf.amateras" %% "scalatra-forms" % "0.0.8",
+ "jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5",
@@ -43,7 +41,7 @@
"com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
- "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")),
+ "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.11" % "test"
),
EclipseKeys.withSource := true,
diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java
index a644f1f..d7b137d 100644
--- a/src/main/java/JettyLauncher.java
+++ b/src/main/java/JettyLauncher.java
@@ -53,6 +53,9 @@
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
context.setServer(server);
context.setWar(location.toExternalForm());
+ if (forceHttps) {
+ context.setInitParameter("org.scalatra.ForceHttps", "true");
+ }
server.setHandler(context);
server.start();
diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala
index 4ef4c88..de3c40d 100644
--- a/src/main/scala/ScalatraBootstrap.scala
+++ b/src/main/scala/ScalatraBootstrap.scala
@@ -17,7 +17,6 @@
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
- context.mount(new SignInController, "/*")
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala
index b32d5df..5bea0b4 100644
--- a/src/main/scala/app/AccountController.scala
+++ b/src/main/scala/app/AccountController.scala
@@ -9,12 +9,10 @@
import org.apache.commons.io.FileUtils
class AccountController extends AccountControllerBase
- with SystemSettingsService with AccountService with RepositoryService with ActivityService
- with OneselfAuthenticator
+ with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
- self: SystemSettingsService with AccountService with RepositoryService with ActivityService
- with OneselfAuthenticator =>
+ self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala
index a026a18..bdea042 100644
--- a/src/main/scala/app/ControllerBase.scala
+++ b/src/main/scala/app/ControllerBase.scala
@@ -10,8 +10,7 @@
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import model.Account
-import scala.Some
-import service.AccountService
+import service.{SystemSettingsService, AccountService}
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
@@ -21,7 +20,7 @@
* Provides generic features for controller implementations.
*/
abstract class ControllerBase extends ScalatraFilter
- with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations {
+ with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations with SystemSettingsService {
implicit val jsonFormats = DefaultFormats
@@ -58,11 +57,7 @@
/**
* Returns the context object for the request.
*/
- implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
-
- private def currentURL: String = defining(request.getQueryString){ queryString =>
- request.getRequestURI + (if(queryString != null) "?" + queryString else "")
- }
+ implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request)
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
@@ -107,27 +102,27 @@
if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin"))
} else {
- org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(currentURL)))
+ 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"))
}
}
}
- protected def baseUrl = defining(request.getRequestURL.toString){ url =>
- url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
- }
+ protected def baseUrl = loadSystemSettings().baseUrl.getOrElse {
+ defining(request.getRequestURL.toString){ url =>
+ url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
+ }
+ }.replaceFirst("/$", "")
}
/**
* Context object for the current request.
*/
-case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
-
- def redirectUrl = if(request.getParameter("redirect") != null){
- request.getParameter("redirect")
- } else {
- currentUrl
- }
+case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
/**
* Get object from cache.
diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala
index 67083aa..16cd05c 100644
--- a/src/main/scala/app/CreateRepositoryController.scala
+++ b/src/main/scala/app/CreateRepositoryController.scala
@@ -113,54 +113,62 @@
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
- if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
- // Insert to the database at first
- val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
- val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
+ 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)
- )
+ 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)
+ // Insert default labels
+ insertDefaultLabels(loginUserName, repository.name)
- // clone repository actually
- JGitUtil.cloneRepository(
- getRepositoryDir(repository.owner, repository.name),
- getRepositoryDir(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))
+ // 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)
+ // 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(_) => ???
}
- case Left(_) => ???
}
}
- }
- // Record activity
- recordForkActivity(repository.owner, repository.name, loginUserName)
+ // Record activity
+ recordForkActivity(repository.owner, repository.name, loginUserName)
+ // redirect to the repository
+ redirect(s"/${loginUserName}/${repository.name}")
+ }
}
- // redirect to the repository
- redirect("/%s/%s".format(loginUserName, repository.name))
}
})
diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala
index d18b3d3..2ee96bd 100644
--- a/src/main/scala/app/IndexController.scala
+++ b/src/main/scala/app/IndexController.scala
@@ -1,16 +1,22 @@
package app
import util._
+import util.Implicits._
import service._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
- with RepositoryService with SystemSettingsService with ActivityService with AccountService
-with UsersAuthenticator
+ with RepositoryService with ActivityService with AccountService with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
- self: RepositoryService with SystemSettingsService with ActivityService with AccountService
- with UsersAuthenticator =>
+ self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
+
+ case class SignInForm(userName: String, password: String)
+
+ val form = mapping(
+ "userName" -> trim(label("Username", text(required))),
+ "password" -> trim(label("Password", text(required)))
+ )(SignInForm.apply)
get("/"){
val loginAccount = context.loginAccount
@@ -22,6 +28,44 @@
)
}
+ get("/signin"){
+ val redirect = params.get("redirect")
+ if(redirect.isDefined && redirect.get.startsWith("/")){
+ session.setAttribute(Keys.Session.Redirect, redirect.get)
+ }
+ html.signin(loadSystemSettings())
+ }
+
+ post("/signin", form){ form =>
+ authenticate(loadSystemSettings(), form.userName, form.password) match {
+ case Some(account) => signin(account)
+ case None => redirect("/signin")
+ }
+ }
+
+ get("/signout"){
+ session.invalidate
+ redirect("/")
+ }
+
+ /**
+ * Set account information into HttpSession and redirect.
+ */
+ private def signin(account: model.Account) = {
+ session.setAttribute(Keys.Session.LoginAccount, account)
+ updateLastLoginDate(account.userName)
+
+ session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
+ if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
+ redirect("/")
+ } else {
+ redirect(redirectUrl)
+ }
+ }.getOrElse {
+ redirect("/")
+ }
+ }
+
/**
* JSON API for collaborator completion.
*
diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala
index 22544e7..be564ee 100644
--- a/src/main/scala/app/IssuesController.scala
+++ b/src/main/scala/app/IssuesController.scala
@@ -4,10 +4,11 @@
import service._
import IssuesService._
-import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
+import util._
import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok
+import model.Issue
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -110,6 +111,11 @@
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
+ // extract references and create refer comment
+ getIssue(owner, name, issueId.toString).foreach { issue =>
+ createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
+ }
+
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
@@ -123,7 +129,11 @@
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
+ // update issue
updateIssue(owner, name, issue.issueId, form.title, form.content)
+ // extract references and create refer comment
+ createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
+
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
@@ -274,6 +284,15 @@
redirect(s"/${repository.owner}/${repository.name}/issues")
}
+ private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
+ StringUtil.extractIssueId(message).foreach { issueId =>
+ if(getIssue(owner, repository, issueId).isDefined){
+ createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
+ fromIssue.issueId + ":" + fromIssue.title, "refer")
+ }
+ }
+ }
+
/**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
@@ -313,6 +332,11 @@
}
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
+ // extract references and create refer comment
+ content.map { content =>
+ createReferComment(owner, name, issue, content)
+ }
+
// notifications
Notifier() match {
case f =>
diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala
index e91ff78..5a3317d 100644
--- a/src/main/scala/app/PullRequestsController.scala
+++ b/src/main/scala/app/PullRequestsController.scala
@@ -18,26 +18,28 @@
import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException
+import service.WebHookService.WebHookPayload
class PullRequestsController extends PullRequestsControllerBase
- with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
- with ReferrerAuthenticator with CollaboratorsAuthenticator
+ with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
+ with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase {
- self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService
- with ReferrerAuthenticator with CollaboratorsAuthenticator =>
+ self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
+ with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping(
- "title" -> trim(label("Title" , text(required, maxlength(100)))),
- "content" -> trim(label("Content", optional(text()))),
- "targetUserName" -> trim(text(required, maxlength(100))),
- "targetBranch" -> trim(text(required, maxlength(100))),
- "requestUserName" -> trim(text(required, maxlength(100))),
- "requestBranch" -> trim(text(required, maxlength(100))),
- "commitIdFrom" -> trim(text(required, maxlength(40))),
- "commitIdTo" -> trim(text(required, maxlength(40)))
+ "title" -> trim(label("Title" , text(required, maxlength(100)))),
+ "content" -> trim(label("Content", optional(text()))),
+ "targetUserName" -> trim(text(required, maxlength(100))),
+ "targetBranch" -> trim(text(required, maxlength(100))),
+ "requestUserName" -> trim(text(required, maxlength(100))),
+ "requestRepositoryName" -> trim(text(required, maxlength(100))),
+ "requestBranch" -> trim(text(required, maxlength(100))),
+ "commitIdFrom" -> trim(text(required, maxlength(40))),
+ "commitIdTo" -> trim(text(required, maxlength(40)))
)(PullRequestForm.apply)
val mergeForm = mapping(
@@ -50,6 +52,7 @@
targetUserName: String,
targetBranch: String,
requestUserName: String,
+ requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String)
@@ -76,8 +79,10 @@
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
+ getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
+ getLabels(owner, name),
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
@@ -90,7 +95,7 @@
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
- val name = repository.name
+ val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
pulls.html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
@@ -100,10 +105,25 @@
} getOrElse NotFound
})
+ get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository =>
+ params("id").toIntOpt.map { issueId =>
+ val branchName = params("branchName")
+ val userName = context.loginAccount.get.userName
+ if(repository.repository.defaultBranch != branchName){
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
+ git.branchDelete().setBranchNames(branchName).call()
+ recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
+ }
+ }
+ createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
+ redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
+ } getOrElse NotFound
+ })
+
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner
- val name = repository.name
+ val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git =>
@@ -175,6 +195,15 @@
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
+ // call web hook
+ getWebHookURLs(owner, name) match {
+ case webHookURLs if(webHookURLs.nonEmpty) =>
+ for(ownerAccount <- getAccountByUserName(owner)){
+ callWebHook(owner, name, webHookURLs,
+ WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
+ }
+ case _ =>
+ }
// notifications
Notifier().toNotify(repository, issueId, "merge"){
@@ -215,73 +244,85 @@
}
})
- get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
+ get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
- val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
- val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
+ val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
+ val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
- (getRepository(originOwner, repository.name, baseUrl),
- getRepository(forkedOwner, repository.name, baseUrl)) match {
- case (Some(originRepository), Some(forkedRepository)) => {
- using(
- Git.open(getRepositoryDir(originOwner, repository.name)),
- Git.open(getRepositoryDir(forkedOwner, repository.name))
- ){ case (oldGit, newGit) =>
- val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
- val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
-
- val forkedId = getForkedCommitId(oldGit, newGit,
- originOwner, repository.name, originBranch,
- forkedOwner, repository.name, forkedBranch)
-
- val oldId = oldGit.getRepository.resolve(forkedId)
- val newId = newGit.getRepository.resolve(forkedBranch)
-
- val (commits, diffs) = getRequestCompareInfo(
- originOwner, repository.name, oldId.getName,
- forkedOwner, repository.name, newId.getName)
-
- pulls.html.compare(
- commits,
- diffs,
- repository.repository.originUserName.map { userName =>
- userName :: getForkedRepositories(userName, repository.name)
- } getOrElse List(repository.owner),
- originBranch,
- forkedBranch,
- oldId.getName,
- newId.getName,
- repository,
- originRepository,
- forkedRepository,
- hasWritePermission(repository.owner, repository.name, context.loginAccount))
+ (for(
+ originRepositoryName <- if(originOwner == forkedOwner){
+ Some(forkedRepository.name)
+ } else {
+ forkedRepository.repository.originRepositoryName.orElse {
+ getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
+ };
+ originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
+ ) yield {
+ using(
+ Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
+ Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
+ ){ case (oldGit, newGit) =>
+ val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
+ val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
+
+ val forkedId = getForkedCommitId(oldGit, newGit,
+ originRepository.owner, originRepository.name, originBranch,
+ forkedRepository.owner, forkedRepository.name, forkedBranch)
+
+ val oldId = oldGit.getRepository.resolve(forkedId)
+ val newId = newGit.getRepository.resolve(forkedBranch)
+
+ val (commits, diffs) = getRequestCompareInfo(
+ originRepository.owner, originRepository.name, oldId.getName,
+ forkedRepository.owner, forkedRepository.name, newId.getName)
+
+ pulls.html.compare(
+ commits,
+ diffs,
+ (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
+ case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
+ case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
+ },
+ originBranch,
+ forkedBranch,
+ oldId.getName,
+ newId.getName,
+ forkedRepository,
+ originRepository,
+ forkedRepository,
+ hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
}
- case _ => NotFound
- }
+ }) getOrElse NotFound
})
- ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository =>
+ ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
- val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
- val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
+ val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
+ val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
- (getRepository(originOwner, repository.name, baseUrl),
- getRepository(forkedOwner, repository.name, baseUrl)) match {
- case (Some(originRepository), Some(forkedRepository)) => {
- using(
- Git.open(getRepositoryDir(originOwner, repository.name)),
- Git.open(getRepositoryDir(forkedOwner, repository.name))
- ){ case (oldGit, newGit) =>
- val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
- val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
-
- pulls.html.mergecheck(
- checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
+ (for(
+ originRepositoryName <- if(originOwner == forkedOwner){
+ Some(forkedRepository.name)
+ } else {
+ forkedRepository.repository.originRepositoryName.orElse {
+ getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
+ };
+ originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
+ ) yield {
+ using(
+ Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
+ Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
+ ){ case (oldGit, newGit) =>
+ val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
+ val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
+
+ pulls.html.mergecheck(
+ checkConflict(originRepository.owner, originRepository.name, originBranch,
+ forkedRepository.owner, forkedRepository.name, forkedBranch))
}
- case _ => NotFound()
- }
+ }) getOrElse NotFound
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
@@ -303,7 +344,7 @@
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
- requestRepositoryName = repository.name,
+ requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
@@ -311,7 +352,7 @@
// fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch
- .setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
+ .setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call
}
@@ -336,12 +377,12 @@
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
-
- withTmpRefSpec(new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true), git) { ref =>
+ val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
+ try {
// fetch objects from origin repository branch
git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
- .setRefSpecs(ref)
+ .setRefSpecs(refSpec)
.call
// merge conflict check
@@ -353,6 +394,10 @@
} catch {
case e: NoMergeBaseException => true
}
+ } finally {
+ val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
+ refUpdate.setForceUpdate(true)
+ refUpdate.delete()
}
}
}
@@ -405,8 +450,7 @@
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
- existsCommitId(userName, repositoryName, commit.getName) &&
- JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
+ existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
@@ -421,7 +465,7 @@
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit)
- }.toList.splitWith{ (commit1, commit2) =>
+ }.toList.splitWith { (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}
diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala
index 05987d5..d745623 100644
--- a/src/main/scala/app/RepositorySettingsController.scala
+++ b/src/main/scala/app/RepositorySettingsController.scala
@@ -21,12 +21,13 @@
with OwnerAuthenticator with UsersAuthenticator =>
// for repository options
- case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
+ case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping(
- "description" -> trim(label("Description" , optional(text()))),
- "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
- "isPrivate" -> trim(label("Repository Type", boolean()))
+ "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
+ "description" -> trim(label("Description" , optional(text()))),
+ "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
+ "isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply)
// for collaborator addition
@@ -43,6 +44,13 @@
"url" -> trim(label("url", text(required, webHook)))
)(WebHookForm.apply)
+ // for transfer ownership
+ case class TransferOwnerShipForm(newOwner: String)
+
+ val transferForm = mapping(
+ "newOwner" -> trim(label("New owner", text(required, transferUser)))
+ )(TransferOwnerShipForm.apply)
+
/**
* Redirect to the Options page.
*/
@@ -70,8 +78,21 @@
repository.repository.isPrivate
} getOrElse form.isPrivate
)
+ // Change repository name
+ if(repository.name != form.repositoryName){
+ // Update database
+ renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
+ // Move git repository
+ defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
+ FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
+ }
+ // Move wiki repository
+ defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
+ FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
+ }
+ }
flash += "info" -> "Repository settings has been updated."
- redirect(s"/${repository.owner}/${repository.name}/settings/options")
+ redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
})
/**
@@ -138,17 +159,13 @@
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))
- val webHookURLs = getWebHookURLs(repository.owner, repository.name)
- if(webHookURLs.nonEmpty){
- val owner = getAccountByUserName(repository.owner).get
- callWebHook(repository.owner, repository.name, webHookURLs,
- WebHookPayload(
- git,
- owner,
- "refs/heads/" + repository.repository.defaultBranch,
- repository,
- commits.toList,
- owner))
+ getWebHookURLs(repository.owner, repository.name) match {
+ case webHookURLs if(webHookURLs.nonEmpty) =>
+ for(ownerAccount <- getAccountByUserName(repository.owner)){
+ callWebHook(repository.owner, repository.name, webHookURLs,
+ WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
+ }
+ case _ =>
}
flash += "info" -> "Test payload deployed!"
@@ -157,10 +174,30 @@
})
/**
- * Display the delete repository page.
+ * Display the danger zone.
*/
- get("/:owner/:repository/settings/delete")(ownerOnly {
- settings.html.delete(_)
+ get("/:owner/:repository/settings/danger")(ownerOnly {
+ settings.html.danger(_)
+ })
+
+ /**
+ * Transfer repository ownership.
+ */
+ post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
+ // Change repository owner
+ if(repository.owner != form.newOwner){
+ // Update database
+ renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
+ // Move git repository
+ defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
+ FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
+ }
+ // Move wiki repository
+ defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
+ FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
+ }
+ }
+ redirect(s"/${form.newOwner}/${repository.name}")
})
/**
@@ -199,4 +236,32 @@
}
}
+ /**
+ * Duplicate check for the rename repository name.
+ */
+ private def renameRepositoryName: Constraint = new Constraint(){
+ override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
+ params.get("repository").filter(_ != value).flatMap { _ =>
+ params.get("owner").flatMap { userName =>
+ getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
+ }
+ }
+ }
+
+ /**
+ * Provides Constraint to validate the repository transfer user.
+ */
+ private def transferUser: Constraint = new Constraint(){
+ override def validate(name: String, value: String, messages: Messages): Option[String] =
+ getAccountByUserName(value) match {
+ case None => Some("User does not exist.")
+ case Some(x) => if(x.userName == params("owner")){
+ Some("This is current repository owner.")
+ } else {
+ params.get("repository").flatMap { repositoryName =>
+ getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala
index 5fbfa75..2726136 100644
--- a/src/main/scala/app/RepositoryViewerController.scala
+++ b/src/main/scala/app/RepositoryViewerController.scala
@@ -3,7 +3,7 @@
import util.Directory._
import util.Implicits._
import util.ControlUtil._
-import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
+import _root_.util._
import service._
import org.scalatra._
import java.io.File
@@ -12,15 +12,16 @@
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream}
+import scala.Some
class RepositoryViewerController extends RepositoryViewerControllerBase
- with RepositoryService with AccountService with ReferrerAuthenticator
+ with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator
/**
* The repository viewer.
*/
trait RepositoryViewerControllerBase extends ControllerBase {
- self: RepositoryService with AccountService with ReferrerAuthenticator =>
+ self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
/**
* Returns converted HTML from Markdown for preview.
@@ -150,11 +151,26 @@
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
(branchName, revCommit.getCommitterIdent.getWhen)
}
- repo.html.branches(branchInfo, repository)
+ repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
}
})
/**
+ * Deletes branch.
+ */
+ get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository =>
+ val branchName = params("branchName")
+ val userName = context.loginAccount.get.userName
+ if(repository.repository.defaultBranch != branchName){
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
+ git.branchDelete().setBranchNames(branchName).call()
+ recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
+ }
+ }
+ redirect(s"/${repository.owner}/${repository.name}/branches")
+ })
+
+ /**
* Displays tags.
*/
get("/:owner/:repository/tags")(referrersOnly {
@@ -175,7 +191,8 @@
}
workDir.mkdirs
- val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
+ val zipFile = new File(workDir, repository.name + "-" +
+ (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
@@ -204,6 +221,7 @@
}
contentType = "application/octet-stream"
+ response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
zipFile
} else {
BadRequest
diff --git a/src/main/scala/app/SearchController.scala b/src/main/scala/app/SearchController.scala
index 99ea743..ab091cf 100644
--- a/src/main/scala/app/SearchController.scala
+++ b/src/main/scala/app/SearchController.scala
@@ -6,13 +6,10 @@
import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase
-with RepositoryService with AccountService with SystemSettingsService with ActivityService
-with RepositorySearchService with IssuesService
-with ReferrerAuthenticator
+ with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService
- with SystemSettingsService with ActivityService with RepositorySearchService
- with ReferrerAuthenticator =>
+ with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
val searchForm = mapping(
"query" -> trim(text(required)),
diff --git a/src/main/scala/app/SignInController.scala b/src/main/scala/app/SignInController.scala
deleted file mode 100644
index b6a43f0..0000000
--- a/src/main/scala/app/SignInController.scala
+++ /dev/null
@@ -1,58 +0,0 @@
-package app
-
-import service._
-import jp.sf.amateras.scalatra.forms._
-import util.Implicits._
-import util.StringUtil._
-import util.Keys
-
-class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
-
-trait SignInControllerBase extends ControllerBase { self: SystemSettingsService with AccountService =>
-
- case class SignInForm(userName: String, password: String)
-
- val form = mapping(
- "userName" -> trim(label("Username", text(required))),
- "password" -> trim(label("Password", text(required)))
- )(SignInForm.apply)
-
- get("/signin"){
- val redirect = params.get("redirect")
- if(redirect.isDefined && redirect.get.startsWith("/")){
- session.setAttribute(Keys.Session.Redirect, redirect.get)
- }
- html.signin(loadSystemSettings())
- }
-
- post("/signin", form){ form =>
- authenticate(loadSystemSettings(), form.userName, form.password) match {
- case Some(account) => signin(account)
- case None => redirect("/signin")
- }
- }
-
- get("/signout"){
- session.invalidate
- redirect("/")
- }
-
- /**
- * Set account information into HttpSession and redirect.
- */
- private def signin(account: model.Account) = {
- session.setAttribute(Keys.Session.LoginAccount, account)
- updateLastLoginDate(account.userName)
-
- session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
- if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
- redirect("/")
- } else {
- redirect(redirectUrl)
- }
- }.getOrElse {
- redirect("/")
- }
- }
-
-}
\ No newline at end of file
diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala
index 68efda5..08231b6 100644
--- a/src/main/scala/app/SystemSettingsController.scala
+++ b/src/main/scala/app/SystemSettingsController.scala
@@ -13,6 +13,7 @@
self: SystemSettingsService with AccountService with AdminAuthenticator =>
private val form = mapping(
+ "baseUrl" -> trim(label("Base URL", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
@@ -33,6 +34,7 @@
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
+ "fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", text(required))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
diff --git a/src/main/scala/app/WikiController.scala b/src/main/scala/app/WikiController.scala
index aaa88a8..704ce20 100644
--- a/src/main/scala/app/WikiController.scala
+++ b/src/main/scala/app/WikiController.scala
@@ -188,16 +188,16 @@
private def conflictForNew: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
- optionIf(targetWikiPage.nonEmpty){
- Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.")
+ targetWikiPage.map { _ =>
+ "Someone has created the wiki since you started. Please reload this page and re-apply your changes."
}
}
}
private def conflictForEdit: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
- optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(false)){
- Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.")
+ targetWikiPage.filter(_.id != params("id")).map{ _ =>
+ "Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
}
}
}
diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala
index 905736b..84e78a1 100644
--- a/src/main/scala/service/AccountService.scala
+++ b/src/main/scala/service/AccountService.scala
@@ -36,11 +36,15 @@
*/
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
- case Right(mailAddress) => {
+ case Right(ldapUserInfo) => {
// Create or update account by LDAP information
- getAccountByUserName(userName) match {
- case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
- case None => createAccount(userName, "", userName, mailAddress, false, None)
+ getAccountByUserName(userName, true) match {
+ case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
+ case Some(x) if(x.isRemoved) => {
+ logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..")
+ defaultAuthentication(userName, password)
+ }
+ case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
}
getAccountByUserName(userName)
}
diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala
index ec6f936..81a9bf8 100644
--- a/src/main/scala/service/IssuesService.scala
+++ b/src/main/scala/service/IssuesService.scala
@@ -343,7 +343,7 @@
def toURL: String =
"?" + List(
- if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
+ if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
milestoneId.map { id => "milestone=" + (id match {
case Some(x) => x.toString
case None => "none"
@@ -364,7 +364,7 @@
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition(
- param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
+ param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map{
case "none" => None
case x => x.toIntOpt
diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala
index 14172c1..74bd139 100644
--- a/src/main/scala/service/RepositoryService.scala
+++ b/src/main/scala/service/RepositoryService.scala
@@ -39,6 +39,67 @@
IssueId insert (userName, repositoryName, 0)
}
+ def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String): Unit = {
+ (Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
+ Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
+
+ val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val commitLog = Query(CommitLog ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
+
+ Repositories.filter { t =>
+ (t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
+ }.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
+
+ Repositories.filter { t =>
+ (t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
+ }.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
+
+ PullRequests.filter { t =>
+ t.requestRepositoryName is oldRepositoryName.bind
+ }.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName)
+
+ deleteRepository(oldUserName, oldRepositoryName)
+
+ WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
+ Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ Collaborators .insertAll(collaborators .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ CommitLog .insertAll(commitLog .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
+ Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+
+ // Update activity messages
+ val updateActivities = Activities.filter { t =>
+ (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
+ (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
+ }.map { t => t.activityId ~ t.message }.list
+
+ updateActivities.foreach { case (activityId, message) =>
+ Activities.filter(_.activityId is activityId.bind).map(_.message).update(
+ message
+ .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
+ .replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
+ .replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
+ .replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
+ .replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
+ )
+ }
+ }
+ }
+
def deleteRepository(userName: String, repositoryName: String): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
@@ -207,11 +268,11 @@
}.length).first
- def getForkedRepositories(userName: String, repositoryName: String): List[String] =
+ def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] =
Query(Repositories).filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}
- .sortBy(_.userName asc).map(_.userName).list
+ .sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list
}
diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala
index 85e94f7..de7a966 100644
--- a/src/main/scala/service/SystemSettingsService.scala
+++ b/src/main/scala/service/SystemSettingsService.scala
@@ -8,6 +8,7 @@
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props =>
+ settings.baseUrl.foreach(props.setProperty(BaseURL, _))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
@@ -31,6 +32,7 @@
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
props.setProperty(LdapBaseDN, ldap.baseDN)
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
+ ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
@@ -47,6 +49,7 @@
props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(
+ getOptionValue(props, BaseURL, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
@@ -71,6 +74,7 @@
getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""),
+ getOptionValue(props, LdapFullNameAttribute, None),
getValue(props, LdapMailAddressAttribute, ""),
getOptionValue[Boolean](props, LdapTls, None),
getOptionValue(props, LdapKeystore, None)))
@@ -87,6 +91,7 @@
import scala.reflect.ClassTag
case class SystemSettings(
+ baseUrl: Option[String],
allowAccountRegistration: Boolean,
gravatar: Boolean,
notification: Boolean,
@@ -101,6 +106,7 @@
bindPassword: Option[String],
baseDN: String,
userNameAttribute: String,
+ fullNameAttribute: Option[String],
mailAttribute: String,
tls: Option[Boolean],
keystore: Option[String])
@@ -117,6 +123,7 @@
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
+ private val BaseURL = "base_url"
private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar"
private val Notification = "notification"
@@ -134,6 +141,7 @@
private val LdapBindPassword = "ldap.bind_password"
private val LdapBaseDN = "ldap.baseDN"
private val LdapUserNameAttribute = "ldap.username_attribute"
+ private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val LdapTls = "ldap.tls"
private val LdapKeystore = "ldap.keystore"
diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala
index 4b8a05c..00fbabd 100644
--- a/src/main/scala/service/WikiService.scala
+++ b/src/main/scala/service/WikiService.scala
@@ -3,7 +3,7 @@
import java.util.Date
import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils
-import util.{PatchUtil, Directory, JGitUtil, LockUtil}
+import util._
import _root_.util.ControlUtil._
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
import org.eclipse.jgit.lib._
@@ -14,6 +14,7 @@
import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._
+import scala.Some
object WikiService {
@@ -59,11 +60,12 @@
*/
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
- optionIf(!JGitUtil.isEmpty(git)){
+ if(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
- WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId)
+ WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
+ file.committer, file.time, file.commitId)
}
- }
+ } else None
}
}
@@ -72,7 +74,7 @@
*/
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
- optionIf(!JGitUtil.isEmpty(git)){
+ if(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index)
val fileName = if(index < 0) path else path.substring(index + 1)
@@ -80,7 +82,7 @@
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes
}
- }
+ } else None
}
/**
@@ -239,7 +241,7 @@
}
}
- optionIf(created || updated || removed){
+ if(created || updated || removed){
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
@@ -256,7 +258,7 @@
})
Some(newHeadId)
- }
+ } else None
}
}
}
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
index e38966d..bfa6992 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/AutoUpdateListener.scala
@@ -50,6 +50,9 @@
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
+ Version(1, 11),
+ Version(1, 10),
+ Version(1, 9),
Version(1, 8),
Version(1, 7),
Version(1, 6),
@@ -120,7 +123,7 @@
System.setProperty("gitbucket.home", datadir)
}
org.h2.Driver.load()
- event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
+ event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
logger.debug("Start schema update")
defining(getConnection(event.getServletContext)){ conn =>
diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala
index 4c02070..8c9a3b6 100644
--- a/src/main/scala/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/servlet/GitRepositoryServlet.scala
@@ -9,7 +9,7 @@
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
-import util.{Keys, JGitUtil, Directory}
+import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
import service._
@@ -56,10 +56,10 @@
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db)
- val userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
+ val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI)
- logger.debug("userName:" + userName)
+ logger.debug("pusher:" + pusher)
defining(request.paths){ paths =>
val owner = paths(1)
@@ -69,7 +69,9 @@
logger.debug("repository:" + owner + "/" + repository)
logger.debug("baseURL:" + baseURL)
- receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL))
+ if(!repository.endsWith(".wiki")){
+ receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL))
+ }
receivePack
}
}
@@ -77,101 +79,107 @@
import scala.collection.JavaConverters._
-class CommitLogHook(owner: String, repository: String, userName: 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])
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
- using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
- commands.asScala.foreach { command =>
- logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
- val commits = command.getType match {
- case ReceiveCommand.Type.DELETE => Nil
- case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
- }
- val refName = command.getRefName.split("/")
- val branchName = refName.drop(2).mkString("/")
+ try {
+ using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
+ commands.asScala.foreach { command =>
+ logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
+ val commits = command.getType match {
+ case ReceiveCommand.Type.DELETE => Nil
+ case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
+ }
+ val refName = command.getRefName.split("/")
+ val branchName = refName.drop(2).mkString("/")
- // Extract new commit and apply issue comment
- val newCommits = if(commits.size > 1000){
- val existIds = getAllCommitIds(owner, repository)
- commits.flatMap { commit =>
- optionIf(!existIds.contains(commit.id)){
- createIssueComment(commit)
- Some(commit)
+ // Extract new commit and apply issue comment
+ val newCommits = if(commits.size > 1000){
+ val existIds = getAllCommitIds(owner, repository)
+ commits.flatMap { commit =>
+ if(!existIds.contains(commit.id)){
+ createIssueComment(commit)
+ Some(commit)
+ } else None
+ }
+ } else {
+ commits.flatMap { commit =>
+ if(!existsCommitId(owner, repository, commit.id)){
+ createIssueComment(commit)
+ Some(commit)
+ } else None
}
}
- } else {
- commits.flatMap { commit =>
- optionIf(!existsCommitId(owner, repository, commit.id)){
- createIssueComment(commit)
- Some(commit)
+
+ // batch insert all new commit id
+ insertAllCommitIds(owner, repository, newCommits.map(_.id))
+
+ // record activity
+ if(refName(1) == "heads"){
+ command.getType match {
+ case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
+ case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
+ case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
+ case _ =>
+ }
+ } else if(refName(1) == "tags"){
+ command.getType match {
+ case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
+ case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
+ case _ =>
}
}
- }
- // batch insert all new commit id
- insertAllCommitIds(owner, repository, newCommits.map(_.id))
+ if(refName(1) == "heads"){
+ command.getType match {
+ case ReceiveCommand.Type.CREATE |
+ ReceiveCommand.Type.UPDATE |
+ ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
+ updatePullRequests(branchName)
+ case _ =>
+ }
+ }
- // record activity
- if(refName(1) == "heads"){
- command.getType match {
- case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, userName, branchName)
- case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, branchName, newCommits)
- case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, userName, branchName)
+ // 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)){
+ callWebHook(owner, repository, webHookURLs,
+ WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
+ }
case _ =>
}
- } else if(refName(1) == "tags"){
- command.getType match {
- case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, branchName, newCommits)
- case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, userName, branchName, newCommits)
- case _ =>
- }
- }
-
- if(refName(1) == "heads"){
- command.getType match {
- case ReceiveCommand.Type.CREATE |
- ReceiveCommand.Type.UPDATE |
- ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
- updatePullRequests(branchName)
- case _ =>
- }
- }
-
- // 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, userName, owner, repository)
- }
- }
-
- // call web hook
- val webHookURLs = getWebHookURLs(owner, repository)
- if(webHookURLs.nonEmpty){
- val payload = WebHookPayload(
- git,
- getAccountByUserName(userName).get,
- command.getRefName,
- getRepository(owner, repository, baseURL).get,
- newCommits,
- getAccountByUserName(owner).get)
-
- callWebHook(owner, repository, webHookURLs, payload)
}
}
+ // update repository last modified time.
+ updateLastActivityDate(owner, repository)
+ } catch {
+ case ex: Exception => {
+ logger.error(ex.toString, ex)
+ throw ex
+ }
}
- // update repository last modified time.
- updateLastActivityDate(owner, repository)
}
private def createIssueComment(commit: CommitInfo) = {
- "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
- val issueId = matchData.group(2)
- if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
- createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
+ StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
+ if(getIssue(owner, repository, issueId).isDefined){
+ getAccountByMailAddress(commit.mailAddress).foreach { account =>
+ createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
+ }
}
}
}
diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala
index 3e3fd5e..02d7fd1 100644
--- a/src/main/scala/util/ControlUtil.scala
+++ b/src/main/scala/util/ControlUtil.scala
@@ -4,6 +4,7 @@
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import org.eclipse.jgit.transport.RefSpec
+import scala.language.reflectiveCalls
/**
* Provides control facilities.
@@ -24,12 +25,12 @@
}
def using[T](git: Git)(f: Git => T): T =
- try f(git) finally git.getRepository.close
+ try f(git) finally git.getRepository.close()
def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
try f(git1, git2) finally {
- git1.getRepository.close
- git2.getRepository.close
+ git1.getRepository.close()
+ git2.getRepository.close()
}
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
@@ -39,22 +40,14 @@
try f(treeWalk) finally treeWalk.release()
- def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
- try {
- f(ref)
- } finally {
- val refUpdate = git.getRepository.updateRef(ref.getDestination)
- refUpdate.setForceUpdate(true)
- refUpdate.delete()
- }
- }
+// def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
+// try {
+// f(ref)
+// } finally {
+// val refUpdate = git.getRepository.updateRef(ref.getDestination)
+// refUpdate.setForceUpdate(true)
+// refUpdate.delete()
+// }
+// }
- def executeIf(condition: => Boolean)(action: => Unit): Boolean =
- if(condition){
- action
- true
- } else false
-
- def optionIf[T](condition: => Boolean)(action: => Option[T]): Option[T] =
- if(condition) action else None
}
diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala
index 8ea3325..d37577a 100644
--- a/src/main/scala/util/Directory.scala
+++ b/src/main/scala/util/Directory.scala
@@ -2,6 +2,7 @@
import java.io.File
import util.ControlUtil._
+import org.apache.commons.io.FileUtils
/**
* Provides directories used by GitBucket.
@@ -14,8 +15,16 @@
case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
// environment variable GITBUCKET_HOME
case Some(env) => new File(env)
- // default is HOME/gitbucket
- case None => new File(System.getProperty("user.home"), "gitbucket")
+ // default is HOME/.gitbucket
+ case None => {
+ val oldHome = new File(System.getProperty("user.home"), "gitbucket")
+ if(oldHome.exists && oldHome.isDirectory && new File(oldHome, "version").exists){
+ //FileUtils.moveDirectory(oldHome, newHome)
+ oldHome
+ } else {
+ new File(System.getProperty("user.home"), ".gitbucket")
+ }
+ }
}
}).getAbsolutePath
diff --git a/src/main/scala/util/FileUtil.scala b/src/main/scala/util/FileUtil.scala
index 0b1a98b..e4c052f 100644
--- a/src/main/scala/util/FileUtil.scala
+++ b/src/main/scala/util/FileUtil.scala
@@ -63,9 +63,9 @@
if(dir.exists()){
FileUtils.deleteDirectory(dir)
}
- try{
+ try {
action(dir)
- }finally{
+ } finally {
FileUtils.deleteDirectory(dir)
}
}
diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala
index 9286362..742641e 100644
--- a/src/main/scala/util/JGitUtil.scala
+++ b/src/main/scala/util/JGitUtil.scala
@@ -79,9 +79,9 @@
}
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
- optionIf(i >= 0){
+ if(i >= 0){
Some(fullMessage.trim.substring(i).trim)
- }
+ } else None
}
}
diff --git a/src/main/scala/util/LDAPUtil.scala b/src/main/scala/util/LDAPUtil.scala
index 3f19b28..05a7171 100644
--- a/src/main/scala/util/LDAPUtil.scala
+++ b/src/main/scala/util/LDAPUtil.scala
@@ -18,51 +18,49 @@
/**
* Try authentication by LDAP using given configuration.
- * Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage).
+ * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
*/
- def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = {
+ def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind(
- ldapSettings.host,
- ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
- ldapSettings.bindDN.getOrElse(""),
- ldapSettings.bindPassword.getOrElse(""),
- ldapSettings.tls.getOrElse(false),
- ldapSettings.keystore.getOrElse("")
- ) match {
- case Some(conn) => {
- withConnection(conn) { conn =>
- findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
- case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
- case None => Left("User does not exist.")
- }
- }
+ host = ldapSettings.host,
+ port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
+ dn = ldapSettings.bindDN.getOrElse(""),
+ password = ldapSettings.bindPassword.getOrElse(""),
+ tls = ldapSettings.tls.getOrElse(false),
+ keystore = ldapSettings.keystore.getOrElse(""),
+ error = "System LDAP authentication failed."
+ ){ conn =>
+ findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
+ case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
+ case None => Left("User does not exist.")
}
- case None => Left("System LDAP authentication failed.")
}
}
- private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = {
+ private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind(
- ldapSettings.host,
- ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
- userDN,
- password,
- ldapSettings.tls.getOrElse(false),
- ldapSettings.keystore.getOrElse("")
- ) match {
- case Some(conn) => {
- withConnection(conn) { conn =>
- findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
- case Some(mailAddress) => Right(mailAddress)
- case None => Left("Can't find mail address.")
- }
- }
+ host = ldapSettings.host,
+ port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
+ dn = userDN,
+ password = password,
+ tls = ldapSettings.tls.getOrElse(false),
+ keystore = ldapSettings.keystore.getOrElse(""),
+ error = "User LDAP Authentication Failed."
+ ){ conn =>
+ findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
+ case Some(mailAddress) => Right(LDAPUserInfo(
+ userName = userName,
+ fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
+ findFullName(conn, userDN, fullNameAttribute)
+ }.getOrElse(userName),
+ mailAddress = mailAddress))
+ case None => Left("Can't find mail address.")
}
- case None => Left("User LDAP Authentication Failed.")
}
}
- private def bind(host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String): Option[LDAPConnection] = {
+ private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
+ (f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) {
// Dynamically set Sun as the security provider
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider())
@@ -87,7 +85,9 @@
// Bind to the server
conn.bind(LDAP_VERSION, dn, password.getBytes)
- Some(conn)
+ // Execute a given function and returns a its result
+ f(conn)
+
} catch {
case e: Exception => {
// Provide more information if something goes wrong
@@ -96,20 +96,15 @@
if (conn.isConnected) {
conn.disconnect()
}
-
- None
+ // Returns an error message
+ Left(error)
}
}
}
- private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = {
- try {
- f(conn)
- } finally {
- conn.disconnect()
- }
- }
-
+ /**
+ * Search a specified user and returns userDN if exists.
+ */
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
@tailrec
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
@@ -130,8 +125,18 @@
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
- optionIf (results.hasMore) {
+ if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
- }
+ } else None
}
+
+ private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] =
+ defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results =>
+ if(results.hasMore) {
+ Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
+ } else None
+ }
+
+ case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String)
+
}
diff --git a/src/main/scala/util/StringUtil.scala b/src/main/scala/util/StringUtil.scala
index 61c5c18..7de2a62 100644
--- a/src/main/scala/util/StringUtil.scala
+++ b/src/main/scala/util/StringUtil.scala
@@ -3,6 +3,8 @@
import java.net.{URLDecoder, URLEncoder}
import org.mozilla.universalchardet.UniversalDetector
import util.ControlUtil._
+import org.apache.commons.io.input.BOMInputStream
+import org.apache.commons.io.IOUtils
object StringUtil {
@@ -27,7 +29,12 @@
def escapeHtml(value: String): String =
value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
- def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content))
+ /**
+ * 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..
+ */
+ def convertFromByteArray(content: Array[Byte]): String =
+ IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
def detectEncoding(content: Array[Byte]): String =
defining(new UniversalDetector(null)){ detector =>
@@ -38,4 +45,14 @@
case e => e
}
}
+
+ /**
+ * 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) }
+
}
diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala
index 6def4df..6b7ab2d 100644
--- a/src/main/scala/view/Markdown.scala
+++ b/src/main/scala/view/Markdown.scala
@@ -7,6 +7,8 @@
import org.pegdown._
import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
+import java.text.Normalizer
+import java.util.Locale
import scala.collection.JavaConverters._
import service.{RequestCache, WikiService}
@@ -110,6 +112,21 @@
printer.print(' ').print(name).print('=').print('"').print(value).print('"')
}
+ private def printHeaderTag(node: HeaderNode): Unit = {
+ val tag = s"h${node.getLevel}"
+ val headerTextString = printChildrenToString(node)
+ val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
+ printer.print(s"""<$tag class="markdown-head">""")
+ printer.print(s"""""")
+ printer.print(s"""""")
+ visitChildren(node)
+ printer.print(s"$tag>")
+ }
+
+ override def visit(node: HeaderNode): Unit = {
+ printHeaderTag(node)
+ }
+
override def visit(node: TextNode): Unit = {
// convert commit id and username to link.
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
@@ -120,5 +137,16 @@
printWithAbbreviations(text)
}
}
+}
+object GitBucketHtmlSerializer {
+
+ private val Whitespace = "[\\s]".r
+
+ def generateAnchorName(text: String): String = {
+ val noWhitespace = Whitespace.replaceAllIn(text, "-")
+ val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD)
+ val noSpecialChars = StringUtil.urlEncode(normalized)
+ noSpecialChars.toLowerCase(Locale.ENGLISH)
+ }
}
diff --git a/src/main/twirl/admin/system.scala.html b/src/main/twirl/admin/system.scala.html
index 9a02cb5..6594e29 100644
--- a/src/main/twirl/admin/system.scala.html
+++ b/src/main/twirl/admin/system.scala.html
@@ -1,5 +1,6 @@
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
@import context._
+@import util.Directory._
@import view.helpers._
@html.main("System Settings"){
@menu("system"){
@@ -9,8 +10,29 @@
System Settings
+
+
+
+ @GitBucketHome
+
+
+
+
+
+
+
+ The base URL is used for redirect, notification email, git repository URL box and more.
+ If the base URL is empty, GitBucket generates URL from request information.
+ You can use this property to adjust URL difference between the reverse proxy and GitBucket.
+