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 Organization = "io.github.gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "4.11.0" val GitBucketVersion = "4.12.0-SNAPSHOT"
val ScalatraVersion = "2.5.0" val ScalatraVersion = "2.5.0"
val JettyVersion = "9.3.9.v20160517" 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 MilestonesController, "/*")
context.mount(new IssuesController, "/*") context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*") context.mount(new PullRequestsController, "/*")
context.mount(new ReleaseController, "/*")
context.mount(new RepositorySettingsController, "/*") context.mount(new RepositorySettingsController, "/*")
// Create GITBUCKET_HOME directory if it does not exist // 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.10.0"),
new Version("4.11.0", new Version("4.11.0",
new LiquibaseMigration("update/gitbucket-core_4.11.xml") 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.model.Account
import gitbucket.core.service.RepositoryService.RepositoryInfo 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.servlet.Database
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
@@ -20,7 +20,11 @@ import org.apache.commons.io.{FileUtils, IOUtils}
* *
* This servlet saves uploaded file. * 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))) configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
@@ -78,6 +82,25 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
} getOrElse BadRequest() } 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") { post("/import") {
import JDBCUtil._ import JDBCUtil._
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin => 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: 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) 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 WebHookEventComponent
with ProtectedBranchComponent with ProtectedBranchComponent
with DeployKeyComponent with DeployKeyComponent
with ReleaseComponent
with ReleaseAssetComponent
object Profile extends CoreProfile 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

@@ -59,7 +59,7 @@ trait ActivityService {
Activities insert Activity(userName, repositoryName, activityUserName, Activities insert Activity(userName, repositoryName, activityUserName,
"open_issue", "open_issue",
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
@@ -135,7 +135,7 @@ trait ActivityService {
Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")), Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
currentDate) 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 = tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName, Activities insert Activity(userName, repositoryName, activityUserName,
"create_tag", "create_tag",
@@ -167,7 +167,7 @@ trait ActivityService {
None, None,
currentDate) 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, Activities insert Activity(userName, repositoryName, activityUserName,
"fork", "fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]", s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
@@ -190,6 +190,13 @@ trait ActivityService {
Some(message), Some(message),
currentDate) 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 = private def cut(value: String, length: Int): String =
if(value.length > length) value.substring(0, length) + "..." else value 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.controller.Context
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.SyntaxSugars._ 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._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.dateColumnType import gitbucket.core.model.Profile.dateColumnType
@@ -216,6 +216,10 @@ trait RepositoryService { self: AccountService =>
t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind) t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind)
}.map(_.pullRequest).list }.map(_.pullRequest).list
val releases = Releases.filter { t =>
t.byRepository(repository.userName, repository.repositoryName)
}.list
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName), JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName),
repository, repository,
@@ -225,6 +229,7 @@ trait RepositoryService { self: AccountService =>
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
), ),
releases.length,
getRepositoryManagers(repository.userName)) getRepositoryManagers(repository.userName))
} }
} }
@@ -458,20 +463,20 @@ trait RepositoryService { self: AccountService =>
object RepositoryService { object RepositoryService {
case class RepositoryInfo(owner: String, name: String, repository: Repository, 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]) { branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) {
/** /**
* Creates instance with issue count and pull request count. * 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]) = 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, repo.branchList, repo.tags, managers) 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]) = 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 httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name)
def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(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 = def getAttachedDir(owner: String, repository: String): File =
new File(getRepositoryFilesDir(owner, repository), "comments") 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. * Directory for files which are attached to issue.
*/ */
@@ -90,4 +96,4 @@ object Directory {
def getWikiRepositoryDir(owner: String, repository: String): File = def getWikiRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git") new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
} }

View File

@@ -14,6 +14,7 @@
case "reopen_issue" => detailActivity(activity, "issue-reopened") case "reopen_issue" => detailActivity(activity, "issue-reopened")
case "open_pullreq" => detailActivity(activity, "git-pull-request") case "open_pullreq" => detailActivity(activity, "git-pull-request")
case "merge_pullreq" => detailActivity(activity, "git-merge") case "merge_pullreq" => detailActivity(activity, "git-merge")
case "release" => detailActivity(activity, "package")
case "create_repository" => simpleActivity(activity, "repo") case "create_repository" => simpleActivity(activity, "repo")
case "create_branch" => simpleActivity(activity, "git-branch") case "create_branch" => simpleActivity(activity, "git-branch")
case "delete_branch" => simpleActivity(activity, "circle-slash") case "delete_branch" => simpleActivity(activity, "circle-slash")

View File

@@ -27,6 +27,7 @@
@menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length) @menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length)
@menuitem("/tags", "tags", "Tags", "tag", repository.tags.length) @menuitem("/tags", "tags", "Tags", "tag", repository.tags.length)
} }
@menuitem("/releases", "releases", "Releases", "package", repository.releaseCount)
@if(repository.repository.options.issuesOption != "DISABLE") { @if(repository.repository.options.issuesOption != "DISABLE") {
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount) @menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
@menuitem("/pulls", "pulls", "Pull requests", "git-pull-request", repository.pullCount) @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>
}
}