Add Release page. (close #607)

This commit is contained in:
KOUNOIKE Yuusuke
2017-04-16 20:56:50 +09:00
parent e576e14460
commit fd30facd8f
20 changed files with 767 additions and 13 deletions

View File

@@ -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"

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<createTable tableName="RELEASE">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="RELEASE_ID" type="int" nullable="false"/>
<column name="NAME" type="varchar(100)" nullable="false"/>
<column name="TAG" type="varchar(100)" nullable="false"/>
<column name="AUTHOR" type="varchar(100)" nullable="false"/>
<column name="CONTENT" type="text" nullable="true"/>
<column name="IS_DRAFT" type="boolean" nullable="false"/>
<column name="IS_PRERELEASE" type="boolean" nullable="false"/>
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_RELEASE_PK" tableName="RELEASE" columnNames="USER_NAME, REPOSITORY_NAME, RELEASE_ID"/>
<addUniqueConstraint constraintName="IDX_RELEASE_UNIQ" tableName="RELEASE" columnNames="USER_NAME, REPOSITORY_NAME, TAG"/>
<addForeignKeyConstraint constraintName="IDX_RELEASE_FK0" baseTableName="RELEASE" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<createTable tableName="RELEASE_ID">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="RELEASE_ID" type="int" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_RELEASE_ID_PK" tableName="RELEASE_ID" columnNames="USER_NAME, REPOSITORY_NAME"/>
<addForeignKeyConstraint constraintName="IDX_RELEASE_ID_FK1" baseTableName="RELEASE_ID" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<createTable tableName="RELEASE_ASSET">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="RELEASE_ID" type="int" nullable="false"/>
<column name="FILE_NAME" type="varchar(260)" nullable="false"/>
<column name="LABEL" type="varchar(100)" nullable="true"/>
<column name="SIZE" type="bigint" nullable="false"/>
<column name="UPLOADER" type="varchar(100)" nullable="false"/>
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_RELEASE_ASSET_PK" tableName="RELEASE_ASSET" columnNames="USER_NAME, REPOSITORY_NAME, RELEASE_ID, FILE_NAME"
/>
<addForeignKeyConstraint constraintName="IDX_RELEASE_ASSET_FK1" baseTableName="RELEASE_ASSET" baseColumnNames="USER_NAME, REPOSITORY_NAME, RELEASE_ID" referencedTableName="RELEASE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, RELEASE_ID"/>
</changeSet>

View File

@@ -47,6 +47,7 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
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

View File

@@ -31,5 +31,8 @@ object GitBucketCoreModule extends Module("gitbucket-core",
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")
)
)

View File

@@ -2,7 +2,7 @@ package gitbucket.core.controller
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 @@ import org.apache.commons.io.{FileUtils, IOUtils}
*
* 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 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
} 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 =>

View File

@@ -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()
})
}

View File

@@ -63,4 +63,15 @@ protected[model] trait TemplateComponent { self: Profile =>
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)
}
}

View File

@@ -55,5 +55,7 @@ trait CoreProfile extends ProfileProvider with Profile
with WebHookEventComponent
with ProtectedBranchComponent
with DeployKeyComponent
with ReleaseComponent
with ReleaseAssetComponent
object Profile extends CoreProfile

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -190,6 +190,13 @@ trait ActivityService {
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
}

View File

@@ -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
}
}
}

View File

@@ -3,7 +3,7 @@ package gitbucket.core.service
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 @@ trait RepositoryService { self: AccountService =>
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 @@ trait RepositoryService { self: AccountService =>
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
releases.length,
getRepositoryManagers(repository.userName))
}
}
@@ -458,20 +463,20 @@ trait RepositoryService { self: AccountService =>
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)

View File

@@ -54,6 +54,12 @@ object Directory {
def getAttachedDir(owner: String, repository: String): File =
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.
*/

View File

@@ -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")

View File

@@ -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)

View File

