diff --git a/src/main/resources/update/gitbucket-core_4.21.xml b/src/main/resources/update/gitbucket-core_4.21.xml new file mode 100644 index 0000000..842389c --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.21.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index 231da65..08458b5 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -2,7 +2,7 @@ import java.util.EnumSet import javax.servlet._ -import gitbucket.core.controller._ +import gitbucket.core.controller.{ReleaseController, _} import gitbucket.core.plugin.PluginRegistry import gitbucket.core.service.SystemSettingsService import gitbucket.core.servlet._ @@ -47,6 +47,7 @@ filter.mount(new MilestonesController, "/*") filter.mount(new IssuesController, "/*") filter.mount(new PullRequestsController, "/*") + filter.mount(new ReleaseController, "/*") filter.mount(new RepositorySettingsController, "/*") context.addFilter("compositeScalatraFilter", filter) diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index b61153a..29bc472 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -48,5 +48,8 @@ new Version("4.19.1"), new Version("4.19.2"), new Version("4.19.3"), - new Version("4.20.0") + new Version("4.20.0"), + new Version("4.21.0", + new LiquibaseMigration("update/gitbucket-core_4.21.xml") + ) ) diff --git a/src/main/scala/gitbucket/core/controller/FileUploadController.scala b/src/main/scala/gitbucket/core/controller/FileUploadController.scala index 5bb0ce1..ca00ee2 100644 --- a/src/main/scala/gitbucket/core/controller/FileUploadController.scala +++ b/src/main/scala/gitbucket/core/controller/FileUploadController.scala @@ -1,7 +1,7 @@ package gitbucket.core.controller import gitbucket.core.model.Account -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._ @@ -19,7 +19,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(FileUtil.MaxFileSize))) @@ -84,6 +88,20 @@ } getOrElse BadRequest() } + post("/release/:owner/:repository/:tag"){ + session.get(Keys.Session.LoginAccount).collect { case _: Account => + val owner = params("owner") + val repository = params("repository") + val tag = params("tag") + execute({ (file, fileId) => + FileUtils.writeByteArrayToFile( + new java.io.File(getReleaseFilesDir(owner, repository), tag + "/" + fileId), + file.get + ) + }, _ => 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..c1cdebf --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/ReleasesController.scala @@ -0,0 +1,140 @@ +package gitbucket.core.controller + +import java.io.File + +import gitbucket.core.service.{AccountService, ActivityService, ReleaseService, RepositoryService} +import gitbucket.core.util.{FileUtil, ReadableUsersAuthenticator, ReferrerAuthenticator, WritableUsersAuthenticator} +import gitbucket.core.util.Directory._ +import gitbucket.core.util.Implicits._ +import org.scalatra.forms._ +import gitbucket.core.releases.html +import org.apache.commons.io.FileUtils +import scala.collection.JavaConverters._ + +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 ReleaseForm( + name: String, + content: Option[String] + ) + + val releaseForm = mapping( + "name" -> trim(text(required)), + "content" -> trim(optional(text())) + )(ReleaseForm.apply) + + get("/:owner/:repository/releases")(referrersOnly {repository => + val releases = getReleases(repository.owner, repository.name) + val assets = getReleaseAssetsMap(repository.owner, repository.name) + + html.list( + repository, + repository.tags.reverse.map { tag => + (tag, releases.find(_.tag == tag.name).map { release => (release, assets(release)) }) + }, + 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") + (for { + release <- getRelease(repository.owner, repository.name, releaseId) + asset <- getReleaseAsset(repository.owner, repository.name, releaseId, fileId) + } yield { + response.setHeader("Content-Disposition", s"attachment; filename=${asset.label}") + Some(RawData( + FileUtil.getMimeType(asset.label), + new File(getReleaseFilesDir(repository.owner, repository.name), release.tag + "/" + fileId) + )) + }).getOrElse(NotFound()) + }) + + get("/:owner/:repository/releases/:tag/create")(writableUsersOnly {repository => + html.form(repository, params("tag"), None) + }) + + post("/:owner/:repository/releases/:tag/create", releaseForm)(writableUsersOnly { (form, repository) => + val tag = params("tag") + val loginAccount = context.loginAccount.get + + // Insert into RELEASE + val release = createRelease(repository.owner, repository.name, form.name, form.content, tag, loginAccount) + + // Insert into RELEASE_ASSET + request.getParameterNames.asScala.filter(_.startsWith("file:")).foreach { paramName => + val Array(_, fileId) = paramName.split(":") + val fileName = params(paramName) + val size = new java.io.File(getReleaseFilesDir(repository.owner, repository.name), tag + "/" + fileId).length + + createReleaseAsset(repository.owner, repository.name, release.releaseId, fileId, fileName, size, loginAccount) + } + + recordReleaseActivity(repository.owner, repository.name, loginAccount.userName, release.releaseId, release.name) + + redirect(s"/${release.userName}/${release.repositoryName}/releases/${release.releaseId}") + }) + + get("/:owner/:repository/releases/:id/edit")(writableUsersOnly {repository => + val releaseId = params("id").toInt + getRelease(repository.owner, repository.name, releaseId).map { release => + html.form(repository, release.tag, Some(release, getReleaseAssets(repository.owner, repository.name, releaseId))) + }.getOrElse(NotFound()) + }) + + post("/:owner/:repository/releases/:id/edit", releaseForm)(writableUsersOnly { (form, repository) => + val releaseId = params("id").toInt + val loginAccount = context.loginAccount.get + + getRelease(repository.owner, repository.name, releaseId).map { release => + // Update RELEASE + updateRelease(repository.owner, repository.name, releaseId, form.name, form.content) + + // Delete and Insert RELEASE_ASSET + deleteReleaseAssets(repository.owner, repository.name, releaseId) + + request.getParameterNames.asScala.filter(_.startsWith("file:")).foreach { paramName => + val Array(_, fileId) = paramName.split(":") + val fileName = params(paramName) + val size = new java.io.File(getReleaseFilesDir(repository.owner, repository.name), release.tag + "/" + fileId).length + + createReleaseAsset(repository.owner, repository.name, release.releaseId, fileId, fileName, size, loginAccount) + } + + redirect(s"/${release.userName}/${release.repositoryName}/releases/${release.releaseId}") + }.getOrElse(NotFound()) + }) + + post("/:owner/:repository/releases/:id/delete")(writableUsersOnly { repository => + val releaseId = params("id") + getRelease(repository.owner, repository.name, releaseId).foreach { release => + FileUtils.deleteDirectory(new File(getReleaseFilesDir(repository.owner, repository.name), release.tag)) + } + deleteRelease(repository.owner, repository.name, releaseId) + redirect(s"/${repository.owner}/${repository.name}/releases") + }) + +} diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index f6c0bb4..b1e5b3d 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -142,6 +142,9 @@ FileUtils.moveDirectory(dir, getRepositoryFilesDir(repository.owner, form.repositoryName)) } } + // Delete parent directory + FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name)) + // Call hooks PluginRegistry().getRepositoryHooks.foreach(_.renamed(repository.owner, repository.name, form.repositoryName)) } diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 48cc2d3..3a3b3b0 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -627,8 +627,8 @@ /** * Displays tags. */ - get("/:owner/:repository/tags")(referrersOnly { - html.tags(_) + get("/:owner/:repository/tags")(referrersOnly { repository => + redirect(s"${repository.owner}/${repository.name}/releases") }) /** diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index 807456b..a70af57 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -63,5 +63,7 @@ with AccountWebHookEventComponent 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..27fdbc0 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Release.scala @@ -0,0 +1,49 @@ +package gitbucket.core.model + +trait ReleaseComponent extends TemplateComponent { + self: Profile => + + import profile.api._ + import self._ + + lazy val Releases = TableQuery[Releases] + + class Releases(tag_ : Tag) extends Table[Release](tag_, "RELEASE") with BasicTemplate { + val releaseId = column[Int]("RELEASE_ID", O AutoInc) + val name = column[String]("NAME") + val tag = column[String]("TAG") + val author = column[String]("AUTHOR") + val content = column[Option[String]]("CONTENT") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + + def * = (userName, repositoryName, releaseId, name, tag, author, content, 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) + + 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) + } + +} + +case class Release( + userName: String, + repositoryName: String, + releaseId: Int = 0, + name: String, + tag: String, + author: String, + content: Option[String], + 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..156ef75 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/ReleasesAsset.scala @@ -0,0 +1,47 @@ +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 BasicTemplate { + val releaseId = column[Int]("RELEASE_ID") + val releaseAssetId = column[Int]("RELEASE_ASSET_ID", O AutoInc) + 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, releaseAssetId, 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) + + 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) + } + +} + +case class ReleaseAsset( + userName: String, + repositoryName: String, + releaseId: Int, + releaseAssetId: Int = 0, + 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 f75a15c..2909761 100644 --- a/src/main/scala/gitbucket/core/service/ActivityService.scala +++ b/src/main/scala/gitbucket/core/service/ActivityService.scala @@ -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..8aaa797 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/ReleaseService.scala @@ -0,0 +1,100 @@ +package gitbucket.core.service + +import gitbucket.core.controller.Context +import gitbucket.core.model.{Account, Release, ReleaseAsset} +import gitbucket.core.util.StringUtil._ +import gitbucket.core.model.Profile.profile.blockingApi._ +import gitbucket.core.model.Profile._ +import gitbucket.core.model.Profile.dateColumnType + +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( + userName = owner, + repositoryName = repository, + releaseId = releaseId, + fileName = fileName, + label = label, + size = size, + uploader = loginAccount.userName, + registeredDate = currentDate, + updatedDate = currentDate + ) + } + + def getReleaseAssets(owner: String, repository: String, releaseId: Int)(implicit s: Session): Seq[ReleaseAsset] = { + ReleaseAssets.filter(x => x.byRelease(owner, repository, releaseId)).list + } + + def getReleaseAssets(owner: String, repository: String, releaseId: String)(implicit s: Session): Seq[ReleaseAsset] = { + if (isInteger(releaseId)) + getReleaseAssets(owner, repository, releaseId.toInt) + else + Seq.empty + } + + def getReleaseAssetsMap(owner: String, repository: String)(implicit s: Session): Map[Release, Seq[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(owner: String, repository: String, name: String, content: Option[String], tag: String, + loginAccount: Account)(implicit context: Context, s: Session): Release = { + Releases insert Release( + userName = owner, + repositoryName = repository, + name = name, + tag = tag, + author = loginAccount.userName, + content = content, + registeredDate = currentDate, + updatedDate = currentDate + ) + getReleaseByTag(owner, repository, tag).get + } + + def getReleases(owner: String, repository: String)(implicit s: Session): Seq[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 getReleaseByTag(owner: String, repository: String, tag: String)(implicit s: Session): Option[Release] = { + Releases filter (_.byTag(owner, repository, tag)) 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 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 7911b2e..20448c8 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 @@ -75,6 +75,8 @@ val protectedBranches = ProtectedBranches .filter(_.byRepository(oldUserName, oldRepositoryName)).list val protectedBranchContexts = ProtectedBranchContexts.filter(_.byRepository(oldUserName, oldRepositoryName)).list val deployKeys = DeployKeys .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val releases = Releases .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val releaseAssets = ReleaseAssets .filter(_.byRepository(oldUserName, oldRepositoryName)).list Repositories.filter { t => (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind) @@ -120,6 +122,8 @@ ProtectedBranches .insertAll(protectedBranches.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) ProtectedBranchContexts.insertAll(protectedBranchContexts.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) DeployKeys .insertAll(deployKeys .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + Releases .insertAll(releases .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + ReleaseAssets .insertAll(releaseAssets .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) // Update source repository of pull requests PullRequests.filter { t => @@ -173,6 +177,8 @@ RepositoryWebHooks .filter(_.byRepository(userName, repositoryName)).delete RepositoryWebHookEvents .filter(_.byRepository(userName, repositoryName)).delete DeployKeys .filter(_.byRepository(userName, repositoryName)).delete + ReleaseAssets .filter(_.byRepository(userName, repositoryName)).delete + Releases .filter(_.byRepository(userName, repositoryName)).delete Repositories .filter(_.byRepository(userName, repositoryName)).delete // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME @@ -514,7 +520,7 @@ this(repo.owner, repo.name, model, issueCount, pullCount, forkedCount, repo.branchList, repo.tags, managers) /** - * Creates instance without issue count and pull request count. + * Creates instance without issue and pull request 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) @@ -531,6 +537,10 @@ (id, path.substring(id.length).stripPrefix("/")) } + +// def getReleaseByTag(tag: String)(implicit s: Session): Option[Release] = { +// Releases filter (_.byTag(owner, name, tag)) firstOption +// } } def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git" @@ -539,5 +549,4 @@ context.settings.sshAddress.map { x => s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git" } } else None def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}" - } diff --git a/src/main/scala/gitbucket/core/util/Directory.scala b/src/main/scala/gitbucket/core/util/Directory.scala index e4d3b4a..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 = 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 6dac32a..04d9de0 100644 --- a/src/main/twirl/gitbucket/core/menu.scala.html +++ b/src/main/twirl/gitbucket/core/menu.scala.html @@ -33,8 +33,8 @@ @menuitem("", "files", "Files", "code") @if(repository.branchList.nonEmpty) { @menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length) - @menuitem("/tags", "tags", "Tags", "tag", repository.tags.length) } + @menuitem("/releases", "releases", "Releases", "tag", repository.tags.length) @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/form.scala.html b/src/main/twirl/gitbucket/core/releases/form.scala.html new file mode 100644 index 0000000..7dacfab --- /dev/null +++ b/src/main/twirl/gitbucket/core/releases/form.scala.html @@ -0,0 +1,87 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + tag: String, + release: Option[(gitbucket.core.model.Release, Seq[gitbucket.core.model.ReleaseAsset])])(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.view.helpers + +@action = { + @release.map { case (release, _) => + @helpers.url(repository)/releases/@release.releaseId/edit + }.getOrElse { + @helpers.url(repository)/releases/@helpers.encodeRefName(tag)/create + } +} + +@gitbucket.core.html.main(s"New Release - ${repository.owner}/${repository.name}", Some(repository)){ + @gitbucket.core.html.menu("releases", repository){ +
+
+
+ @if(release.isEmpty){ +

New release for @tag

+ } else { +

Update release for @tag

+ } + + + @gitbucket.core.helper.html.preview( + repository = repository, + content = release.flatMap { case (release, _) => release.content }.getOrElse(""), + enableWikiLink = false, + enableRefsLink = true, + enableLineBreaks = true, + enableTaskList = true, + hasWritePermission = true, + completionContext = "releases", + style = "height: 200px; max-height: 500px;", + elastic = true, + placeholder = "Describe this release" + ) +
    + @release.map { case (release, assets) => + @assets.map { asset => +
  • + @asset.label + (remove) + +
  • + } + } +
+
+
Attach release files by dragging & dropping, or selecting them.
+
+
+ @if(release.isEmpty){ + + } else { + + } +
+
+
+
+ } +} + 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..ed8f4a2 --- /dev/null +++ b/src/main/twirl/gitbucket/core/releases/list.scala.html @@ -0,0 +1,64 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + releases: Seq[(gitbucket.core.util.JGitUtil.TagInfo, Option[(gitbucket.core.model.Release, Seq[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){ + + + + + + @releases.map { case (tag, release) => + + + + } + +
@releases.length releases
+
+ @tag.name
+ @tag.id.substring(0, 7)
+ @gitbucket.core.helper.html.datetimeago(tag.time) +
+
+
+ @release.map { case (release, assets) => +

@release.name

+

+ @helpers.avatar(release.author, 20) @helpers.user(release.author, styleClass="username") released this @gitbucket.core.helper.html.datetimeago(release.registeredDate) +

+ @helpers.markdown( + markdown = release.content getOrElse "No description provided.", + repository = repository, + enableWikiLink = false, + enableRefsLink = true, + enableLineBreaks = true, + enableTaskList = true, + hasWritePermission = hasWritePermission + ) + }.getOrElse { + @if(hasWritePermission){ + + } + } +

Downloads

+ +
+
+
+ } +} 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..369ee50 --- /dev/null +++ b/src/main/twirl/gitbucket/core/releases/release.scala.html @@ -0,0 +1,65 @@ +@(release: gitbucket.core.model.Release, + assets: Seq[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){ +
+
+ @defining(repository.tags.find(_.name == release.tag)){ tag => + @tag.map { tag => + @tag.name
+ @tag.id.substring(0, 7)
+ @gitbucket.core.helper.html.datetimeago(tag.time) + } + } +
+
+
+

+ @release.name + @if(hasWritePermission){ +
+
+ Edit + +
+
+ } +

+

+ @helpers.avatar(release.author, 20) @helpers.user(release.author, styleClass="username") released this @gitbucket.core.helper.html.datetimeago(release.registeredDate) +

+ @helpers.markdown( + markdown = release.content getOrElse "No description provided.", + repository = repository, + enableWikiLink = false, + enableRefsLink = true, + enableLineBreaks = true, + enableTaskList = true, + hasWritePermission = hasWritePermission + ) +

Downloads

+ +
+
+
+ } +} + diff --git a/src/main/twirl/gitbucket/core/repo/tags.scala.html b/src/main/twirl/gitbucket/core/repo/tags.scala.html deleted file mode 100644 index c3503aa..0000000 --- a/src/main/twirl/gitbucket/core/repo/tags.scala.html +++ /dev/null @@ -1,29 +0,0 @@ -@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) -@import gitbucket.core.view.helpers -@gitbucket.core.html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @gitbucket.core.html.menu("tags", repository){ - - - - - - - - - - - @repository.tags.reverseMap { tag => - - - - - - - } - -
TagDateCommitDownload
@tag.name@gitbucket.core.helper.html.datetimeago(tag.time, false)@tag.id.substring(0, 10) - ZIP - TAR.GZ -
- } -}