diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 49d184a..507d345 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,7 @@ # Guideline for Issues -- At first, See [FAQ](https://github.com/gitbucket/gitbucket/wiki/FAQ) and check issues whether there is a same question or request in the past. +- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past. - If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue. -- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja). +- We can also support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja). - Write an issue in English. At least, write subject in English. +- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. diff --git a/README.md b/README.md index d3813a8..e13546e 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,35 @@ -GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.png)](https://gitter.im/gitbucket/gitbucket) [![Build Status](https://travis-ci.org/gitbucket/gitbucket.svg?branch=master)](https://travis-ci.org/gitbucket/gitbucket) +0;95;0cGitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.png)](https://gitter.im/gitbucket/gitbucket) [![Build Status](https://travis-ci.org/gitbucket/gitbucket.svg?branch=master)](https://travis-ci.org/gitbucket/gitbucket) ========= -GitBucket is a Git platform powered by Scala offering: +GitBucket is a Git web platform powered by Scala offering: + - Easy installation +- Intuitive UI - High extensibility by plugins - API compatibility with GitHub +You can try an [online demo](https://gitbucket.herokuapp.com/) *(ID: root / Pass: root)* of GitBucket, and also get the latest information at [GitBucket News](https://gitbucket.github.io/gitbucket-news/). + Features -------- -The current version of GitBucket provides a basic features below: +The current version of GitBucket provides many features such as: -- Public / Private Git repository (http and ssh access) +- Public / Private Git repositories (with http/https and ssh access) - GitLFS support -- Repository viewer includes online file editor -- Issues, Pull request and Wiki for repositories -- Activity timeline and email notification +- Repository viewer including an online file editor +- Issues, Pull Requests and Wiki for repositories +- Activity timeline and email notifications - Account and group management with LDAP integration -- Plug-in system +- a Plug-in system If you want to try the development version of GitBucket, see the [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md). Installation -------- -GitBucket requires **Java8**. You have to install it if it is not already installed. +GitBucket requires **Java8**. You have to install it, if it is not already installed. 1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`. -2. Go to `http://[hostname]:8080/` and log in with **root** / **root**. +2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**. You can specify following options: @@ -45,24 +49,37 @@ Plugins -------- -GitBucket has a plug-in system to allow extensions to GitBucket. We provide some official plug-ins: +GitBucket has a plug-in system that allows extra functionality. Officially the following plug-ins are provided: - [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) - [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin) +- [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin) You can find more plugins made by the community at [GitBucket community plugins](http://gitbucket-plugins.github.io/). Support -------- -- If you have any questions about GitBucket, send it to the [gitter room](https://gitter.im/gitbucket/gitbucket) before opening an issue. -- Make sure check whether there is the same question or request in the past. -- When raise a new issue, write at least the subject in **English**. -- We can also provide support in Japanese at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja). -- The first priority of GitBucket is easy installation and API compatibility with GitHub, so we might reject if your request is against it. +- If you have any questions about GitBucket, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past. +- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue. +- We can also provide support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja). +- Write an issue in English. At least, write subject in English. +- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. Release Notes ------------- +### 4.12 - 30 Apr 2017 +- [Gist plug-in](https://github.com/gitbucket/gitbucket-gist-plugin) provides JavaScript to embed snippet +- Dropdown menu filter in the branch comparing page +- Caution for the embedded H2 database + +### 4.11 - 1 Apr 2017 +- Deploy keys support +- Auto generate avatar images +- Collaborators of the private forked repository are copied from the original repository +- Cache avatar images in the browser +- New extension point to receive events about repository + ### 4.10 - 25 Feb 2017 - Update to Scala 2.12, Scalatra 2.5 and Slick 3.2 - Display file size in the file viewer diff --git a/build.sbt b/build.sbt index 17299e9..54a056f 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ val Organization = "io.github.gitbucket" val Name = "gitbucket" -val GitBucketVersion = "4.10.0" +val GitBucketVersion = "4.12.0" val ScalatraVersion = "2.5.0" val JettyVersion = "9.3.9.v20160517" @@ -10,7 +10,7 @@ organization := Organization name := Name version := GitBucketVersion -scalaVersion := "2.12.1" +scalaVersion := "2.12.2" // dependency settings resolvers ++= Seq( @@ -21,8 +21,8 @@ "amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/" ) libraryDependencies ++= Seq( - "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.6.1.201703071140-r", - "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.6.1.201703071140-r", + "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.7.0.201704051617-r", + "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.7.0.201704051617-r", "org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.json4s" %% "json4s-jackson" % "3.5.0", diff --git a/doc/how_to_run.md b/doc/how_to_run.md index ea31909..237dd68 100644 --- a/doc/how_to_run.md +++ b/doc/how_to_run.md @@ -4,15 +4,15 @@ Run for Development -------- -If you want to test GitBucket, input following command at the root directory of the source tree. +If you want to test GitBucket, type the following command in the root directory of the source tree. ``` $ sbt ~jetty:start ``` -Then access to `http://localhost:8080/` by your browser. The default administrator account is `root` and password is `root`. +Then access `http://localhost:8080/` in your browser. The default administrator account is `root` and password is `root`. -Source code modification is detected and reloaded automatically. You can modify logging configuration by editing `src/main/resources/logback-dev.xml`. +Source code modifications are detected and a reloaded happens automatically. You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`. Build war file -------- @@ -23,9 +23,9 @@ $ sbt package ``` -`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`. +`gitbucket_2.12-x.x.x.war` is generated into `target/scala-2.12`. -To build executable war file, run +To build an executable war file, run ``` $ sbt executable @@ -35,8 +35,8 @@ Run tests spec --------- -To run the full serie of tests, run the following command: +To run the full series of tests, run the following command: ``` -sbt test +$ sbt test ``` diff --git a/doc/notification.md b/doc/notification.md index 90a88bb..f9fb1e0 100644 --- a/doc/notification.md +++ b/doc/notification.md @@ -1,7 +1,7 @@ Notification Email ======== -GitBucket sends email to target users by enabling the notification email by an administrator. +GitBucket can send email notification to users if this feature is enabled by an administrator. The timing of the notification are as follows: @@ -20,4 +20,4 @@ * collaborators * participants -However, the operation in person is excluded from the target. +However, the person performing the operation is excluded from the notification. diff --git a/doc/release.md b/doc/release.md index 682dc88..d417cb7 100644 --- a/doc/release.md +++ b/doc/release.md @@ -34,8 +34,6 @@ Generate release files -------- -Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https://maven.apache.org/). - ### Make release war file Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`. @@ -52,4 +50,12 @@ $ sbt publish-signed ``` -Then operate release sequence at https://oss.sonatype.org/. +Then logged-in https://oss.sonatype.org/ and delete following files from the staging repository: + +- gitbucket_2.12-x.x.x.war +- gitbucket_2.12-x.x.x.war.asc +- gitbucket_2.12-x.x.x.war.asc.md5 +- gitbucket_2.12-x.x.x.war.asc.sha1 +- gitbucket_2.12-x.x.x.war.md5 + +At last, close and release the repository. diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java index d810941..04ae950 100644 --- a/src/main/java/JettyLauncher.java +++ b/src/main/java/JettyLauncher.java @@ -8,6 +8,8 @@ public class JettyLauncher { public static void main(String[] args) throws Exception { + System.setProperty("java.awt.headless", "true"); + String host = null; int port = 8080; InetSocketAddress address = null; @@ -19,19 +21,25 @@ if(arg.startsWith("--") && arg.contains("=")) { String[] dim = arg.split("="); if(dim.length >= 2) { - if(dim[0].equals("--host")) { - host = dim[1]; - } else if(dim[0].equals("--port")) { - port = Integer.parseInt(dim[1]); - } else if(dim[0].equals("--prefix")) { - contextPath = dim[1]; - if(!contextPath.startsWith("/")){ - contextPath = "/" + contextPath; - } - } else if(dim[0].equals("--gitbucket.home")){ - System.setProperty("gitbucket.home", dim[1]); - } else if(dim[0].equals("--temp_dir")){ - tmpDirPath = dim[1]; + switch (dim[0]) { + case "--host": + host = dim[1]; + break; + case "--port": + port = Integer.parseInt(dim[1]); + break; + case "--prefix": + contextPath = dim[1]; + if (!contextPath.startsWith("/")) { + contextPath = "/" + contextPath; + } + break; + case "--gitbucket.home": + System.setProperty("gitbucket.home", dim[1]); + break; + case "--temp_dir": + tmpDirPath = dim[1]; + break; } } } diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index dd8665d..b33a909 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -31,5 +31,6 @@ new Version("4.10.0"), new Version("4.11.0", new LiquibaseMigration("update/gitbucket-core_4.11.xml") - ) + ), + new Version("4.12.0") ) diff --git a/src/main/scala/gitbucket/core/api/CreateAStatus.scala b/src/main/scala/gitbucket/core/api/CreateAStatus.scala index 3871999..f8c087f 100644 --- a/src/main/scala/gitbucket/core/api/CreateAStatus.scala +++ b/src/main/scala/gitbucket/core/api/CreateAStatus.scala @@ -19,8 +19,8 @@ def isValid: Boolean = { CommitState.valueOf(state).isDefined && // only http - target_url.filterNot(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length<255).isEmpty && - context.filterNot(f => f.length<255).isEmpty && - description.filterNot(f => f.length<1000).isEmpty + target_url.forall(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length < 255) && + context.forall(f => f.length < 255) && + description.forall(f => f.length < 1000) } } diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 1394702..51dd1d1 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -15,7 +15,6 @@ import org.apache.commons.io.FileUtils import org.scalatra.i18n.Messages import org.scalatra.BadRequest -import java.util.Date class AccountController extends AccountControllerBase @@ -61,31 +60,31 @@ val sshKeyForm = mapping( "title" -> trim(label("Title", text(required, maxlength(100)))), - "publicKey" -> trim(label("Key" , text(required, validPublicKey))) + "publicKey" -> trim2(label("Key" , text(required, validPublicKey))) )(SshKeyForm.apply) val personalTokenForm = mapping( - "note" -> trim(label("Token", text(required, maxlength(100)))) + "note" -> trim(label("Token", text(required, maxlength(100)))) )(PersonalTokenForm.apply) case class NewGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String], members: String) case class EditGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) val newGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "description" -> trim(label("Group description", optional(text()))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))) + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) )(NewGroupForm.apply) val editGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), "description" -> trim(label("Group description", optional(text()))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))), - "clearImage" -> trim(label("Clear image" ,boolean())) + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())) )(EditGroupForm.apply) case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) @@ -150,20 +149,21 @@ get("/:userName/_avatar"){ val userName = params("userName") - getAccountByUserName(userName).map{ account => + contentType = "image/png" + getAccountByUserName(userName).flatMap{ account => response.setDateHeader("Last-Modified", account.updatedDate.getTime) account.image.map{ image => - RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image)) + Some(RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image))) }.getOrElse{ - contentType = "image/png" - (if (account.isGroupAccount) { + if (account.isGroupAccount) { TextAvatarUtil.textGroupAvatar(account.fullName) } else { TextAvatarUtil.textAvatar(account.fullName) - }).getOrElse(Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")) + } } }.getOrElse{ - NotFound() + response.setHeader("Cache-Control", "max-age=3600") + Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png") } } diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index ab2a0f1..e873ae2 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -125,7 +125,7 @@ get ("/api/v3/repos/:owner/:repo/branches/:branch")(referrersOnly { repository => //import gitbucket.core.api._ (for{ - branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined + branch <- params.get("branch") if repository.branchList.contains(branch) br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch) } yield { val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) @@ -287,7 +287,7 @@ patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository => import gitbucket.core.api._ (for{ - branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined + branch <- params.get("branch") if repository.branchList.contains(branch) protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch) } yield { diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala index ceff777..de80f97 100644 --- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -40,10 +40,6 @@ contentType = formats("json") } -// TODO Scala 2.11 -// // Don't set content type via Accept header. -// override def format(implicit request: HttpServletRequest) = "" - override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try { val httpRequest = request.asInstanceOf[HttpServletRequest] val httpResponse = response.asInstanceOf[HttpServletResponse] @@ -151,13 +147,17 @@ } } - // TODO Scala 2.11 - override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty, - includeContextPath: Boolean = true, includeServletPath: Boolean = true, - absolutize: Boolean = true, withSessionId: Boolean = true) - (implicit request: HttpServletRequest, response: HttpServletResponse): String = - if (path.startsWith("http")) path - else baseUrl + super.url(path, params, false, false, false) + /** + * Extends scalatra-form's trim rule to eliminate CR and LF. + */ + protected def trim2[T](valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T](){ + def convert(value: String, messages: Messages): T = valueType.convert(trim(value), messages) + + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = + valueType.validate(name, trim(value), params, messages) + + private def trim(value: String): String = if(value == null) null else value.replaceAll("\r\n", "").trim + } /** * Use this method to response the raw data against XSS. diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 9d0316f..524f26f 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -26,13 +26,13 @@ "password" -> trim(label("Password", text(required))) )(SignInForm.apply) - val searchForm = mapping( - "query" -> trim(text(required)), - "owner" -> trim(text(required)), - "repository" -> trim(text(required)) - )(SearchForm.apply) - - case class SearchForm(query: String, owner: String, repository: String) +// val searchForm = mapping( +// "query" -> trim(text(required)), +// "owner" -> trim(text(required)), +// "repository" -> trim(text(required)) +// )(SearchForm.apply) +// +// case class SearchForm(query: String, owner: String, repository: String) get("/"){ @@ -163,7 +163,7 @@ get("/search"){ val query = params.getOrElse("query", "").trim.toLowerCase - val visibleRepositories = getVisibleRepositories(context.loginAccount, None) + val visibleRepositories = getVisibleRepositories(context.loginAccount, repositoryUserName = None, withoutPhysicalInfo = true) val repositories = visibleRepositories.filter { repository => repository.name.toLowerCase.indexOf(query) >= 0 || repository.owner.toLowerCase.indexOf(query) >= 0 } diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 0ca0305..4fc7974 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -40,7 +40,7 @@ ) val optionsForm = mapping( - "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), identifier, renameRepositoryName))), + "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), repository, renameRepositoryName))), "description" -> trim(label("Description" , optional(text()))), "isPrivate" -> trim(label("Repository Type" , boolean())), "issuesOption" -> trim(label("Issues Option" , text(required, featureOption))), @@ -63,7 +63,7 @@ val deployKeyForm = mapping( "title" -> trim(label("Title", text(required, maxlength(100)))), - "publicKey" -> trim(label("Key" , text(required))), // TODO duplication check in the repository? + "publicKey" -> trim2(label("Key" , text(required))), // TODO duplication check in the repository? "allowWrite" -> trim(label("Key" , boolean())) )(DeployKeyForm.apply) @@ -139,6 +139,12 @@ FileUtils.moveDirectory(dir, getLfsDir(repository.owner, form.repositoryName)) } } + // Move attached directory + defining(getAttachedDir(repository.owner, repository.name)){ dir => + if(dir.isDirectory) { + FileUtils.moveDirectory(dir, getAttachedDir(repository.owner, form.repositoryName)) + } + } // Delete parent directory FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name)) @@ -157,7 +163,7 @@ /** Update default branch */ post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) => - if(repository.branchList.find(_ == form.defaultBranch).isEmpty){ + if(!repository.branchList.contains(form.defaultBranch)){ redirect(s"/${repository.owner}/${repository.name}/settings/options") } else { saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch) @@ -174,7 +180,7 @@ get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository => import gitbucket.core.api._ val branch = params("branch") - if(repository.branchList.find(_ == branch).isEmpty){ + if(!repository.branchList.contains(branch)){ redirect(s"/${repository.owner}/${repository.name}/settings/branches") } else { val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) @@ -352,6 +358,12 @@ FileUtils.moveDirectory(dir, getLfsDir(form.newOwner, repository.name)) } } + // Move attached directory + defining(getAttachedDir(repository.owner, repository.name)){ dir => + if(dir.isDirectory) { + FileUtils.moveDirectory(dir, getAttachedDir(form.newOwner, repository.name)) + } + } // Delere parent directory FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name)) diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 1b4ecc9..6fc90d7 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -232,7 +232,7 @@ oldFileName = form.oldFileName, content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator), charset = form.charset, - message = if(form.oldFileName.exists(_ == form.newFileName)){ + message = if(form.oldFileName.contains(form.newFileName)){ form.message.getOrElse(s"Update ${form.newFileName}") } else { form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}") @@ -614,7 +614,7 @@ val permission = JGitUtil.processTree(git, headTip){ (path, tree) => // Add all entries except the editing file - if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){ + if(!newPath.contains(path) && !oldPath.contains(path)){ builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) } // Retrieve permission if file exists to keep it @@ -670,12 +670,13 @@ private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = { val revision = name.stripSuffix(suffix) - - val filename = repository.name + "-" + - (if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix - + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision)) + val oid = git.getRepository.resolve(revision) + val revCommit = JGitUtil.getRevCommitFromId(git, oid) + val sha1 = oid.getName() + val repositorySuffix = (if(sha1.startsWith(revision)) sha1 else revision).replace('/','-') + val filename = repository.name + "-" + repositorySuffix + suffix contentType = "application/octet-stream" response.setHeader("Content-Disposition", s"attachment; filename=${filename}") @@ -683,6 +684,7 @@ git.archive .setFormat(suffix.tail) + .setPrefix(repository.name + "-" + repositorySuffix + "/") .setTree(revCommit) .setOutputStream(response.getOutputStream) .call() diff --git a/src/main/scala/gitbucket/core/model/Milestone.scala b/src/main/scala/gitbucket/core/model/Milestone.scala index 81c4f2b..491fefd 100644 --- a/src/main/scala/gitbucket/core/model/Milestone.scala +++ b/src/main/scala/gitbucket/core/model/Milestone.scala @@ -9,10 +9,10 @@ class Milestones(tag: Tag) extends Table[Milestone](tag, "MILESTONE") with MilestoneTemplate { override val milestoneId = column[Int]("MILESTONE_ID", O AutoInc) val title = column[String]("TITLE") - val description = column[String]("DESCRIPTION") - val dueDate = column[java.util.Date]("DUE_DATE") - val closedDate = column[java.util.Date]("CLOSED_DATE") - def * = (userName, repositoryName, milestoneId, title, description.?, dueDate.?, closedDate.?) <> (Milestone.tupled, Milestone.unapply) + val description = column[Option[String]]("DESCRIPTION") + val dueDate = column[Option[java.util.Date]]("DUE_DATE") + val closedDate = column[Option[java.util.Date]]("CLOSED_DATE") + def * = (userName, repositoryName, milestoneId, title, description, dueDate, closedDate) <> (Milestone.tupled, Milestone.unapply) def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId) def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], milestoneId: Rep[Int]) = byMilestone(userName, repositoryName, milestoneId) diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala index b299004..a2e8976 100644 --- a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala +++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala @@ -83,7 +83,7 @@ def addRenderer(extension: String, renderer: Renderer): Unit = renderers += ((extension, renderer)) - def getRenderer(extension: String): Renderer = renderers.get(extension).getOrElse(DefaultRenderer) + def getRenderer(extension: String): Renderer = renderers.getOrElse(extension, DefaultRenderer) def renderableExtensions: Seq[String] = renderers.keys.toSeq @@ -175,7 +175,7 @@ }).foreach { pluginJar => val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader) try { - val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin] + val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin] // Migration val solidbase = new Solidbase() diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index c6c4085..4a9b12c 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -111,7 +111,7 @@ val (_, cs) = status.head Some(CommitStatusInfo( count = status.length, - successCount = status.filter(_._2.state == CommitState.SUCCESS).length, + successCount = status.count(_._2.state == CommitState.SUCCESS), context = (if(status.length == 1) Some(cs.context) else None), state = (if(status.length == 1) Some(cs.state) else None), targetUrl = (if(status.length == 1) cs.targetUrl else None), diff --git a/src/main/scala/gitbucket/core/service/MilestonesService.scala b/src/main/scala/gitbucket/core/service/MilestonesService.scala index 1e95856..276aa7c 100644 --- a/src/main/scala/gitbucket/core/service/MilestonesService.scala +++ b/src/main/scala/gitbucket/core/service/MilestonesService.scala @@ -21,7 +21,7 @@ def updateMilestone(milestone: Milestone)(implicit s: Session): Unit = Milestones .filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId)) - .map (t => (t.title, t.description.?, t.dueDate.?, t.closedDate.?)) + .map (t => (t.title, t.description, t.dueDate, t.closedDate)) .update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate) def openMilestone(milestone: Milestone)(implicit s: Session): Unit = diff --git a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala index 577b843..d74280c 100644 --- a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala +++ b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala @@ -76,7 +76,7 @@ includeAdministrators: Boolean) extends AccountService with CommitStatusService { def isAdministrator(pusher: String)(implicit session: Session): Boolean = - pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty + pusher == owner || getGroupMembers(owner).exists(gm => gm.userName == pusher && gm.isManager) /** * Can't be force pushed diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala index 3807e55..03f2e6e 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -265,7 +265,7 @@ val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ") state -> summary } - lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.exists(_==s.context) } + lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.contains(s.context) } lazy val isAllSuccess = commitStateSummary._1==CommitState.SUCCESS } } diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 3640f1c..3580833 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -262,11 +262,19 @@ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName) }, repository, - getForkedCount( - repository.originUserName.getOrElse(repository.userName), - repository.originRepositoryName.getOrElse(repository.repositoryName) - ), - getRepositoryManagers(repository.userName)) + if(withoutPhysicalInfo){ + -1 + } else { + getForkedCount( + repository.originUserName.getOrElse(repository.userName), + repository.originRepositoryName.getOrElse(repository.repositoryName) + ) + }, + if(withoutPhysicalInfo){ + Nil + } else { + getRepositoryManagers(repository.userName) + }) } } @@ -300,7 +308,7 @@ case None => Repositories filter(_.isPrivate === false.bind) }).filter { t => repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true) - }.sortBy(_.lastActivityDate desc).list.map{ repository => + }.sortBy(_.lastActivityDate desc).list.map { repository => new RepositoryInfo( if(withoutPhysicalInfo){ new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName) @@ -308,11 +316,19 @@ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName) }, repository, - getForkedCount( - repository.originUserName.getOrElse(repository.userName), - repository.originRepositoryName.getOrElse(repository.repositoryName) - ), - getRepositoryManagers(repository.userName)) + if(withoutPhysicalInfo){ + -1 + } else { + getForkedCount( + repository.originUserName.getOrElse(repository.userName), + repository.originRepositoryName.getOrElse(repository.repositoryName) + ) + }, + if(withoutPhysicalInfo) { + Nil + } else { + getRepositoryManagers(repository.userName) + }) } } diff --git a/src/main/scala/gitbucket/core/service/WikiService.scala b/src/main/scala/gitbucket/core/service/WikiService.scala index c5dad27..ab034fc 100644 --- a/src/main/scala/gitbucket/core/service/WikiService.scala +++ b/src/main/scala/gitbucket/core/service/WikiService.scala @@ -177,7 +177,7 @@ val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") JGitUtil.processTree(git, headId){ (path, tree) => - if(revertInfo.find(x => x.filePath == path).isEmpty){ + if(!revertInfo.exists(x => x.filePath == path)){ builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) } } diff --git a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala index 7e0ba97..2947fde 100644 --- a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala +++ b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala @@ -51,21 +51,21 @@ private def pluginRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain, settings: SystemSettings, isUpdating: Boolean, filter: GitRepositoryFilter): Unit = { - implicit val r = request + Database() withSession { implicit session => + val account = for { + auth <- Option(request.getHeader("Authorization")) + Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2) + account <- authenticate(settings, username, password) + } yield { + request.setAttribute(Keys.Request.UserName, account.userName) + account + } - val account = for { - auth <- Option(request.getHeader("Authorization")) - Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2) - account <- authenticate(settings, username, password) - } yield { - request.setAttribute(Keys.Request.UserName, account.userName) - account - } - - if(filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)){ - chain.doFilter(request, response) - } else { - AuthUtil.requireAuth(response) + if (filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)) { + chain.doFilter(request, response) + } else { + AuthUtil.requireAuth(response) + } } } diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index 9461803..0629c14 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -105,7 +105,7 @@ } } case AuthType.DeployKeyType(key) => { - getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)) match { + getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).contains(key)) match { case List(_) => true case _ => false } @@ -123,7 +123,7 @@ } } case AuthType.DeployKeyType(key) => { - getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)) match { + getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).contains(key)) match { case List(x) if x.allowWrite => true case _ => false } diff --git a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala index 814e3e9..e3be40d 100644 --- a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala +++ b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala @@ -68,7 +68,7 @@ private def authenticateGenericUser(userName: String, key: PublicKey, session: ServerSession, genericUser: String)(implicit s: Session): Boolean = { // find all users having the key we got from ssh val possibleUserNames = getAllKeys().filter { sshKey => - SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key) + SshUtil.str2PublicKey(sshKey.publicKey).contains(key) }.map(_.userName).distinct // determine the user - if different accounts share the same key, tough luck @@ -85,7 +85,7 @@ }.getOrElse { // search deploy keys val existsDeployKey = getAllDeployKeys().exists { sshKey => - SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key) + SshUtil.str2PublicKey(sshKey.publicKey).contains(key) } if(existsDeployKey){ // found deploy key for repository diff --git a/src/main/scala/gitbucket/core/ssh/SshUtil.scala b/src/main/scala/gitbucket/core/ssh/SshUtil.scala index 62eb97a..9563ab3 100644 --- a/src/main/scala/gitbucket/core/ssh/SshUtil.scala +++ b/src/main/scala/gitbucket/core/ssh/SshUtil.scala @@ -18,16 +18,17 @@ val parts = key.split(" ") if (parts.size < 2) { logger.debug(s"Invalid PublicKey Format: ${key}") - return None - } - try { - val encodedKey = parts(1) - val decode = Base64.getDecoder.decode(Constants.encodeASCII(encodedKey)) - Some(new ByteArrayBuffer(decode).getRawPublicKey) - } catch { - case e: Throwable => - logger.debug(e.getMessage, e) - None + None + } else { + try { + val encodedKey = parts(1) + val decode = Base64.getDecoder.decode(Constants.encodeASCII(encodedKey)) + Some(new ByteArrayBuffer(decode).getRawPublicKey) + } catch { + case e: Throwable => + logger.debug(e.getMessage, e) + None + } } } diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index ac1a9b5..3c494ae 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -952,7 +952,7 @@ * @return the last modified commit of specified path */ def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = { - return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next + git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next } def getBranches(owner: String, name: String, defaultBranch: String, origin: Boolean): Seq[BranchInfo] = { diff --git a/src/main/twirl/gitbucket/core/admin/system.scala.html b/src/main/twirl/gitbucket/core/admin/system.scala.html index c78ad6d..4234243 100644 --- a/src/main/twirl/gitbucket/core/admin/system.scala.html +++ b/src/main/twirl/gitbucket/core/admin/system.scala.html @@ -1,4 +1,5 @@ @(info: Option[Any])(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.util.DatabaseConfig @gitbucket.core.html.main("System settings"){ @gitbucket.core.admin.html.menu("system"){ @gitbucket.core.helper.html.information(info) @@ -20,7 +21,17 @@ DATABASE_URL - @gitbucket.core.util.DatabaseConfig.url + @if(DatabaseConfig.url.startsWith("jdbc:h2:")) { + +

