diff --git a/build.sbt b/build.sbt
index b7c12bc..0fa50e9 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,6 @@
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
-val GitBucketVersion = "4.11.0"
+val GitBucketVersion = "4.12.0-SNAPSHOT"
val ScalatraVersion = "2.5.0"
val JettyVersion = "9.3.9.v20160517"
diff --git a/src/main/resources/update/gitbucket-core_4.12.xml b/src/main/resources/update/gitbucket-core_4.12.xml
new file mode 100644
index 0000000..3e503ff
--- /dev/null
+++ b/src/main/resources/update/gitbucket-core_4.12.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala
index 949bf80..3ea8aad 100644
--- a/src/main/scala/ScalatraBootstrap.scala
+++ b/src/main/scala/ScalatraBootstrap.scala
@@ -47,6 +47,7 @@
context.mount(new MilestonesController, "/*")
context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*")
+ context.mount(new ReleaseController, "/*")
context.mount(new RepositorySettingsController, "/*")
// Create GITBUCKET_HOME directory if it does not exist
diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
index dd8665d..2d5211e 100644
--- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
+++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
@@ -31,5 +31,8 @@
new Version("4.10.0"),
new Version("4.11.0",
new LiquibaseMigration("update/gitbucket-core_4.11.xml")
+ ),
+ new Version("4.12.0",
+ new LiquibaseMigration("update/gitbucket-core_4.12.xml")
)
)
diff --git a/src/main/scala/gitbucket/core/controller/FileUploadController.scala b/src/main/scala/gitbucket/core/controller/FileUploadController.scala
index cff4ceb..eae3479 100644
--- a/src/main/scala/gitbucket/core/controller/FileUploadController.scala
+++ b/src/main/scala/gitbucket/core/controller/FileUploadController.scala
@@ -2,7 +2,7 @@
import gitbucket.core.model.Account
import gitbucket.core.service.RepositoryService.RepositoryInfo
-import gitbucket.core.service.{AccountService, RepositoryService}
+import gitbucket.core.service.{AccountService, RepositoryService, ReleaseService}
import gitbucket.core.servlet.Database
import gitbucket.core.util._
import gitbucket.core.util.SyntaxSugars._
@@ -20,7 +20,11 @@
*
* This servlet saves uploaded file.
*/
-class FileUploadController extends ScalatraServlet with FileUploadSupport with RepositoryService with AccountService {
+class FileUploadController extends ScalatraServlet
+ with FileUploadSupport
+ with RepositoryService
+ with AccountService
+ with ReleaseService{
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
@@ -78,6 +82,25 @@
} getOrElse BadRequest()
}
+ post("/release/:owner/:repository/:id"){
+ session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account =>
+ val owner = params("owner")
+ val repository = params("repository")
+ val releaseId = params("id").toInt
+ val release = getRelease(owner, repository, releaseId)
+ execute({ (file, fileId) =>
+ val fileName = file.getName
+ release.map { rel =>
+ createReleaseAsset(owner, repository, releaseId, fileId, fileName, file.size, loginAccount)
+ FileUtils.writeByteArrayToFile(new java.io.File(
+ getReleaseFilesDir(owner, repository) + s"/${rel.tag}",
+ fileId), file.get)
+ fileName
+ }
+ }, (_ => true))
+ }.getOrElse(BadRequest())
+ }
+
post("/import") {
import JDBCUtil._
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin =>
diff --git a/src/main/scala/gitbucket/core/controller/ReleasesController.scala b/src/main/scala/gitbucket/core/controller/ReleasesController.scala
new file mode 100644
index 0000000..d13498c
--- /dev/null
+++ b/src/main/scala/gitbucket/core/controller/ReleasesController.scala
@@ -0,0 +1,139 @@
+package gitbucket.core.controller
+
+import gitbucket.core.service.{RepositoryService, AccountService, ReleaseService, ActivityService}
+import gitbucket.core.util.{ReferrerAuthenticator, ReadableUsersAuthenticator, WritableUsersAuthenticator, FileUtil, Notifier}
+import gitbucket.core.util.Directory._
+import gitbucket.core.util.Implicits._
+import gitbucket.core.util.SyntaxSugars._
+import gitbucket.core.view.Markdown
+import io.github.gitbucket.scalatra.forms._
+import gitbucket.core.releases.html
+
+class ReleaseController extends ReleaseControllerBase
+ with RepositoryService
+ with AccountService
+ with ReleaseService
+ with ActivityService
+ with ReadableUsersAuthenticator
+ with ReferrerAuthenticator
+ with WritableUsersAuthenticator
+
+trait ReleaseControllerBase extends ControllerBase {
+ self: RepositoryService
+ with AccountService
+ with ReleaseService
+ with ReadableUsersAuthenticator
+ with ReferrerAuthenticator
+ with WritableUsersAuthenticator
+ with ActivityService =>
+
+ case class ReleaseCreateForm(
+ name: String,
+ content: Option[String],
+ isPrerelease: Boolean
+ )
+
+ val releaseCreateForm = mapping(
+ "name" -> trim(text(required)),
+ "content" -> trim(optional(text())),
+ "isprerelease" -> boolean()
+ )(ReleaseCreateForm.apply)
+
+ val releaseTitleEditForm = mapping(
+ "title" -> trim(label("Title", text(required)))
+ )(x => x)
+
+ val releaseEditForm = mapping(
+ "content" -> trim(optional(text()))
+ )(x => x)
+
+ get("/:owner/:repository/releases")(referrersOnly {repository =>
+ html.list(
+ repository,
+ getReleaseTagMap(repository.owner, repository.name),
+ getReleaseAssetsMap(repository.owner, repository.name),
+ hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
+ })
+
+ get("/:owner/:repository/releases/:id")(referrersOnly {repository =>
+ val id = params("id")
+ getRelease(repository.owner, repository.name, id).map{ release =>
+ html.release(release, getReleaseAssets(repository.owner, repository.name, id), hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
+ }.getOrElse(NotFound())
+ })
+
+ get("/:owner/:repository/releases/:id/assets/:fileId")(referrersOnly {repository =>
+ val releaseId = params("id")
+ val fileId = params("fileId")
+ getRelease(repository.owner, repository.name, releaseId).flatMap{ release =>
+ getReleaseAsset(repository.owner, repository.name, releaseId, fileId).flatMap{ asset =>
+ response.setHeader("Content-Disposition", s"attachment; filename=${asset.label}")
+ Some(RawData(FileUtil.getMimeType(asset.label), new java.io.File(getReleaseFilesDir(repository.owner, repository.name) + s"/${release.tag}", fileId)))
+ }
+ }.getOrElse(NotFound())
+ })
+
+ get("/:owner/:repository/releases/:tag/create")(writableUsersOnly {repository =>
+ val tag = params("tag")
+ defining(repository.owner, repository.name){ case (owner, name) =>
+ html.create(repository, tag)
+ }
+ })
+
+ post("/:owner/:repository/releases/:tag/create", releaseCreateForm)(writableUsersOnly { (form, repository) =>
+ val tag = params("tag")
+ val release = createRelease(repository, form.name, form.content, tag, false, form.isPrerelease, context.loginAccount.get)
+ recordReleaseActivity(repository.owner, repository.name, context.loginAccount.get.userName, release.releaseId, release.name)
+
+ redirect(s"/${release.userName}/${release.repositoryName}/releases/${release.releaseId}")
+ })
+
+ get("/:owner/:repository/release/delete/:id")(writableUsersOnly { repository =>
+ deleteRelease(repository.owner, repository.name, params("id"))
+
+ redirect(s"/${repository.owner}/${repository.name}/releases")
+ })
+
+ ajaxPost("/:owner/:repository/releases/edit_title/:id", releaseTitleEditForm)(writableUsersOnly { (title, repository) =>
+ defining(repository.owner, repository.name) { case (owner, name) =>
+ getRelease(owner, name, params("id")).map { release =>
+ updateRelease(owner, name, release.releaseId, title, release.content)
+ redirect(s"/${owner}/${name}/releases/_data/${release.releaseId}")
+ } getOrElse NotFound()
+ }
+ })
+
+ ajaxPost("/:owner/:repository/releases/edit/:id", releaseEditForm)(writableUsersOnly { (content, repository) =>
+ defining(repository.owner, repository.name){ case (owner, name) =>
+ getRelease(owner, name, params("id")).map { release =>
+ updateRelease(owner, name, release.releaseId, release.name, content)
+ redirect(s"/${owner}/${name}/releases/_data/${release.releaseId}")
+ } getOrElse NotFound()
+ }
+ })
+
+ ajaxGet("/:owner/:repository/releases/_data/:id")(writableUsersOnly { repository =>
+ getRelease(repository.owner, repository.name, params("id")) map { x =>
+ params.get("dataType") collect {
+ case t if t == "html" => html.editrelease(x.content, x.releaseId, repository)
+ } getOrElse {
+ contentType = formats("json")
+ org.json4s.jackson.Serialization.write(
+ Map(
+ "title" -> x.name,
+ "content" -> Markdown.toHtml(
+ markdown = x.content getOrElse "No description given.",
+ repository = repository,
+ enableWikiLink = false,
+ enableRefsLink = true,
+ enableAnchor = true,
+ enableLineBreaks = true,
+ enableTaskList = true,
+ hasWritePermission = true
+ )
+ )
+ )
+ }
+ } getOrElse NotFound()
+ })
+}
diff --git a/src/main/scala/gitbucket/core/model/BasicTemplate.scala b/src/main/scala/gitbucket/core/model/BasicTemplate.scala
index c3b8e1b..a33d96f 100644
--- a/src/main/scala/gitbucket/core/model/BasicTemplate.scala
+++ b/src/main/scala/gitbucket/core/model/BasicTemplate.scala
@@ -63,4 +63,15 @@
def byBranch(owner: String, repository: String, branchName: String) = byRepository(owner, repository) && (branch === branchName.bind)
def byBranch(owner: Rep[String], repository: Rep[String], branchName: Rep[String]) = byRepository(owner, repository) && (this.branch === branchName)
}
+
+ trait ReleaseTemplate extends BasicTemplate { self: Table[_] =>
+ val releaseId = column[Int]("RELEASE_ID")
+
+ def byRelease(owner: String, repository: String, releaseId: Int) =
+ byRepository(owner, repository) && (this.releaseId === releaseId.bind)
+
+ def byRelease(userName: Rep[String], repositoryName: Rep[String], releaseId: Rep[Int]) =
+ byRepository(userName, repositoryName) && (this.releaseId === releaseId)
+ }
+
}
diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala
index 332e7ea..76d64fc 100644
--- a/src/main/scala/gitbucket/core/model/Profile.scala
+++ b/src/main/scala/gitbucket/core/model/Profile.scala
@@ -55,5 +55,7 @@
with WebHookEventComponent
with ProtectedBranchComponent
with DeployKeyComponent
+ with ReleaseComponent
+ with ReleaseAssetComponent
object Profile extends CoreProfile
diff --git a/src/main/scala/gitbucket/core/model/Release.scala b/src/main/scala/gitbucket/core/model/Release.scala
new file mode 100644
index 0000000..3d3b02f
--- /dev/null
+++ b/src/main/scala/gitbucket/core/model/Release.scala
@@ -0,0 +1,53 @@
+package gitbucket.core.model
+
+trait ReleaseComponent extends TemplateComponent {
+ self: Profile =>
+
+ import profile.api._
+ import self._
+
+ lazy val ReleaseId = TableQuery[ReleaseId]
+ lazy val Releases = TableQuery[Releases]
+
+ class ReleaseId(tag: Tag) extends Table[(String, String, Int)](tag, "RELEASE_ID") with ReleaseTemplate {
+ def * = (userName, repositoryName, releaseId)
+
+ def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
+ }
+
+ class Releases(tag_ : Tag) extends Table[Release](tag_, "RELEASE") with ReleaseTemplate {
+ val name = column[String]("NAME")
+ val tag = column[String]("TAG")
+ val author = column[String]("AUTHOR")
+ val content = column[Option[String]]("CONTENT")
+ val isDraft = column[Boolean]("IS_DRAFT")
+ val isPrerelease = column[Boolean]("IS_PRERELEASE")
+ val registeredDate = column[java.util.Date]("REGISTERED_DATE")
+ val updatedDate = column[java.util.Date]("UPDATED_DATE")
+
+ def * = (userName, repositoryName, releaseId, name, tag, author, content, isDraft, isPrerelease, registeredDate, updatedDate) <> (Release.tupled, Release.unapply)
+
+ def byPrimaryKey(owner: String, repository: String, releaseId: Int) = byRelease(owner, repository, releaseId)
+
+ def byTag(owner: String, repository: String, tag: String) =
+ byRepository(owner, repository) && (this.tag === tag.bind)
+
+ def byTag(userName: Rep[String], repositoryName: Rep[String], tag: Rep[String]) =
+ byRepository(userName, repositoryName) && (this.tag === tag)
+ }
+
+}
+
+case class Release(
+ userName: String,
+ repositoryName: String,
+ releaseId: Int = 0,
+ name: String,
+ tag: String,
+ author: String,
+ content: Option[String],
+ isDraft: Boolean,
+ isPrerelease: Boolean,
+ registeredDate: java.util.Date,
+ updatedDate: java.util.Date
+)
diff --git a/src/main/scala/gitbucket/core/model/ReleasesAsset.scala b/src/main/scala/gitbucket/core/model/ReleasesAsset.scala
new file mode 100644
index 0000000..8125348
--- /dev/null
+++ b/src/main/scala/gitbucket/core/model/ReleasesAsset.scala
@@ -0,0 +1,38 @@
+package gitbucket.core.model
+
+import java.util.Date
+
+trait ReleaseAssetComponent extends TemplateComponent {
+ self: Profile =>
+
+ import profile.api._
+ import self._
+
+ lazy val ReleaseAssets = TableQuery[ReleaseAssets]
+
+ class ReleaseAssets(tag : Tag) extends Table[ReleaseAsset](tag, "RELEASE_ASSET") with ReleaseTemplate {
+ val fileName = column[String]("FILE_NAME")
+ val label = column[String]("LABEL")
+ val size = column[Long]("SIZE")
+ val uploader = column[String]("UPLOADER")
+ val registeredDate = column[Date]("REGISTERED_DATE")
+ val updatedDate = column[Date]("UPDATED_DATE")
+
+ def * = (userName, repositoryName, releaseId, fileName, label, size, uploader, registeredDate, updatedDate) <> (ReleaseAsset.tupled, ReleaseAsset.unapply)
+
+ def byPrimaryKey(owner: String, repository: String, releaseId: Int, fileName: String) = byRelease(owner, repository, releaseId) && (this.fileName === fileName.bind)
+ }
+
+}
+
+case class ReleaseAsset(
+ userName: String,
+ repositoryName: String,
+ releaseId: Int,
+ fileName: String,
+ label: String,
+ size: Long,
+ uploader: String,
+ registeredDate: Date,
+ updatedDate: Date
+)
diff --git a/src/main/scala/gitbucket/core/service/ActivityService.scala b/src/main/scala/gitbucket/core/service/ActivityService.scala
index 433909d..75b73ed 100644
--- a/src/main/scala/gitbucket/core/service/ActivityService.scala
+++ b/src/main/scala/gitbucket/core/service/ActivityService.scala
@@ -59,7 +59,7 @@
Activities insert Activity(userName, repositoryName, activityUserName,
"open_issue",
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
- Some(title),
+ Some(title),
currentDate)
def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
@@ -135,7 +135,7 @@
Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
currentDate)
- def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
+ def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"create_tag",
@@ -167,7 +167,7 @@
None,
currentDate)
- def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit =
+ 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}]",
@@ -190,6 +190,13 @@
Some(message),
currentDate)
+ def recordReleaseActivity(userName: String, repositoryName: String, activityUserName: String, releaseId: Int, name: String)(implicit s: Session): Unit =
+ Activities insert Activity(userName, repositoryName, activityUserName,
+ "release",
+ s"[user:${activityUserName}] released ${name} at [repo:${userName}/${repositoryName}]",
+ None,
+ currentDate)
+
private def cut(value: String, length: Int): String =
if(value.length > length) value.substring(0, length) + "..." else value
}
diff --git a/src/main/scala/gitbucket/core/service/ReleaseService.scala b/src/main/scala/gitbucket/core/service/ReleaseService.scala
new file mode 100644
index 0000000..0ca68b6
--- /dev/null
+++ b/src/main/scala/gitbucket/core/service/ReleaseService.scala
@@ -0,0 +1,129 @@
+package gitbucket.core.service
+
+import gitbucket.core.controller.Context
+import gitbucket.core.model.{Account, Release, ReleaseAsset}
+import gitbucket.core.util.StringUtil._
+import gitbucket.core.util.Implicits._
+import gitbucket.core.model.Profile.profile.blockingApi._
+import gitbucket.core.model.Profile._
+import gitbucket.core.model.Profile.profile._
+import gitbucket.core.model.Profile.dateColumnType
+import gitbucket.core.service.RepositoryService.RepositoryInfo
+
+trait ReleaseService {
+ self: AccountService with RepositoryService =>
+
+ def createReleaseAsset(owner: String, repository: String, releaseId: Int, fileName: String, label: String, size: Long, loginAccount: Account)(implicit s: Session): Unit = {
+ ReleaseAssets insert ReleaseAsset(
+ owner,
+ repository,
+ releaseId,
+ fileName,
+ label,
+ size,
+ loginAccount.userName,
+ currentDate,
+ currentDate
+ )
+ }
+
+ def getReleaseAssets(owner: String, repository: String, releaseId: Int)(implicit s: Session): List[ReleaseAsset] = {
+ ReleaseAssets.filter(x => x.byRelease(owner, repository, releaseId)).list
+ }
+
+ def getReleaseAssets(owner: String, repository: String, releaseId: String)(implicit s: Session): List[ReleaseAsset] = {
+ if (isInteger(releaseId))
+ getReleaseAssets(owner, repository, releaseId.toInt)
+ else
+ List.empty
+ }
+
+ def getReleaseAssetsMap(owner: String, repository: String)(implicit s: Session): Map[Release, List[ReleaseAsset]] = {
+ val releases = getReleases(owner, repository)
+ releases.map(rel => (rel -> getReleaseAssets(owner, repository, rel.releaseId))).toMap
+ }
+
+ def getReleaseAsset(owner: String, repository: String, releaseId: String, fileId: String)(implicit s: Session): Option[ReleaseAsset] = {
+ if (isInteger(releaseId))
+ ReleaseAssets.filter(x => x.byPrimaryKey(owner, repository, releaseId.toInt, fileId)) firstOption
+ else None
+ }
+
+ def deleteReleaseAssets(owner: String, repository: String, releaseId: Int)(implicit s: Session): Unit = {
+ ReleaseAssets.filter(x => x.byRelease(owner, repository, releaseId)) delete
+ }
+
+ def createRelease(repository: RepositoryInfo, name: String, content:Option[String], tag: String,
+ isDraft: Boolean, isPrerelease: Boolean, loginAccount: Account)(implicit context: Context, s: Session): Release = {
+ val releaseId = insertRelease(repository.owner, repository.name, loginAccount.userName, name, tag,
+ content, isDraft, isPrerelease)
+ val release = getRelease(repository.owner, repository.name, releaseId.toString).get
+ release
+ }
+
+ def getReleases(owner: String, repository: String)(implicit s: Session): List[Release] = {
+ Releases.filter(x => x.byRepository(owner, repository)).list
+ }
+
+ def getRelease(owner: String, repository: String, releaseId: Int)(implicit s: Session): Option[Release] = {
+ Releases filter (_.byPrimaryKey(owner, repository, releaseId)) firstOption
+ }
+
+ def getRelease(owner: String, repository: String, releaseId: String)(implicit s: Session): Option[Release] = {
+ if (isInteger(releaseId))
+ getRelease(owner, repository, releaseId.toInt)
+ else None
+ }
+
+ def getReleaseTagMap(owner: String, repository: String)(implicit s: Session): Map[String, Release] = {
+ val releases = getReleases(owner, repository)
+ releases.map(rel => (rel.tag -> rel)).toMap
+ }
+
+ def insertRelease(owner: String, repository: String, loginUser: String, name: String, tag: String,
+ content: Option[String], isDraft: Boolean, isPrerelease: Boolean)(implicit s: Session): Int = {
+ // next id number
+ val id = sql"SELECT RELEASE_ID + 1 FROM RELEASE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
+ .firstOption.getOrElse(1)
+ Releases insert Release(
+ owner,
+ repository,
+ id,
+ name,
+ tag,
+ loginUser,
+ content,
+ isDraft,
+ isPrerelease,
+ currentDate,
+ currentDate
+ )
+
+ // increment issue id
+ if (id > 1){
+ ReleaseId
+ .filter(_.byPrimaryKey(owner, repository))
+ .map(_.releaseId)
+ .update(id) > 0
+ }else{
+ ReleaseId.insert(owner, repository, id)
+ }
+
+ id
+ }
+
+ def updateRelease(owner: String, repository: String, releaseId: Int, title: String, content: Option[String])(implicit s: Session): Int = {
+ Releases
+ .filter (_.byPrimaryKey(owner, repository, releaseId))
+ .map { t => (t.name, t.content, t.updatedDate) }
+ .update (title, content, currentDate)
+ }
+
+ def deleteRelease(owner: String, repository: String, releaseId: String)(implicit s: Session): Unit = {
+ if (isInteger(releaseId)){
+ val relId = releaseId.toInt
+ deleteReleaseAssets(owner, repository, relId)
+ Releases filter (_.byPrimaryKey(owner, repository, relId)) delete
+ }
+ }
+}
diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala
index 3640f1c..cc3159d 100644
--- a/src/main/scala/gitbucket/core/service/RepositoryService.scala
+++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala
@@ -3,7 +3,7 @@
import gitbucket.core.controller.Context
import gitbucket.core.util._
import gitbucket.core.util.SyntaxSugars._
-import gitbucket.core.model.{Account, Collaborator, Repository, RepositoryOptions, Role}
+import gitbucket.core.model.{Account, Collaborator, Repository, RepositoryOptions, Role, Release}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.dateColumnType
@@ -216,6 +216,10 @@
t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind)
}.map(_.pullRequest).list
+ val releases = Releases.filter { t =>
+ t.byRepository(repository.userName, repository.repositoryName)
+ }.list
+
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName),
repository,
@@ -225,6 +229,7 @@
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
+ releases.length,
getRepositoryManagers(repository.userName))
}
}
@@ -458,20 +463,20 @@
object RepositoryService {
case class RepositoryInfo(owner: String, name: String, repository: Repository,
- issueCount: Int, pullCount: Int, forkedCount: Int,
+ issueCount: Int, pullCount: Int, forkedCount: Int, releaseCount: Int,
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) {
/**
* Creates instance with issue count and pull request count.
*/
- def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
- this(repo.owner, repo.name, model, issueCount, pullCount, forkedCount, repo.branchList, repo.tags, managers)
+ def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, releaseCount: Int, managers: Seq[String]) =
+ this(repo.owner, repo.name, model, issueCount, pullCount, forkedCount, releaseCount, repo.branchList, repo.tags, managers)
/**
- * Creates instance without issue count and pull request count.
+ * Creates instance without issue, pull request and release count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
- this(repo.owner, repo.name, model, 0, 0, forkedCount, repo.branchList, repo.tags, managers)
+ this(repo.owner, repo.name, model, 0, 0, forkedCount, 0, repo.branchList, repo.tags, managers)
def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name)
def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name)
diff --git a/src/main/scala/gitbucket/core/util/Directory.scala b/src/main/scala/gitbucket/core/util/Directory.scala
index e54a786..b30b455 100644
--- a/src/main/scala/gitbucket/core/util/Directory.scala
+++ b/src/main/scala/gitbucket/core/util/Directory.scala
@@ -55,6 +55,12 @@
new File(getRepositoryFilesDir(owner, repository), "comments")
/**
+ * Directory for released files
+ */
+ def getReleaseFilesDir(owner: String, repository: String): File =
+ new File(getRepositoryFilesDir(owner, repository), "releases")
+
+ /**
* Directory for files which are attached to issue.
*/
def getLfsDir(owner: String, repository: String): File =
@@ -90,4 +96,4 @@
def getWikiRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
-}
\ No newline at end of file
+}
diff --git a/src/main/twirl/gitbucket/core/helper/activities.scala.html b/src/main/twirl/gitbucket/core/helper/activities.scala.html
index 8d01d0d..2b95fbc 100644
--- a/src/main/twirl/gitbucket/core/helper/activities.scala.html
+++ b/src/main/twirl/gitbucket/core/helper/activities.scala.html
@@ -14,6 +14,7 @@
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")
diff --git a/src/main/twirl/gitbucket/core/menu.scala.html b/src/main/twirl/gitbucket/core/menu.scala.html
index e75d7f8..e27e23c 100644
--- a/src/main/twirl/gitbucket/core/menu.scala.html
+++ b/src/main/twirl/gitbucket/core/menu.scala.html
@@ -27,6 +27,7 @@
@menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length)
@menuitem("/tags", "tags", "Tags", "tag", repository.tags.length)
}
+ @menuitem("/releases", "releases", "Releases", "package", repository.releaseCount)
@if(repository.repository.options.issuesOption != "DISABLE") {
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
@menuitem("/pulls", "pulls", "Pull requests", "git-pull-request", repository.pullCount)
diff --git a/src/main/twirl/gitbucket/core/releases/create.scala.html b/src/main/twirl/gitbucket/core/releases/create.scala.html
new file mode 100644
index 0000000..396ae9c
--- /dev/null
+++ b/src/main/twirl/gitbucket/core/releases/create.scala.html
@@ -0,0 +1,33 @@
+@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, tag: String)(implicit context: gitbucket.core.controller.Context)
+@import gitbucket.core.view.helpers
+@gitbucket.core.html.main(s"New Release - ${repository.owner}/${repository.name}", Some(repository)){
+@gitbucket.core.html.menu("releases", repository){
+
+}
+}
diff --git a/src/main/twirl/gitbucket/core/releases/editrelease.scala.html b/src/main/twirl/gitbucket/core/releases/editrelease.scala.html
new file mode 100644
index 0000000..f7a3aef
--- /dev/null
+++ b/src/main/twirl/gitbucket/core/releases/editrelease.scala.html
@@ -0,0 +1,38 @@
+@(content: Option[String], releaseId: Int,
+repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
+@gitbucket.core.helper.html.attached(repository, "releases"){
+
+}
+
+
+
+
+
diff --git a/src/main/twirl/gitbucket/core/releases/list.scala.html b/src/main/twirl/gitbucket/core/releases/list.scala.html
new file mode 100644
index 0000000..11e5b5b
--- /dev/null
+++ b/src/main/twirl/gitbucket/core/releases/list.scala.html
@@ -0,0 +1,69 @@
+@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
+tagReleaseMap: Map[String, gitbucket.core.model.Release],
+releaseAssetsMap: Map[gitbucket.core.model.Release, List[gitbucket.core.model.ReleaseAsset]],
+hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
+@import gitbucket.core.view.helpers
+@gitbucket.core.html.main("Releases" + s" - ${repository.owner}/${repository.name}", Some(repository)){
+@gitbucket.core.html.menu("releases", repository){
+
+
+ @tagReleaseMap.count(_ => true) releases |
+
+
+ @repository.tags.reverse.map{ tag =>
+
+
+ @tagReleaseMap.get(tag.name).map{rel =>
+
+
+
+
+
+
+ @helpers.avatar(rel.author, 20)
+ @helpers.user(rel.author, styleClass="username strong")
+ released this @gitbucket.core.helper.html.datetimeago(rel.registeredDate)
+
+
+
+ @helpers.markdown(
+ markdown = rel.content getOrElse "No description provided.",
+ repository = repository,
+ enableWikiLink = false,
+ enableRefsLink = true,
+ enableLineBreaks = true,
+ enableTaskList = true,
+ hasWritePermission = hasWritePermission
+ )
+
+ Downloads
+
+
+
+
+ }.getOrElse{
+
+ @gitbucket.core.helper.html.datetimeago(tag.time)
+
+
+ @tag.name
+ @if(hasWritePermission){
+
+ }
+
+ }
+ |
+
+ }
+
+
+}}
diff --git a/src/main/twirl/gitbucket/core/releases/release.scala.html b/src/main/twirl/gitbucket/core/releases/release.scala.html
new file mode 100644
index 0000000..3cadc3a
--- /dev/null
+++ b/src/main/twirl/gitbucket/core/releases/release.scala.html
@@ -0,0 +1,151 @@
+@(release: gitbucket.core.model.Release,
+assets: List[gitbucket.core.model.ReleaseAsset],
+hasWritePermission: Boolean,
+repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
+@import gitbucket.core.view.helpers
+@gitbucket.core.html.main(s"${release.name} - Release ${release.releaseId} - ${repository.owner}/${repository.name}", Some(repository)){
+@gitbucket.core.html.menu("releases", repository){
+
+
+
+
+
+
+
+
+ @release.name
+
+
+
+
+
+
+
+
+
+
+ @helpers.avatar(release.author, 20)
+ @helpers.user(release.author, styleClass="username strong") released this @gitbucket.core.helper.html.datetimeago(release.registeredDate)
+
+
+ @if(hasWritePermission){
+
+ }
+
+
+
+ @helpers.markdown(
+ markdown = release.content getOrElse "No description provided.",
+ repository = repository,
+ enableWikiLink = false,
+ enableRefsLink = true,
+ enableLineBreaks = true,
+ enableTaskList = true,
+ hasWritePermission = hasWritePermission
+ )
+
+
+
Downloads
+ @if(hasWritePermission){
+
+
Attach release files by dragging & dropping, or selecting them.
+
+ }
+
+ @assets.map{ asset =>
+ -
+
+ @asset.label
+ @helpers.readableSize(Some(asset.size))
+
+ }
+
+
+
+
+
+
+
+}
+}