@@ -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){
<form action="@helpers.url(repository)/releases/@tag/create" method="POST" validate="true" class="form-group">
<div class="row-fluid">
<div class="col-md-9">
<h3>New release for @tag</h3>
<span id="error-title" class="error"></span>
<input type="text" id="release-name" name="name" class="form-control" value="" placeholder="Title" style="margin-bottom: 6px;" autofocus/>
@gitbucket.core.helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true,
completionContext = "releases",
style = "height: 200px; max-height: 500px;",
elastic = true,
placeholder = "Describe this release"
)
<input type="checkbox" id="release-isprerelease" name="isprerelease"/>
<label for="release-isprerelease">This is a pre-release</label>
<div class="align-right">
<input type="submit" class="btn btn-success" value="Submit new release"/>
</div>
</div>
</div>
</form>
}
}

View File

@@ -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"){
<textarea id="edit-content" class="form-control">@content.getOrElse("")</textarea>
}
<div>
<input type="button" id="cancel-release" class="btn btn-danger" value="Cancel"/>
<input type="button" id="update-release" class="btn btn-default pull-right" value="Update comment"/>
</div>
<script>
$(function(){
var callback = function(data){
$('#update, #cancel').removeAttr('disabled');
$('#release-note').empty().html(data.content);
};
$('#update-release').click(function(){
$('#update, #cancel').attr('disabled', 'disabled');
$.ajax({
url: '@context.path/@repository.owner/@repository.name/releases/edit/@releaseId',
type: 'POST',
data: {
content : $('#edit-content').val()
}
}).done(
callback
).fail(function(req) {
$('#update, #cancel').removeAttr('disabled');
});
});
$('#cancel-release').click(function(){
$('#update, #cancel').attr('disabled', 'disabled');
$.get('@context.path/@repository.owner/@repository.name/releases/_data/@releaseId', callback);
return false;
});
});
</script>

View File

@@ -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){
<table class="table table-bordered table-releases">
<thead>
<tr><th>@tagReleaseMap.count(_ => true) releases</div></th></tr>
</thead>
<tbody>
@repository.tags.reverse.map{ tag =>
<tr>
<td>
@tagReleaseMap.get(tag.name).map{rel =>
<div class="col-md-1"><a href="@helpers.url(repository)/tree/@tag.name"><i class="octicon octicon-tag"></i>@tag.name</a></div>
<div class="col-md-11" style="border-left: 1px solid #eee">
<h3><a href="@helpers.url(repository)/releases/@rel.releaseId">@rel.name</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
<span class="muted">
@helpers.avatar(rel.author, 20)
@helpers.user(rel.author, styleClass="username strong")
released this @gitbucket.core.helper.html.datetimeago(rel.registeredDate)
</span>
</div>
<div class="panel-body release-note markdown-body">
@helpers.markdown(
markdown = rel.content getOrElse "No description provided.",
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
)
</div>
<h2>Downloads</h2>
<ul style="list-style: none">
@releaseAssetsMap(rel).map{ asset =>
<li><i class="octicon octicon-file"></i><a href="@helpers.url(repository)/releases/@rel.releaseId/assets/@asset.fileName">@asset.label</a></li>
}
</ul>
<ul style="list-style: none;">
<li><a href="@helpers.url(repository)/archive/@{helpers.encodeRefName(rel.tag)}.zip"><i class="octicon octicon-file-zip"></i>ZIP</a></li>
<li><a href="@helpers.url(repository)/archive/@{helpers.encodeRefName(rel.tag)}.tar.gz"><i class="octicon octicon-file-zip"></i>TAR.GZ</a></li>
</ul>
</div>
</div>
}.getOrElse{
<div class="col-md-1">
@gitbucket.core.helper.html.datetimeago(tag.time) <i class="octicon octicon-tag"></i>
</div>
<div class="col-md-11" style="border-left: 1px solid #eee">
<a href="@helpers.url(repository)/tree/@tag.name"><i class="octicon octicon-tag"></i>@tag.name</a>
@if(hasWritePermission){
<div class="show-title pull-right">
<a class="btn btn-success" href="@helpers.url(repository)/releases/@tag.name/create">Create release</a>
</div>
}
</div>
}
</td>
</tr>
}
</tbody>
</table>
}}

View File

