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){ +
+
+
+

New release for @tag

+ + + @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" + ) + + +
+ +
+
+
+
+} +} 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){ + + + + + + @repository.tags.reverse.map{ tag => + + + + } + +
@tagReleaseMap.count(_ => true) releases
+ @tagReleaseMap.get(tag.name).map{rel => + +
+

@rel.name

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

+
    + @releaseAssetsMap(rel).map{ asset => +
  • @asset.label
  • + } +
+ +
+
+ }.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.tag
+
+
+
+
+ @if(hasWritePermission){ + Edit title + Delete + } +
+ +

+ + @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)) +
  • + } +
+ +
+
+
+
+ +} +}