Compare commits

...

7 Commits

Author SHA1 Message Date
Naoki Takezoe
1a4961c3e1 Merge pull request #2489 from gitbucket/release-4.34.0
GitBucket 4.34.0 release
2020-07-26 02:38:51 +09:00
Naoki Takezoe
561220237f Update README and CHANGELOG 2020-07-26 02:28:47 +09:00
Naoki Takezoe
f45b85aa71 Bump gist-plugin (refs #2443) and notifications-plugin (refs #2433) 2020-07-25 21:04:45 +09:00
Naoki Takezoe
83b3a7983e Update version to 4.34.0 2020-07-25 17:52:33 +09:00
Naoki Takezoe
63d4c5054e Deny empty password (#2488) 2020-07-25 17:49:58 +09:00
Naoki Takezoe
c8f6017be9 Migrate activity log to JSON-based file from database (#2468)
This also includes:
- Record rename and transfer repository in activity log
- Check existence of repsoitory when rendering activity logs
- Drop Akka dependency
2020-07-18 23:28:40 +09:00
Naoki Takezoe
f9fcb54861 Fix coursier cache path (#2481) 2020-07-18 17:17:41 +09:00
35 changed files with 788 additions and 545 deletions

View File

@@ -22,7 +22,7 @@ jobs:
path: |
~/.ivy2/cache
~/.sbt
~/.coursier
~/.cache/coursier/v1
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
- name: Set up JDK
uses: actions/setup-java@v1

View File

@@ -1,6 +1,22 @@
# Changelog
All changes to the project will be documented in this file.
### 4.34.0 - 26 Jul 2020
- Enhancement admin settings UI
- File upload settings
- Restrict repository operations
- User-defined CSS
- Limit the repository list in the sidebar
- Improve MariaDB support
- Improve activity logging
- CLI option to persist session on disk in the standalone mode
- Web API updates
- Add [list commits API](https://developer.github.com/v3/repos/commits/#list-commits)
- Bundled plugins updates
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) 4.18.0 -> 4.19.0
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin) 1.8.0 -> 1.9.0
### 4.33.0 - 31 Dec 2019
- All CLI options are configurable by environment variables

View File

@@ -57,11 +57,20 @@ Support
What's New in 4.33.x
-------------
### 4.33.0 - 31 Dec 2019
### 4.34.0 - 26 Jul 2020
- All CLI options are configurable by environment variables
- Folding pull request files
- WebHook security options
- Add assignee and assignees properties to some Web APIs' response
- Enhancement admin settings UI
- File upload settings
- Restrict repository operations
- User-defined CSS
- Limit the repository list in the sidebar
- Improve MariaDB support
- Improve activity logging
- CLI option to persist session on disk in the standalone mode
- Web API updates
- Add [list commits API](https://developer.github.com/v3/repos/commits/#list-commits)
- Bundled plugins updates
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) 4.18.0 -> 4.19.0
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin) 1.8.0 -> 1.9.0
See the [change log](CHANGELOG.md) for all of the updates.

View File

@@ -3,7 +3,7 @@ import com.typesafe.sbt.pgp.PgpKeys._
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.33.0"
val GitBucketVersion = "4.34.0"
val ScalatraVersion = "2.7.0-RC1"
val JettyVersion = "9.4.30.v20200611"
val JgitVersion = "5.8.0.202006091008-r"
@@ -54,11 +54,9 @@ libraryDependencies ++= Seq(
"ch.qos.logback" % "logback-classic" % "1.2.3",
"com.zaxxer" % "HikariCP" % "3.4.5",
"com.typesafe" % "config" % "1.4.0",
"com.typesafe.akka" %% "akka-actor" % "2.5.27",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"com.github.bkromhout" % "java-diff-utils" % "2.1.1",
"org.cache2k" % "cache2k-all" % "1.2.4.Final",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.8.1-akka-2.5.x" exclude ("com.mchange", "c3p0") exclude ("com.zaxxer", "HikariCP-java6"),
"net.coobird" % "thumbnailator" % "0.4.11",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "5.64.4",

View File

@@ -1,4 +1,4 @@
notifications:1.8.0
gist:4.18.0
notifications:1.9.0
gist:4.19.0
emoji:4.6.0
pages:1.8.0

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<dropTable tableName="ACTIVITY" />
</changeSet>

View File

@@ -1,7 +1,22 @@
package gitbucket.core
import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration}
import io.github.gitbucket.solidbase.model.{Version, Module}
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import java.sql.Connection
import java.util
import java.util.UUID
import gitbucket.core.model.Activity
import gitbucket.core.util.Directory.ActivityLog
import gitbucket.core.util.JDBCUtil
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration, SqlMigration}
import io.github.gitbucket.solidbase.model.{Module, Version}
import org.json4s.NoTypeHints
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write
import scala.util.Using
object GitBucketCoreModule
extends Module(
@@ -65,5 +80,38 @@ object GitBucketCoreModule
new Version("4.31.1"),
new Version("4.31.2"),
new Version("4.32.0", new LiquibaseMigration("update/gitbucket-core_4.32.xml")),
new Version("4.33.0")
new Version("4.33.0"),
new Version(
"4.34.0",
new Migration() {
override def migrate(moduleId: String, version: String, context: util.Map[String, AnyRef]): Unit = {
implicit val formats = Serialization.formats(NoTypeHints)
import JDBCUtil._
val conn = context.get(Solidbase.CONNECTION).asInstanceOf[Connection]
val list = conn.select("SELECT * FROM ACTIVITY ORDER BY ACTIVITY_ID") {
rs =>
Activity(
activityId = UUID.randomUUID().toString,
userName = rs.getString("USER_NAME"),
repositoryName = rs.getString("REPOSITORY_NAME"),
activityUserName = rs.getString("ACTIVITY_USER_NAME"),
activityType = rs.getString("ACTIVITY_TYPE"),
message = rs.getString("MESSAGE"),
additionalInfo = {
val additionalInfo = rs.getString("ADDITIONAL_INFO")
if (rs.wasNull()) None else Some(additionalInfo)
},
activityDate = rs.getTimestamp("ACTIVITY_DATE")
)
}
Using.resource(new FileOutputStream(ActivityLog, true)) { out =>
list.foreach { activity =>
out.write((write(activity) + "\n").getBytes(StandardCharsets.UTF_8))
}
}
}
},
new LiquibaseMigration("update/gitbucket-core_4.34.xml")
)
)

View File

@@ -35,6 +35,7 @@ class AccountController
with WebHookService
with PrioritiesService
with RepositoryCreationService
with RequestCache
trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService

View File

@@ -52,6 +52,7 @@ class ApiController
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with WritableUsersAuthenticator
with RequestCache
trait ApiControllerBase extends ControllerBase {

View File

@@ -22,6 +22,7 @@ class DashboardController
with WebHookPullRequestReviewCommentService
with MilestonesService
with UsersAuthenticator
with RequestCache
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with AccountService with UsersAuthenticator =>

View File

@@ -29,6 +29,7 @@ class IndexController
with AccessTokenService
with AccountFederationService
with OpenIDConnectService
with RequestCache
trait IndexControllerBase extends ControllerBase {
self: RepositoryService
@@ -78,7 +79,7 @@ trait IndexControllerBase extends ControllerBase {
}
.getOrElse {
gitbucket.core.html.index(
getRecentActivities(),
getRecentPublicActivities(),
getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = false
)
@@ -161,7 +162,7 @@ trait IndexControllerBase extends ControllerBase {
get("/activities.atom") {
contentType = "application/atom+xml; type=feed"
xml.feed(getRecentActivities())
xml.feed(getRecentPublicActivities())
}
post("/sidebar-collapse") {

View File

@@ -30,6 +30,7 @@ class IssuesController
with WebHookPullRequestReviewCommentService
with CommitsService
with PrioritiesService
with RequestCache
trait IssuesControllerBase extends ControllerBase {
self: IssuesService

View File

@@ -36,6 +36,7 @@ class PullRequestsController
with MergeService
with ProtectedBranchService
with PrioritiesService
with RequestCache
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService

View File

@@ -2,7 +2,14 @@ package gitbucket.core.controller
import java.io.File
import gitbucket.core.service.{AccountService, ActivityService, PaginationHelper, ReleaseService, RepositoryService}
import gitbucket.core.service.{
AccountService,
ActivityService,
PaginationHelper,
ReleaseService,
RepositoryService,
RequestCache
}
import gitbucket.core.util._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
@@ -22,6 +29,7 @@ class ReleaseController
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with RequestCache
trait ReleaseControllerBase extends ControllerBase {
self: RepositoryService

View File

@@ -30,8 +30,10 @@ class RepositorySettingsController
with ProtectedBranchService
with CommitStatusService
with DeployKeyService
with ActivityService
with OwnerAuthenticator
with UsersAuthenticator
with RequestCache
trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService
@@ -40,6 +42,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
with ProtectedBranchService
with CommitStatusService
with DeployKeyService
with ActivityService
with OwnerAuthenticator
with UsersAuthenticator =>
@@ -97,9 +100,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"events" -> webhookEvents,
"ctype" -> label("ctype", text()),
"token" -> optional(trim(label("token", text(maxlength(100)))))
)(
(url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
)
)((url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token))
// for rename repository
case class RenameRepositoryForm(repositoryName: String)
@@ -251,9 +252,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Send the test request to registered web hook URLs.
*/
ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h =>
Array(h.getName, h.getValue)
}
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] =
h.map { h =>
Array(h.getName, h.getValue)
}
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
@@ -371,7 +373,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
if (context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin) {
if (repository.name != form.repositoryName) {
// Update database and move git repository
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
// Record activity log
recordRenameRepositoryActivity(
repository.owner,
form.repositoryName,
repository.name,
context.loginAccount.get.userName
)
}
redirect(s"/${repository.owner}/${form.repositoryName}")
} else Forbidden()
@@ -384,7 +394,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
if (context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin) {
// Change repository owner
if (repository.owner != form.newOwner) {
// Update database and move git repository
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Record activity log
recordRenameRepositoryActivity(
form.newOwner,
repository.name,
repository.owner,
context.loginAccount.get.userName
)
}
redirect(s"/${form.newOwner}/${repository.name}")
} else Forbidden()
@@ -435,32 +453,34 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/**
* Provides duplication check for web hook url.
*/
private def webHook(needExists: Boolean): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] =
if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) {
Some(if (needExists) {
"URL had not been registered yet."
private def webHook(needExists: Boolean): Constraint =
new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] =
if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) {
Some(if (needExists) {
"URL had not been registered yet."
} else {
"URL had been registered already."
})
} else {
"URL had been registered already."
})
} else {
None
}
}
private def webhookEvents = new ValueType[Set[WebHook.Event]] {
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t)
}.toSet
None
}
}
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
if (convert(name, params, messages).isEmpty) {
Seq(name -> messages("error.required").format(name))
} else {
Nil
private def webhookEvents =
new ValueType[Set[WebHook.Event]] {
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t)
}.toSet
}
}
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
if (convert(name, params, messages).isEmpty) {
Seq(name -> messages("error.required").format(name))
} else {
Nil
}
}
// /**
// * Provides Constraint to validate the collaborator name.
@@ -480,70 +500,77 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/**
* Duplicate check for the rename repository name.
*/
private def renameRepositoryName: Constraint = new Constraint() {
override def validate(
name: String,
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] = {
for {
repoName <- params.optionValue("repository") if repoName != value
userName <- params.optionValue("owner")
_ <- getRepositoryNamesOfUser(userName).find(_ == value)
} yield {
"Repository already exists."
private def renameRepositoryName: Constraint =
new Constraint() {
override def validate(
name: String,
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] = {
for {
repoName <- params.optionValue("repository") if repoName != value
userName <- params.optionValue("owner")
_ <- getRepositoryNamesOfUser(userName).find(_ == value)
} yield {
"Repository already exists."
}
}
}
}
/**
*
*/
private def featureOption: Constraint = new Constraint() {
override def validate(
name: String,
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] =
if (Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
}
private def featureOption: Constraint =
new Constraint() {
override def validate(
name: String,
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] =
if (Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
}
/**
* 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."
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."
}
}
}
}
}
}
}
}
private def mergeOptions = new ValueType[Seq[String]] {
override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = {
params.getOrElse("mergeOptions", Nil)
}
override def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = {
val mergeOptions = params.getOrElse("mergeOptions", Nil)
if (mergeOptions.isEmpty) {
Seq("mergeOptions" -> "At least one option must be enabled.")
} else if (!mergeOptions.forall(x => Seq("merge-commit", "squash", "rebase").contains(x))) {
Seq("mergeOptions" -> "mergeOptions are invalid.")
} else {
Nil
private def mergeOptions =
new ValueType[Seq[String]] {
override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = {
params.getOrElse("mergeOptions", Nil)
}
override def validate(
name: String,
params: Map[String, Seq[String]],
messages: Messages
): Seq[(String, String)] = {
val mergeOptions = params.getOrElse("mergeOptions", Nil)
if (mergeOptions.isEmpty) {
Seq("mergeOptions" -> "At least one option must be enabled.")
} else if (!mergeOptions.forall(x => Seq("merge-commit", "squash", "rebase").contains(x))) {
Seq("mergeOptions" -> "mergeOptions are invalid.")
} else {
Nil
}
}
}
}
}

View File

@@ -60,6 +60,7 @@ class RepositoryViewerController
with WebHookPullRequestService
with WebHookPullRequestReviewCommentService
with ProtectedBranchService
with RequestCache
/**
* The repository viewer.

View File

@@ -24,6 +24,7 @@ class WikiController
with WebHookService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with RequestCache
trait WikiControllerBase extends ControllerBase {
self: WikiService

View File

@@ -1,5 +1,9 @@
package gitbucket.core.model
/**
* ActivityComponent has been deprecated, but keep it for binary compatibility.
*/
@deprecated("ActivityComponent has been deprecated, but keep it for binary compatibility.", "4.34.0")
trait ActivityComponent extends TemplateComponent { self: Profile =>
import profile.api._
import self._
@@ -7,14 +11,7 @@ trait ActivityComponent extends TemplateComponent { self: Profile =>
lazy val Activities = TableQuery[Activities]
class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
val activityId = column[Int]("ACTIVITY_ID", O AutoInc)
val activityUserName = column[String]("ACTIVITY_USER_NAME")
val activityType = column[String]("ACTIVITY_TYPE")
val message = column[String]("MESSAGE")
val additionalInfo = column[String]("ADDITIONAL_INFO")
val activityDate = column[java.util.Date]("ACTIVITY_DATE")
def * =
(userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
def * = ???
}
}
@@ -26,5 +23,5 @@ case class Activity(
message: String,
additionalInfo: Option[String],
activityDate: java.util.Date,
activityId: Int = 0
activityId: String
)

View File

@@ -45,7 +45,7 @@ trait CoreProfile
with Profile
with AccessTokenComponent
with AccountComponent
with ActivityComponent
with ActivityComponent // ActivityComponent has been deprecated, but keep it for binary compatibility
with CollaboratorComponent
with CommitCommentComponent
with CommitStatusComponent

View File

@@ -17,7 +17,9 @@ trait AccountService {
def authenticate(settings: SystemSettings, userName: String, password: String)(
implicit s: Session
): Option[Account] = {
val account = if (settings.ldapAuthentication) {
val account = if (password.isEmpty) {
None
} else if (settings.ldapAuthentication) {
ldapAuthentication(settings, userName, password)
} else {
defaultAuthentication(userName, password)

View File

@@ -3,65 +3,169 @@ package gitbucket.core.service
import gitbucket.core.model.Activity
import gitbucket.core.util.JGitUtil
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.util.Directory._
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
import scala.util.Using
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import java.util.UUID
import gitbucket.core.controller.Context
import org.apache.commons.io.input.ReversedLinesFileReader
import scala.collection.mutable.ListBuffer
trait ActivityService {
self: RequestCache =>
def deleteOldActivities(limit: Int)(implicit s: Session): Int = {
Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id =>
Activities.filter(_.activityId <= id.bind).delete
} getOrElse 0
private implicit val formats = Serialization.formats(NoTypeHints)
private def writeLog(activity: Activity): Unit = {
Using.resource(new FileOutputStream(ActivityLog, true)) { out =>
out.write((write(activity) + "\n").getBytes(StandardCharsets.UTF_8))
}
}
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] =
Activities
.join(Repositories)
.on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter {
case (t1, t2) =>
if (isPublic) {
(t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind)
} else {
(t1.activityUserName === activityUserName.bind)
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = {
if (!ActivityLog.exists()) {
List.empty
} else {
val list = new ListBuffer[Activity]
Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
var json: String = null
while (list.length < 50 && { json = reader.readLine(); json } != null) {
val activity = read[Activity](json)
if (activity.activityUserName == activityUserName) {
if (isPublic == false) {
list += activity
} else {
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
list += activity
}
}
}
}
}
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
list.toList
}
}
def getRecentActivities()(implicit s: Session): List[Activity] =
Activities
.join(Repositories)
.on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => t2.isPrivate === false.bind }
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
def getRecentPublicActivities()(implicit context: Context): List[Activity] = {
if (!ActivityLog.exists()) {
List.empty
} else {
val list = new ListBuffer[Activity]
Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
var json: String = null
while (list.length < 50 && { json = reader.readLine(); json } != null) {
val activity = read[Activity](json)
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
list += activity
}
}
}
list.toList
}
}
def getRecentActivitiesByOwners(owners: Set[String])(implicit s: Session): List[Activity] =
Activities
.join(Repositories)
.on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) }
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = {
if (!ActivityLog.exists()) {
List.empty
} else {
val list = new ListBuffer[Activity]
Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
var json: String = null
while (list.length < 50 && { json = reader.readLine(); json } != null) {
val activity = read[Activity](json)
if (owners.contains(activity.userName)) {
list += activity
} else if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
list += activity
}
}
}
list.toList
}
}
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String)(
implicit s: Session
): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"create_repository",
s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
None,
currentDate
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"create_repository",
s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordDeleteRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"delete_repository",
s"[user:${activityUserName}] deleted [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordTransferRepositoryActivity(
userName: String,
repositoryName: String,
oldUserName: String,
activityUserName: String
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"transfer_repository",
s"[user:${activityUserName}] transfered [repo:${oldUserName}/${repositoryName}] to [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordRenameRepositoryActivity(
userName: String,
repositoryName: String,
oldRepositoryName: String,
activityUserName: String
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"rename_repository",
s"[user:${activityUserName}] renamed [repo:${userName}/${oldRepositoryName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCreateIssueActivity(
userName: String,
@@ -69,16 +173,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
title: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"open_issue",
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"open_issue",
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCloseIssueActivity(
userName: String,
@@ -86,16 +194,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
title: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"close_issue",
s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"close_issue",
s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordClosePullRequestActivity(
userName: String,
@@ -103,16 +215,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
title: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"close_issue",
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"close_issue",
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordReopenIssueActivity(
userName: String,
@@ -120,16 +236,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
title: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"reopen_issue",
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"reopen_issue",
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordReopenPullRequestActivity(
userName: String,
@@ -137,16 +257,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
title: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"reopen_issue",
s"[user:${activityUserName}] reopened pull request [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"reopen_issue",
s"[user:${activityUserName}] reopened pull request [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCommentIssueActivity(
userName: String,
@@ -154,16 +278,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
comment: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"comment_issue",
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"comment_issue",
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCommentPullRequestActivity(
userName: String,
@@ -171,16 +299,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
comment: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"comment_issue",
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"comment_issue",
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCommentCommitActivity(
userName: String,
@@ -188,32 +320,40 @@ trait ActivityService {
activityUserName: String,
commitId: String,
comment: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"comment_commit",
s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]",
Some(cut(comment, 200)),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"comment_commit",
s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]",
Some(cut(comment, 200)),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCreateWikiPageActivity(
userName: String,
repositoryName: String,
activityUserName: String,
pageName: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"create_wiki",
s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
Some(pageName),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"create_wiki",
s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
Some(pageName),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordEditWikiPageActivity(
userName: String,
@@ -221,16 +361,20 @@ trait ActivityService {
activityUserName: String,
pageName: String,
commitId: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"edit_wiki",
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
Some(pageName + ":" + commitId),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"edit_wiki",
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
Some(pageName + ":" + commitId),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordPushActivity(
userName: String,
@@ -238,23 +382,27 @@ trait ActivityService {
activityUserName: String,
branchName: String,
commits: List[JGitUtil.CommitInfo]
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"push",
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
Some(
commits
.take(5)
.map { commit =>
commit.id + ":" + commit.shortMessage
}
.mkString("\n")
),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"push",
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
Some(
commits
.take(5)
.map { commit =>
commit.id + ":" + commit.shortMessage
}
.mkString("\n")
),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCreateTagActivity(
userName: String,
@@ -262,16 +410,20 @@ trait ActivityService {
activityUserName: String,
tagName: String,
commits: List[JGitUtil.CommitInfo]
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"create_tag",
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"create_tag",
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordDeleteTagActivity(
userName: String,
@@ -279,61 +431,80 @@ trait ActivityService {
activityUserName: String,
tagName: String,
commits: List[JGitUtil.CommitInfo]
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
None,
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCreateBranchActivity(
userName: String,
repositoryName: String,
activityUserName: String,
branchName: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"create_branch",
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"create_branch",
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordDeleteBranchActivity(
userName: String,
repositoryName: String,
activityUserName: String,
branchName: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"delete_branch",
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
None,
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"delete_branch",
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(
implicit s: Session
): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
None,
currentDate
def recordForkActivity(
userName: String,
repositoryName: String,
activityUserName: String,
forkedUserName: String
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordPullRequestActivity(
userName: String,
@@ -341,16 +512,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
title: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"open_pullreq",
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"open_pullreq",
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordMergeActivity(
userName: String,
@@ -358,16 +533,20 @@ trait ActivityService {
activityUserName: String,
issueId: Int,
message: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"merge_pullreq",
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(message),
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"merge_pullreq",
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(message),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordReleaseActivity(
userName: String,
@@ -375,16 +554,20 @@ trait ActivityService {
activityUserName: String,
releaseName: String,
tagName: String
)(implicit s: Session): Unit =
Activities insert Activity(
userName,
repositoryName,
activityUserName,
"release",
s"[user:${activityUserName}] released [release:${userName}/${repositoryName}/${tagName}:${releaseName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"release",
s"[user:${activityUserName}] released [release:${userName}/${repositoryName}/${tagName}:${releaseName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
private def cut(value: String, length: Int): String =
if (value.length > length) value.substring(0, length) + "..." else value

View File

@@ -1,6 +1,5 @@
package gitbucket.core.service
import gitbucket.core.api.JsonFormat
import gitbucket.core.controller.Context
import gitbucket.core.util._
import gitbucket.core.util.SyntaxSugars._
@@ -9,14 +8,11 @@ import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.dateColumnType
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.WebHookService.WebHookPushPayload
import gitbucket.core.util.Directory.{getRepositoryDir, getRepositoryFilesDir, getTemporaryDir, getWikiRepositoryDir}
import gitbucket.core.util.JGitUtil.{CommitInfo, FileInfo}
import gitbucket.core.util.JGitUtil.FileInfo
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.lib.{Repository => _, _}
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import scala.util.Using
trait RepositoryService {
@@ -119,15 +115,6 @@ trait RepositoryService {
}
.update(newUserName, newRepositoryName)
// Updates activity fk before deleting repository because activity is sorted by activityId
// and it can't be changed by deleting-and-inserting record.
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
Activities
.filter(_.activityId === activity.activityId.bind)
.map(x => (x.userName, x.repositoryName))
.update(newUserName, newRepositoryName)
}
deleteRepositoryOnModel(oldUserName, oldRepositoryName)
RepositoryWebHooks.insertAll(
@@ -213,50 +200,6 @@ trait RepositoryService {
collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)): _*
)
// Update activity messages
Activities
.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}@%")
}
.map { t =>
t.activityId -> t.message
}
.list
.foreach {
case (activityId, message) =>
Activities
.filter(_.activityId === 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}#"
)
.replace(
s"[commit:${oldUserName}/${oldRepositoryName}@",
s"[commit:${newUserName}/${newRepositoryName}@"
)
)
}
// Move git repository
defining(getRepositoryDir(oldUserName, oldRepositoryName)) { dir =>
if (dir.isDirectory) {
@@ -304,7 +247,7 @@ trait RepositoryService {
}
private def deleteRepositoryOnModel(userName: String, repositoryName: String)(implicit s: Session): Unit = {
Activities.filter(_.byRepository(userName, repositoryName)).delete
// Activities.filter(_.byRepository(userName, repositoryName)).delete
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
CommitComments.filter(_.byRepository(userName, repositoryName)).delete
IssueLabels.filter(_.byRepository(userName, repositoryName)).delete

View File

@@ -1,9 +1,11 @@
package gitbucket.core.service
import gitbucket.core.model.{Session, Issue, Account}
import gitbucket.core.model.{Account, Issue, Repository, Session}
import gitbucket.core.util.Implicits
import gitbucket.core.controller.Context
import Implicits.request2Session
import gitbucket.core.model.Profile.{Accounts, Repositories}
import gitbucket.core.model.Profile.profile.blockingApi._
/**
* This service is used for a view helper mainly.
@@ -23,21 +25,41 @@ trait RequestCache
private implicit def context2Session(implicit context: Context): Session =
request2Session(context.request)
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: Context): Option[Issue] = {
def getIssueFromCache(userName: String, repositoryName: String, issueId: String)(
implicit context: Context
): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}") {
super.getIssue(userName, repositoryName, issueId)
}
}
def getAccountByUserName(userName: String)(implicit context: Context): Option[Account] = {
def getAccountByUserNameFromCache(userName: String)(implicit context: Context): Option[Account] = {
context.cache(s"account.${userName}") {
super.getAccountByUserName(userName)
}
}
def getAccountByMailAddress(mailAddress: String)(implicit context: Context): Option[Account] = {
def getAccountByMailAddressFromCache(mailAddress: String)(implicit context: Context): Option[Account] = {
context.cache(s"account.${mailAddress}") {
super.getAccountByMailAddress(mailAddress)
}
}
def getRepositoryInfoFromCache(userName: String, repositoryName: String)(
implicit context: Context
): Option[Repository] = {
context.cache(s"repository.${userName}/${repositoryName}") {
Repositories
.join(Accounts)
.on(_.userName === _.userName)
.filter {
case (t1, t2) =>
t1.byRepository(userName, repositoryName) && t2.removed === false.bind
}
.map {
case (t1, t2) => t1
}
.firstOption
}
}
}

View File

@@ -240,7 +240,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
with WebHookPullRequestService
with WebHookPullRequestReviewCommentService
with CommitsService
with SystemSettingsService {
with SystemSettingsService
with RequestCache {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil

View File

@@ -2,11 +2,10 @@ package gitbucket.core.servlet
import java.io.{File, FileOutputStream}
import akka.event.Logging
import com.typesafe.config.ConfigFactory
import gitbucket.core.GitBucketCoreModule
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.{ActivityService, SystemSettingsService}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import gitbucket.core.util.JDBCUtil._
@@ -21,8 +20,6 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
import org.apache.commons.io.{FileUtils, IOUtils}
import org.slf4j.LoggerFactory
import akka.actor.{Actor, ActorSystem, Props}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import scala.jdk.CollectionConverters._
import scala.util.Using
@@ -35,23 +32,23 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
// ActorSystem for Quartz scheduler
private val system = ActorSystem(
"job",
ConfigFactory.parseString("""
|akka {
| daemonic = on
| coordinated-shutdown.run-by-jvm-shutdown-hook = off
| quartz {
| schedules {
| Daily {
| expression = "0 0 0 * * ?"
| }
| }
| }
|}
""".stripMargin)
)
// // ActorSystem for Quartz scheduler
// private val system = ActorSystem(
// "job",
// ConfigFactory.parseString("""
// |akka {
// | daemonic = on
// | coordinated-shutdown.run-by-jvm-shutdown-hook = off
// | quartz {
// | schedules {
// | Daily {
// | expression = "0 0 0 * * ?"
// | }
// | }
// | }
// |}
// """.stripMargin)
// )
override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
@@ -95,10 +92,10 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
}
// Start Quartz scheduler
val scheduler = QuartzSchedulerExtension(system)
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
// // Start Quartz scheduler
// val scheduler = QuartzSchedulerExtension(system)
//
// scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
}
private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = {
@@ -172,8 +169,8 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
}
override def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown Quartz scheduler
system.terminate()
// // Shutdown Quartz scheduler
// system.terminate()
// Shutdown plugins
PluginRegistry.shutdown(event.getServletContext, loadSystemSettings())
// Close datasource
@@ -181,21 +178,3 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
}
}
class DeleteOldActivityActor extends Actor with SystemSettingsService with ActivityService {
private val logger = Logging(context.system, this)
def receive = {
case s: String => {
loadSystemSettings().activityLogLimit.foreach { limit =>
if (limit > 0) {
Database() withTransaction { implicit session =>
val rows = deleteOldActivities(limit)
logger.info(s"Deleted ${rows} activity logs")
}
}
}
}
}
}

View File

@@ -29,6 +29,8 @@ object Directory {
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val ActivityLog = new File(GitBucketHome, "activity.log")
val RepositoryHome = s"${GitBucketHome}/repositories"
val DatabaseHome = s"${GitBucketHome}/data"

View File

@@ -17,7 +17,7 @@ trait AvatarImageProvider { self: RequestCache =>
val src = if (mailAddress.isEmpty) {
// by user name
getAccountByUserName(userName).map { account =>
getAccountByUserNameFromCache(userName).map { account =>
if (account.image.isEmpty && context.settings.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else {
@@ -28,7 +28,7 @@ trait AvatarImageProvider { self: RequestCache =>
}
} else {
// by mail address
getAccountByMailAddress(mailAddress).map { account =>
getAccountByMailAddressFromCache(mailAddress).map { account =>
if (account.image.isEmpty && context.settings.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else {

View File

@@ -16,7 +16,7 @@ trait LinkConverter { self: RequestCache =>
val userName = repository.repository.userName
val repositoryName = repository.repository.repositoryName
getIssue(userName, repositoryName, issueId.toString) match {
getIssueFromCache(userName, repositoryName, issueId.toString) match {
case Some(issue) =>
s"""<a href="${context.path}/${userName}/${repositoryName}/${if (issue.isPullRequest) "pull" else "issues"}/${issueId}"><strong>${StringUtil
.escapeHtml(title)}</strong> #${issueId}</a>"""
@@ -43,7 +43,7 @@ trait LinkConverter { self: RequestCache =>
escaped
// convert username/project@SHA to link
.replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r) { m =>
getAccountByUserName(m.group(2)).map { _ =>
getAccountByUserNameFromCache(m.group(2)).map { _ =>
s"""<code><a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(
3
)}@${m.group(4).substring(0, 7)}</a></code>"""
@@ -53,7 +53,7 @@ trait LinkConverter { self: RequestCache =>
// convert username/project#Num to link
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) {
m =>
getIssue(m.group(2), m.group(3), m.group(4)) match {
getIssueFromCache(m.group(2), m.group(3), m.group(4)) match {
case Some(issue) if (issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m.group(4)}">${m.group(2)}/${m.group(
3
@@ -68,7 +68,7 @@ trait LinkConverter { self: RequestCache =>
// convert username@SHA to link
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r) { m =>
getAccountByUserName(m.group(2)).map { _ =>
getAccountByUserNameFromCache(m.group(2)).map { _ =>
s"""<code><a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m
.group(3)
.substring(0, 7)}</a></code>"""
@@ -77,7 +77,7 @@ trait LinkConverter { self: RequestCache =>
// convert username#Num to link
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) { m =>
getIssue(m.group(2), repository.name, m.group(3)) match {
getIssueFromCache(m.group(2), repository.name, m.group(3)) match {
case Some(issue) if (issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/pull/${m.group(3)}">${m.group(2)}#${m
.group(3)}</a>""")
@@ -92,7 +92,7 @@ trait LinkConverter { self: RequestCache =>
// convert issue id to link
.replaceBy(("(?<=(^|\\W))(GH-|(?<!&)" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r) { m =>
val prefix = if (m.group(2) == "issue:") "#" else m.group(2)
getIssue(repository.owner, repository.name, m.group(3)) match {
getIssueFromCache(repository.owner, repository.name, m.group(3)) match {
case Some(issue) if (issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(3)}">${prefix}${m
.group(3)}</a>""")
@@ -106,7 +106,7 @@ trait LinkConverter { self: RequestCache =>
// convert @username to link
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_\\.]+)(?=(\\W|$))".r) { m =>
getAccountByUserName(m.group(2)).map { _ =>
getAccountByUserNameFromCache(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
}
}

View File

@@ -3,7 +3,6 @@ package gitbucket.core.view
import java.text.SimpleDateFormat
import java.util.{Date, Locale, TimeZone}
import com.nimbusds.jose.util.JSONObjectUtils
import gitbucket.core.controller.Context
import gitbucket.core.model.CommitState
import gitbucket.core.model.PullRequest
@@ -196,7 +195,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
import scala.util.matching.Regex._
implicit class RegexReplaceString(private val s: String) extends AnyVal {
def replaceAll(pattern: String, replacer: (Match) => String): String = {
def replaceAll(pattern: String)(replacer: Match => String): String = {
pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$"))
}
}
@@ -204,50 +203,64 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* Convert link notations in the activity message.
*/
// format: off
def activityMessage(message: String)(implicit context: Context): Html =
Html(
message
.replaceAll(
"\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]",
s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>"""
)
.replaceAll(
"\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]",
s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>"""
)
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll(
"\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]",
(m: Match) =>
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil
.escapeHtml(
m.group(3)
)}</a>"""
)
.replaceAll(
"\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]",
(m: Match) =>
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil
.escapeHtml(
m.group(3)
)}</a>"""
)
.replaceAll("\\[user:([^\\s]+?)\\]", (m: Match) => user(m.group(1)).body)
.replaceAll(
"\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]",
(m: Match) =>
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m
.group(2)}@${m.group(3).substring(0, 7)}</a>"""
)
.replaceAll(
"\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]",
(m: Match) =>
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/releases/${encodeRefName(m.group(3))}">${StringUtil
.escapeHtml(
m.group(4)
)}</a>"""
)
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/issues/${m.group(3)}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
} else {
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
}
}
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/pull/${m.group(3)}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
} else {
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
}
}
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]") { m =>
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}">${m.group(1)}/${m.group(2)}</a>"""
} else {
s"${m.group(1)}/${m.group(2)}"
}
}
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m =>
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(3))}</a>"""
} else {
StringUtil.escapeHtml(m.group(3))
}
}
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m =>
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(3))}</a>"""
} else {
StringUtil.escapeHtml(m.group(3))
}
}
.replaceAll("\\[user:([^\\s]+?)\\]") { m =>
user(m.group(1)).body
}
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]") { m =>
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>"""
} else {
s"${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}"
}
}
.replaceAll("\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]") { m =>
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/releases/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(4))}</a>"""
} else {
StringUtil.escapeHtml(m.group(4))
}
}
)
// format: off
/**
* Remove html tags from the given Html instance.
@@ -333,9 +346,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
content: Html
)(implicit context: Context): Html =
(if (mailAddress.isEmpty) {
getAccountByUserName(userName)
getAccountByUserNameFromCache(userName)
} else {
getAccountByMailAddress(mailAddress)
getAccountByMailAddressFromCache(mailAddress)
}).map { account =>
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${content}</a>""")
} getOrElse content

View File

@@ -7,21 +7,24 @@
@activities.map { activity =>
<div class="block">
@(activity.activityType match {
case "open_issue" => detailActivity(activity, "issue-opened")
case "comment_issue" => detailActivity(activity, "comment-discussion")
case "comment_commit" => detailActivity(activity, "comment-discussion")
case "close_issue" => detailActivity(activity, "issue-closed")
case "reopen_issue" => detailActivity(activity, "issue-reopened")
case "open_pullreq" => detailActivity(activity, "git-pull-request")
case "merge_pullreq" => detailActivity(activity, "git-merge")
case "release" => detailActivity(activity, "package")
case "create_repository" => simpleActivity(activity, "repo")
case "create_branch" => simpleActivity(activity, "git-branch")
case "delete_branch" => simpleActivity(activity, "circle-slash")
case "create_tag" => simpleActivity(activity, "tag")
case "delete_tag" => simpleActivity(activity, "circle-slash")
case "fork" => simpleActivity(activity, "repo-forked")
case "push" => customActivity(activity, "git-commit"){
case "open_issue" => simpleActivity(activity)
case "comment_issue" => simpleActivity(activity)
case "comment_commit" => simpleActivity(activity)
case "close_issue" => simpleActivity(activity)
case "reopen_issue" => simpleActivity(activity)
case "open_pullreq" => simpleActivity(activity)
case "merge_pullreq" => simpleActivity(activity)
case "release" => simpleActivity(activity)
case "create_repository" => simpleActivity(activity)
case "delete_repository" => simpleActivity(activity)
case "rename_repository" => simpleActivity(activity)
case "transfer_repository" => simpleActivity(activity)
case "create_branch" => simpleActivity(activity)
case "delete_branch" => simpleActivity(activity)
case "create_tag" => simpleActivity(activity)
case "delete_tag" => simpleActivity(activity)
case "fork" => simpleActivity(activity)
case "push" => customActivity(activity){
<div class="small activity-message">
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
@@ -37,12 +40,12 @@
}}
</div>
}
case "create_wiki" => customActivity(activity, "book"){
case "create_wiki" => customActivity(activity){
<div class="small activity-message">
Created <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
</div>
}
case "edit_wiki" => customActivity(activity, "book"){
case "edit_wiki" => customActivity(activity){
activity.additionalInfo.get.split(":") match {
case Array(pageName, commitId) =>
<div class="small activity-message">
@@ -60,26 +63,7 @@
}
}
@detailActivity(activity: gitbucket.core.model.Activity, image: String) = {
@*
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
*@
<div>
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
</div>
@activity.additionalInfo.map { additionalInfo =>
<div class=" activity-message">@additionalInfo</div>
}
</div>
}
@customActivity(activity: gitbucket.core.model.Activity, image: String)(additionalInfo: Any) = {
@*
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
*@
@customActivity(activity: gitbucket.core.model.Activity)(additionalInfo: Any) = {
<div>
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@@ -90,10 +74,7 @@
</div>
}
@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = {
@*
<div class="activity-icon-small"><i class="octicon octicon-@image"></i></div>
*@
@simpleActivity(activity: gitbucket.core.model.Activity) = {
<div>
<span class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</span>
<div>

View File

@@ -14,7 +14,7 @@ import scala.util.Using
class MergeServiceSpec extends FunSpec {
val service = new MergeService with AccountService with ActivityService with IssuesService with LabelsService
with MilestonesService with RepositoryService with PrioritiesService with PullRequestService with CommitsService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService {}
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with RequestCache {}
val branch = "master"
val issueId = 10
def initRepository(owner: String, name: String): File = {

View File

@@ -18,7 +18,8 @@ class PullRequestServiceSpec
with PrioritiesService
with WebHookService
with WebHookPullRequestService
with WebHookPullRequestReviewCommentService {
with WebHookPullRequestReviewCommentService
with RequestCache {
def swap(r: (Issue, PullRequest)) = (r._2 -> r._1)

View File

@@ -99,7 +99,7 @@ trait ServiceSpecBase extends MockitoSugar {
lazy val dummyService = new RepositoryService with AccountService with ActivityService with IssuesService
with MergeService with PullRequestService with CommitsService with CommitStatusService with LabelsService
with MilestonesService with PrioritiesService with WebHookService with WebHookPullRequestService
with WebHookPullRequestReviewCommentService {
with WebHookPullRequestReviewCommentService with RequestCache {
override def fetchAsPullRequest(
userName: String,
repositoryName: String,

View File

@@ -7,7 +7,7 @@ import gitbucket.core.model.WebHookContentType
class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
lazy val service = new WebHookPullRequestService with AccountService with ActivityService with RepositoryService
with MergeService with PullRequestService with IssuesService with CommitsService with LabelsService
with MilestonesService with PrioritiesService with WebHookPullRequestReviewCommentService
with MilestonesService with PrioritiesService with WebHookPullRequestReviewCommentService with RequestCache
test("WebHookPullRequestService.getPullRequestsByRequestForWebhook") {
withTestDB { implicit session =>

View File

@@ -169,8 +169,9 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
context: Context
): Html = getAvatarImageHtml(userName, size, mailAddress, tooltip)
override def getAccountByMailAddress(mailAddress: String)(implicit context: Context): Option[Account] = account
override def getAccountByUserName(userName: String)(implicit context: Context): Option[Account] = account
override def getAccountByMailAddressFromCache(mailAddress: String)(implicit context: Context): Option[Account] =
account
override def getAccountByUserNameFromCache(userName: String)(implicit context: Context): Option[Account] = account
}
}