@gitbucket.core.util.DatabaseConfig.url

+

+ Your GitBucket is running on embedded H2 database. + Recommend to configure to use external database if you would like to use GitBucket for important purpose. +

+ + }else{ + @gitbucket.core.util.DatabaseConfig.url + } diff --git a/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html b/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html index faf5d5a..ec18663 100644 --- a/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html @@ -49,10 +49,7 @@ $('#filter-box').keyup(function(){ var inputVal = $('#filter-box').val(); $.each($('li.repo-link a'), function(index, elem) { - console.log(inputVal); - console.log(elem.text.trim()); - console.log(elem.text.trim().lastIndexOf(inputVal, 0)); - if (!inputVal || !elem.text.trim() || elem.text.trim().indexOf(inputVal) >= 0) { + if ( !inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >= 0 ) { $(elem).parent().show(); } else { $(elem).parent().hide(); diff --git a/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html b/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html index 09e3feb..38a5d93 100644 --- a/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html +++ b/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html @@ -31,7 +31,7 @@ $('#branch-control-input').keyup(function() { var inputVal = $('#branch-control-input').val(); $.each($('#branch-control-input').parent().parent().find('a'), function(index, elem) { - if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) { + if (!inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >= 0) { $(elem).parent().show(); } else { $(elem).parent().hide(); diff --git a/src/main/twirl/gitbucket/core/helper/dropdown.scala.html b/src/main/twirl/gitbucket/core/helper/dropdown.scala.html index 2becc79..a98c53c 100644 --- a/src/main/twirl/gitbucket/core/helper/dropdown.scala.html +++ b/src/main/twirl/gitbucket/core/helper/dropdown.scala.html @@ -32,7 +32,7 @@ $('#@{filter}-input').keyup(function() { var inputVal = $('#@{filter}-input').val(); $.each($('#@{filter}-input').parent().parent().find('a'), function(index, elem) { - if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) { + if ( !inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >=0 ) { $(elem).parent().show(); } else { $(elem).parent().hide(); diff --git a/src/main/twirl/gitbucket/core/pulls/compare.scala.html b/src/main/twirl/gitbucket/core/pulls/compare.scala.html index d390c17..17a399a 100644 --- a/src/main/twirl/gitbucket/core/pulls/compare.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/compare.scala.html @@ -221,3 +221,27 @@ } }); + + diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index c2b225d..337b989 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -342,6 +342,8 @@ ul.dropdown-menu { padding: 2px 0; + overflow: auto; + max-height: 100vh; } ul.dropdown-menu li { @@ -556,6 +558,7 @@ background-color: transparent; padding: 2px; margin: 0px; + white-space: pre-wrap; } #repository-url { diff --git a/src/test/scala/gitbucket/core/GitBucketCoreModuleSpec.scala b/src/test/scala/gitbucket/core/GitBucketCoreModuleSpec.scala index 8f2d7c7..449e8db 100644 --- a/src/test/scala/gitbucket/core/GitBucketCoreModuleSpec.scala +++ b/src/test/scala/gitbucket/core/GitBucketCoreModuleSpec.scala @@ -29,7 +29,7 @@ } test("Migration MySQL", ExternalDBTest){ - val config = aMysqldConfig(v5_7_10) + val config = aMysqldConfig(v5_7_latest) .withPort(3306) .withUser("sa", "sa") .withCharset(Charset.UTF8)