@@ -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){
<div class="row">
<div class="col-md-1">
<div><a href="@helpers.url(repository)/tree/@release.tag"><i class="octicon octicon-tag"></i>@release.tag</a></div>
</div>
<div class="col-md-11" style="border-left: 1px solid #eee">
<div>
<div class="show-title pull-right">
@if(hasWritePermission){
<a class="btn btn-default" href="#" id="edit">Edit title</a>
<a class="btn btn-danger" href="#" id="delete-release">Delete</a>
}
</div>
<div class="edit-title pull-right" style="display: none;">
<a class="btn btn-success" href="#" id="update">Save</a> <a class="btn btn-default" href="#" id="cancel">Cancel</a>
</div>
<h1 class="body-title">
<span class="show-title">
<span id="show-title">@release.name</span>
</span>
<span class="edit-title" style="display: none;">
<span id="error-edit-title" class="error"></span>
<input type="text" class="form-control" style="width: 700px;" id="edit-title" value="@release.name"/>
</span>
</h1>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<span class="muted">
@helpers.avatar(release.author, 20)
@helpers.user(release.author, styleClass="username strong") released this @gitbucket.core.helper.html.datetimeago(release.registeredDate)
</span>
<span class="pull-right">
@if(hasWritePermission){
<a href="#"><i class="octicon octicon-pencil" aria-label="Edit" id="edit-notes"></i></a>&nbsp;
}
</span>
</div>
<div class="panel-body markdown-body" id="release-note">
@helpers.markdown(
markdown = release.content getOrElse "No description provided.",
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
)
</div>
<div class="panel-body">
<h2>Downloads</h2>
@if(hasWritePermission){
<div style="border: 3px dashed #ccc; background-color: #eee" >
<div id="drop" class="clickable">Attach release files by dragging &amp; dropping, or selecting them.</div>
</div>
}
<ul style="list-style: none;" id="attachedFiles">
@assets.map{ asset =>
<li>
<i class="octicon octicon-file"></i>
<a href="@helpers.url(repository)/releases/@release.releaseId/assets/@asset.fileName">@asset.label</a>
<span class="label label-default">@helpers.readableSize(Some(asset.size))</span>
</li>
}
</ul>
<ul style="list-style: none;">
<li><a href="@helpers.url(repository)/archive/@{helpers.encodeRefName(release.tag)}.zip"><i class="octicon octicon-file-zip"></i>ZIP</a></li>
<li><a href="@helpers.url(repository)/archive/@{helpers.encodeRefName(release.tag)}.tar.gz"><i class="octicon octicon-file-zip"></i>TAR.GZ</a></li>
</ul>
</div>
</div>
</div>
</div>
<script>
$(function(){
$('#edit').click(function(){
$('.edit-title').show();
$('.show-title').hide();
return false;
});
$('#update').click(function(){
$(this).attr('disabled', 'disabled');
$.ajax({
url: '@helpers.url(repository)/releases/edit_title/@release.releaseId',
type: 'POST',
data: {
title : $('#edit-title').val()
}
}).done(function(data){
$('#show-title').empty().text(data.title);
$('#cancel').click();
$(this).removeAttr('disabled');
}).fail(function(req){
$(this).removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
});
return false;
});
$('#cancel').click(function(){
$('.edit-title').hide();
$('.show-title').show();
return false;
});
$('#edit-notes').click(function(){
var id = @release.releaseId;
var url = '@helpers.url(repository)/releases/_data/' + id;
var $content = $('#release-note');
$.get(url,
{
dataType : 'html'
},
function(data){
$content.empty().html(data);
});
return false;
});
$('#delete-release').click(function(){
if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id');
$.post('@helpers.url(repository)/issue_comments/delete/' + id,
function(data){
if(data > 0) {
$('#comment-' + id).prev('div.issue-avatar-image').remove();
$('#comment-' + id).remove();
}
});
}
return false;
});
$("#drop").dropzone({
url: '@context.path/upload/release/@repository.owner/@repository.name/@release.releaseId',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attach = '<li><a href="@context.baseUrl/@repository.owner/@repository.name/_release/@release.releaseId/' + id + '">'
+ '<i class="octicon octicon-file"></i>' + file.name + '</a></li>';
$('#attachedFiles').append(attach);
}
});
});
</script>
}
}