diff --git a/README.md b/README.md
index 8b93b51..1c211e1 100644
--- a/README.md
+++ b/README.md
@@ -10,17 +10,17 @@
- Repository search (Code and Issues)
- Wiki
- Issues
+- Fork / Pull request
+- Mail notification
- Activity timeline
- User management (for Administrators)
- Group (like Organization in Github)
Following features are not implemented, but we will make them in the future release!
-- Fork and pull request
- Network graph
- Statics
- Watch / Star
-- Notification
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -37,7 +37,7 @@
Release Notes
--------
-### 1.5 - COMMING SOON!
+### 1.5 - 4 Sep 2013
- Fork and pull request.
- LDAP authentication.
- Mail notification.
@@ -45,6 +45,7 @@
- Add the branch tab in the repository viewer.
- Encoding auto detection for the file content in the repository viewer.
- Add favicon, header logo and icons for the timeline.
+- Specify data directory via environment variable GITBUCKET_HOME.
- Fixed some bugs.
### 1.4 - 31 Jul 2013
diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala
index 8d6b21c..11c0d8b 100644
--- a/src/main/scala/app/ControllerBase.scala
+++ b/src/main/scala/app/ControllerBase.scala
@@ -1,7 +1,7 @@
package app
import _root_.util.Directory._
-import _root_.util.{StringUtil, FileUtil, Validations}
+import _root_.util.{FileUtil, Validations}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
@@ -22,9 +22,8 @@
implicit val jsonFormats = DefaultFormats
-// before() {
-// contentType = "text/html"
-// }
+ // Don't set content type via Accept header.
+ override def format(implicit request: HttpServletRequest) = ""
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val httpRequest = request.asInstanceOf[HttpServletRequest]
diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala
index fab8b75..adee1e8 100644
--- a/src/main/scala/app/IssuesController.scala
+++ b/src/main/scala/app/IssuesController.scala
@@ -4,7 +4,7 @@
import service._
import IssuesService._
-import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator}
+import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier}
import org.scalatra.Ok
class IssuesController extends IssuesControllerBase
@@ -112,6 +112,11 @@
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
+ // notifications
+ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
+ Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
+ }
+
redirect(s"/${owner}/${name}/issues/${issueId}")
})
@@ -129,21 +134,15 @@
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
- if(issue.isPullRequest){
- redirect(s"/${repository.owner}/${repository.name}/pull/${form.issueId}#comment-${id}")
- } else {
- redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
- }
+ redirect(s"/${repository.owner}/${repository.name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
- if(issue.isPullRequest){
- redirect(s"/${repository.owner}/${repository.name}/pull/${form.issueId}#comment-${id}")
- } else {
- redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
- }
+ redirect(s"/${repository.owner}/${repository.name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
@@ -282,9 +281,10 @@
val (action, recordActivity) =
getAction(issue)
.collect {
- case "close" if(issue.isPullRequest) => true -> (Some("close") -> Some(recordClosePullRequestActivity _))
- case "close" if(!issue.isPullRequest) => true -> (Some("close") -> Some(recordCloseIssueActivity _))
- case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _))
+ case "close" => true -> (Some("close") ->
+ Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
+ case "reopen" => false -> (Some("reopen") ->
+ Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
@@ -300,15 +300,29 @@
}
// record activity
- content foreach { content =>
- if(issue.isPullRequest)
- recordCommentPullRequestActivity(owner, name, userName, issueId, content)
- else
- recordCommentIssueActivity(owner, name, userName, issueId, content)
+ content foreach {
+ (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
+ (owner, name, userName, issueId, _)
}
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
- (issue, commentId)
+ // notifications
+ Notifier() match {
+ case f =>
+ content foreach {
+ f.toNotify(repository, issueId, _){
+ Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
+ }
+ }
+ action foreach {
+ f.toNotify(repository, issueId, _){
+ Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
+ }
+ }
+ }
+
+ issue -> commentId
}
}
diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala
index 0cd143f..3a287f3 100644
--- a/src/main/scala/app/PullRequestsController.scala
+++ b/src/main/scala/app/PullRequestsController.scala
@@ -1,6 +1,6 @@
package app
-import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator}
+import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier}
import util.Directory._
import util.Implicits._
import service._
@@ -100,7 +100,7 @@
getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) =>
val remote = getRepositoryDir(repository.owner, repository.name)
val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}")
- val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).call
+ val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
try {
// mark issue as merged and close.
@@ -155,6 +155,11 @@
}
}
+ // notifications
+ Notifier().toNotify(repository, issueId, "merge"){
+ Notifier.msgStatus(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
+ }
+
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} finally {
@@ -179,7 +184,7 @@
FileUtils.deleteDirectory(tmpdir)
}
- val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).call
+ val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call
try {
git.checkout.setName(branch).call
@@ -303,8 +308,14 @@
.call
}
+ // record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
+ // notifications
+ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
+ Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
+ }
+
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
})
diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala
index 40c71eb..6d9d2ad 100644
--- a/src/main/scala/service/SystemSettingsService.scala
+++ b/src/main/scala/service/SystemSettingsService.scala
@@ -47,7 +47,7 @@
if(getValue(props, Notification, false)){
Some(Smtp(
getValue(props, SmtpHost, ""),
- getOptionValue(props, SmtpPort, Some(25)),
+ getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SmtpUser, None),
getOptionValue(props, SmtpPassword, None),
getOptionValue[Boolean](props, SmtpSsl, None)))
@@ -99,6 +99,7 @@
password: Option[String],
ssl: Option[Boolean])
+ val DefaultSmtpPort = 25
val DefaultLdapPort = 389
private val AllowAccountRegistration = "allow_account_registration"
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
index 4cda144..dd6dd6e 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/AutoUpdateListener.scala
@@ -113,6 +113,7 @@
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
override def contextInitialized(event: ServletContextEvent): Unit = {
+ event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${Directory.DatabaseHome}")
super.contextInitialized(event)
logger.debug("H2 started")
diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala
index 94c432b..eac94c4 100644
--- a/src/main/scala/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/servlet/GitRepositoryServlet.scala
@@ -24,21 +24,9 @@
override def init(config: ServletConfig): Unit = {
setReceivePackFactory(new GitBucketReceivePackFactory())
-
- // TODO are there any other ways...?
- super.init(new ServletConfig(){
- def getInitParameter(name: String): String = name match {
- case "base-path" => Directory.RepositoryHome
- case "export-all" => "true"
- case name => config.getInitParameter(name)
- }
- def getInitParameterNames(): java.util.Enumeration[String] = {
- config.getInitParameterNames
- }
-
- def getServletContext(): ServletContext = config.getServletContext
- def getServletName(): String = config.getServletName
- });
+ config.getServletContext.setInitParameter("base-path", Directory.RepositoryHome)
+ config.getServletContext.setInitParameter("export-all", "true")
+ super.init(config)
}
}
diff --git a/src/main/scala/servlet/TransactionFilter.scala b/src/main/scala/servlet/TransactionFilter.scala
index 5a26734..0e96273 100644
--- a/src/main/scala/servlet/TransactionFilter.scala
+++ b/src/main/scala/servlet/TransactionFilter.scala
@@ -3,7 +3,6 @@
import javax.servlet._
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest
-import scala.slick.session.Database
/**
* Controls the transaction with the open session in view pattern.
@@ -21,15 +20,19 @@
// assets don't need transaction
chain.doFilter(req, res)
} else {
- val context = req.getServletContext
- Database.forURL(context.getInitParameter("db.url"),
- context.getInitParameter("db.user"),
- context.getInitParameter("db.password")) withTransaction {
+ Database(req.getServletContext) withTransaction {
logger.debug("TODO begin transaction")
chain.doFilter(req, res)
logger.debug("TODO end transaction")
}
}
}
-
-}
\ No newline at end of file
+
+}
+
+object Database {
+ def apply(context: ServletContext): scala.slick.session.Database =
+ scala.slick.session.Database.forURL(context.getInitParameter("db.url"),
+ context.getInitParameter("db.user"),
+ context.getInitParameter("db.password"))
+}
diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala
index 754d737..73cee55 100644
--- a/src/main/scala/util/Directory.scala
+++ b/src/main/scala/util/Directory.scala
@@ -7,11 +7,16 @@
*/
object Directory {
- val GitBucketHome = new File(System.getProperty("user.home"), "gitbucket").getAbsolutePath
+ val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
+ case Some(env) => new File(env)
+ case None => new File(System.getProperty("user.home"), "gitbucket")
+ }).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = s"${GitBucketHome}/repositories"
+
+ val DatabaseHome = s"${GitBucketHome}/data"
/**
* Repository names of the specified user.
diff --git a/src/main/scala/util/Notifier.scala b/src/main/scala/util/Notifier.scala
index dab8eb3..4fc522f 100644
--- a/src/main/scala/util/Notifier.scala
+++ b/src/main/scala/util/Notifier.scala
@@ -1,37 +1,104 @@
package util
-import org.apache.commons.mail.{DefaultAuthenticator, SimpleEmail}
+import scala.concurrent._
+import ExecutionContext.Implicits.global
+import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
+import org.slf4j.LoggerFactory
-import service.SystemSettingsService.{SystemSettings, Smtp}
+import app.Context
+import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
+import servlet.Database
+import SystemSettingsService.Smtp
-trait Notifier {
+trait Notifier extends RepositoryService with AccountService with IssuesService {
+ def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
+ (msg: String => String)(implicit context: Context): Unit
+
+ protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) =
+ (
+ // individual repository's owner
+ issue.userName ::
+ // collaborators
+ getCollaborators(issue.userName, issue.repositoryName) :::
+ // participants
+ issue.openedUserName ::
+ getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
+ )
+ .distinct
+ .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
+ .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
}
object Notifier {
- def apply(settings: SystemSettings) = {
- new Mailer(settings.smtp.get)
+ // TODO We want to be able to switch to mock.
+ def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
+ case settings if settings.notification => new Mailer(settings.smtp.get)
+ case _ => new MockMailer
}
+ def msgIssue(url: String) = (content: String) => s"""
+ |${content}
+ |--
+ |View it on GitBucket
+ """.stripMargin
+
+ def msgPullRequest(url: String) = (content: String) => s"""
+ |${content}