diff --git a/LICENSE b/LICENSE index d645695..b98f26a 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2013-2016 GitBucket Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 46ddfbf..a2b6586 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,12 @@ Release Notes ------------- +### 4.4 - 28 Aug 2016 +- Import a SQL dump file to the database +- `go get` support in private repositories +- Sort milestones by due date +- apache-sshd has been updated to 1.2.0 + ### 4.3 - 30 Jul 2016 - Emoji support by [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin) - User name suggestion diff --git a/build.sbt b/build.sbt index 1e2a8a9..090ac28 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ val Organization = "io.github.gitbucket" val Name = "gitbucket" -val GitBucketVersion = "4.3.0" +val GitBucketVersion = "4.4.0" val ScalatraVersion = "2.4.1" val JettyVersion = "9.3.9.v20160517" diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index 4d71888..d1685ba 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -13,5 +13,6 @@ new LiquibaseMigration("update/gitbucket-core_4.2.xml") ), new Version("4.2.1"), - new Version("4.3.0") + new Version("4.3.0"), + new Version("4.4.0") ) diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 4a9b218..9a2f2a4 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -109,14 +109,13 @@ * https://developer.github.com/v3/repos/contents/#get-contents */ get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository => - val (id, path) = repository.splitPath(multiParams("splat").head) - val refStr = params("ref") + val path = multiParams("splat").head match { + case s if s.isEmpty => "." + case s => s + } + val refStr = params.getOrElse("ref", repository.repository.defaultBranch) using(Git.open(getRepositoryDir(params("owner"), params("repo")))){ git => - if (path.isEmpty) { - JsonFormat(getFileList(git, refStr, ".").map{f => ApiContents(f)}) - } else { - JsonFormat(getFileList(git, refStr, path).map{f => ApiContents(f)}) - } + JsonFormat(getFileList(git, refStr, path).map{f => ApiContents(f)}) } }) diff --git a/src/main/scala/gitbucket/core/controller/DashboardController.scala b/src/main/scala/gitbucket/core/controller/DashboardController.scala index 0f4648e..1227f24 100644 --- a/src/main/scala/gitbucket/core/controller/DashboardController.scala +++ b/src/main/scala/gitbucket/core/controller/DashboardController.scala @@ -15,20 +15,7 @@ with UsersAuthenticator => get("/dashboard/issues")(usersOnly { - val q = request.getParameter("q") - val account = context.loginAccount.get - Option(q).map { q => - val condition = IssueSearchCondition(q, Map[String, Int]()) - q match { - case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}") - case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}") - case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}") - case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}") - case _ => searchIssues("created_by") - } - } getOrElse { - searchIssues("created_by") - } + searchIssues("created_by") }) get("/dashboard/issues/assigned")(usersOnly { @@ -44,20 +31,7 @@ }) get("/dashboard/pulls")(usersOnly { - val q = request.getParameter("q") - val account = context.loginAccount.get - Option(q).map { q => - val condition = IssueSearchCondition(q, Map[String, Int]()) - q match { - case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}") - case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}") - case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}") - case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}") - case _ => searchPullRequests("created_by") - } - } getOrElse { - searchPullRequests("created_by") - } + searchPullRequests("created_by") }) get("/dashboard/pulls/created_by")(usersOnly { @@ -73,14 +47,7 @@ }) private def getOrCreateCondition(key: String, filter: String, userName: String) = { - val condition = session.putAndGet(key, if(request.hasQueryString){ - val q = request.getParameter("q") - if(q == null){ - IssueSearchCondition(request) - } else { - IssueSearchCondition(q, Map[String, Int]()) - } - } else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition())) + val condition = IssueSearchCondition(request) filter match { case "assigned" => condition.copy(assigned = Some(Some(userName)), author = None, mentioned = None) diff --git a/src/main/scala/gitbucket/core/controller/FileUploadController.scala b/src/main/scala/gitbucket/core/controller/FileUploadController.scala index 163600e..ac90bbc 100644 --- a/src/main/scala/gitbucket/core/controller/FileUploadController.scala +++ b/src/main/scala/gitbucket/core/controller/FileUploadController.scala @@ -79,15 +79,10 @@ } post("/import") { + import JDBCUtil._ session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin => execute({ (file, fileId) => - if(file.getName.endsWith(".xml")){ - import JDBCUtil._ - val conn = request2Session(request).conn - conn.importAsXML(file.getInputStream) - } else { - throw new RuntimeException("Import is available for only the XML file.") - } + request2Session(request).conn.importAsSQL(file.getInputStream) }, _ => true) } redirect("/admin/data") diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index 4bdfac2..f425b60 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -363,16 +363,7 @@ val sessionKey = Keys.Session.Issues(owner, repoName) // retrieve search condition - val condition = session.putAndGet(sessionKey, - if(request.hasQueryString){ - val q = request.getParameter("q") - if(q == null || q.trim.isEmpty){ - IssueSearchCondition(request) - } else { - IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap) - } - } else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) - ) + val condition = IssueSearchCondition(request) html.list( "issues", diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index ed5810b..803ab51 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -520,10 +520,7 @@ val sessionKey = Keys.Session.Pulls(owner, repoName) // retrieve search condition - val condition = session.putAndGet(sessionKey, - if(request.hasQueryString) IssueSearchCondition(request) - else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) - ) + val condition = IssueSearchCondition(request) gitbucket.core.issues.html.list( "pulls", diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 8d06409..deaf4d0 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -303,12 +303,7 @@ post("/admin/export")(adminOnly { import gitbucket.core.util.JDBCUtil._ - val session = request2Session(request) - val file = if(params("type") == "sql"){ - session.conn.exportAsSQL(request.getParameterValues("tableNames").toSeq) - } else { - session.conn.exportAsXML(request.getParameterValues("tableNames").toSeq) - } + val file = request2Session(request).conn.exportAsSQL(request.getParameterValues("tableNames").toSeq) contentType = "application/octet-stream" response.setHeader("Content-Disposition", "attachment; filename=" + file.getName) diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index cc41c21..c38b291 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -475,50 +475,6 @@ } /** - * Restores IssueSearchCondition instance from filter query. - */ - def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = { - val conditions = filter.split("[  \t]+").flatMap { x => - x.split(":") match { - case Array(key, value) => Some((key, value)) - case _ => None - } - }.groupBy(_._1).map { case (key, values) => - key -> values.map(_._2).toSeq - } - - val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match { - case "created-asc" => ("created" , "asc" ) - case "comments-desc" => ("comments", "desc") - case "comments-asc" => ("comments", "asc" ) - case "updated-desc" => ("comments", "desc") - case "updated-asc" => ("comments", "asc" ) - case _ => ("created" , "desc") - } - - IssueSearchCondition( - conditions.get("label").map(_.toSet).getOrElse(Set.empty), - conditions.get("milestone").flatMap(_.headOption) match { - case None => None - case Some("none") => Some(None) - case Some(x) => Some(Some(x)) - }, - conditions.get("author").flatMap(_.headOption), - conditions.get("assignee").flatMap(_.headOption) match { - case None => None - case Some("none") => Some(None) - case Some(x) => Some(Some(x)) - }, - conditions.get("mentions").flatMap(_.headOption), - conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"), - sort, - direction, - conditions.get("visibility").flatMap(_.headOption), - conditions.get("group").map(_.toSet).getOrElse(Set.empty) - ) - } - - /** * Restores IssueSearchCondition instance from request parameters. */ def apply(request: HttpServletRequest): IssueSearchCondition = diff --git a/src/main/scala/gitbucket/core/util/Implicits.scala b/src/main/scala/gitbucket/core/util/Implicits.scala index 12623a4..d93ebe2 100644 --- a/src/main/scala/gitbucket/core/util/Implicits.scala +++ b/src/main/scala/gitbucket/core/util/Implicits.scala @@ -85,12 +85,6 @@ } implicit class RichSession(session: HttpSession){ - - def putAndGet[T](key: String, value: T): T = { - session.setAttribute(key, value) - value - } - def getAndRemove[T](key: String): Option[T] = { val value = session.getAttribute(key).asInstanceOf[T] if(value == null){ diff --git a/src/main/scala/gitbucket/core/util/JDBCUtil.scala b/src/main/scala/gitbucket/core/util/JDBCUtil.scala index 60116a5..34bee29 100644 --- a/src/main/scala/gitbucket/core/util/JDBCUtil.scala +++ b/src/main/scala/gitbucket/core/util/JDBCUtil.scala @@ -3,11 +3,8 @@ import java.io._ import java.sql._ import java.text.SimpleDateFormat -import javax.xml.stream.{XMLStreamConstants, XMLInputFactory, XMLOutputFactory} import ControlUtil._ -import scala.StringBuilder import scala.annotation.tailrec -import scala.collection.mutable import scala.collection.mutable.ListBuffer /** @@ -64,65 +61,34 @@ } } - def importAsXML(in: InputStream): Unit = { + def importAsSQL(in: InputStream): Unit = { conn.setAutoCommit(false) try { - val factory = XMLInputFactory.newInstance() - using(factory.createXMLStreamReader(in, "UTF-8")){ reader => - // stateful objects - var elementName = "" - var insertTable = "" - var insertColumns = Map.empty[String, (String, String)] + using(in){ in => + var out = new ByteArrayOutputStream() - while(reader.hasNext){ - reader.next() + var length = 0 + val bytes = new scala.Array[Byte](1024 * 8) + var stringLiteral = false - reader.getEventType match { - case XMLStreamConstants.START_ELEMENT => - elementName = reader.getName.getLocalPart - if(elementName == "insert"){ - insertTable = reader.getAttributeValue(null, "table") - } else if(elementName == "delete"){ - val tableName = reader.getAttributeValue(null, "table") - conn.update(s"DELETE FROM ${tableName}") - } else if(elementName == "column"){ - val columnName = reader.getAttributeValue(null, "name") - val columnType = reader.getAttributeValue(null, "type") - val columnValue = reader.getElementText - insertColumns = insertColumns + (columnName -> (columnType, columnValue)) - } - case XMLStreamConstants.END_ELEMENT => - // Execute insert statement - reader.getName.getLocalPart match { - case "insert" => { - val sb = new StringBuilder() - sb.append(s"INSERT INTO ${insertTable} (") - sb.append(insertColumns.map { case (columnName, _) => columnName }.mkString(", ")) - sb.append(") VALUES (") - sb.append(insertColumns.map { case (_, (columnType, columnValue)) => - if(columnType == null || columnValue == null){ - "NULL" - } else if(columnType == "string"){ - "'" + columnValue.replace("'", "''") + "'" - } else if(columnType == "timestamp"){ - "'" + columnValue + "'" - } else { - columnValue.toString - } - }.mkString(", ")) - sb.append(")") + var count = 0 - conn.update(sb.toString) - - insertColumns = Map.empty[String, (String, String)] // Clear column information - } - case _ => // Nothing to do - } - case _ => // Nothing to do + while({ length = in.read(bytes); length != -1 }){ + for(i <- 0 to length - 1){ + val c = bytes(i) + if(c == '\''){ + stringLiteral = !stringLiteral + } + if(c == ';' && !stringLiteral){ + val sql = new String(out.toByteArray, "UTF-8") + conn.update(sql) + out = new ByteArrayOutputStream() + } else { + out.write(c) + } } } } - conn.commit() } catch { @@ -133,68 +99,6 @@ } } - def exportAsXML(targetTables: Seq[String]): File = { - val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") - val file = File.createTempFile("gitbucket-export-", ".xml") - - val factory = XMLOutputFactory.newInstance() - using(factory.createXMLStreamWriter(new FileOutputStream(file), "UTF-8")){ writer => - val dbMeta = conn.getMetaData - val allTablesInDatabase = allTablesOrderByDependencies(dbMeta) - - writer.writeStartDocument("UTF-8", "1.0") - writer.writeStartElement("tables") - - println(allTablesInDatabase.mkString(", ")) - - allTablesInDatabase.reverse.foreach { tableName => - if (targetTables.contains(tableName)) { - writer.writeStartElement("delete") - writer.writeAttribute("table", tableName) - writer.writeEndElement() - } - } - - allTablesInDatabase.foreach { tableName => - if (targetTables.contains(tableName)) { - select(s"SELECT * FROM ${tableName}") { rs => - writer.writeStartElement("insert") - writer.writeAttribute("table", tableName) - val rsMeta = rs.getMetaData - (1 to rsMeta.getColumnCount).foreach { i => - val columnName = rsMeta.getColumnName(i) - val (columnType, columnValue) = if(rs.getObject(columnName) == null){ - (null, null) - } else { - rsMeta.getColumnType(i) match { - case Types.BOOLEAN | Types.BIT => ("boolean", rs.getBoolean(columnName)) - case Types.VARCHAR | Types.CLOB | Types.CHAR | Types.LONGVARCHAR => ("string", rs.getString(columnName)) - case Types.INTEGER => ("int", rs.getInt(columnName)) - case Types.TIMESTAMP => ("timestamp", dateFormat.format(rs.getTimestamp(columnName))) - } - } - writer.writeStartElement("column") - writer.writeAttribute("name", columnName) - if(columnType != null){ - writer.writeAttribute("type", columnType) - } - if(columnValue != null){ - writer.writeCharacters(columnValue.toString) - } - writer.writeEndElement() - } - writer.writeEndElement() - } - } - } - - writer.writeEndElement() - writer.writeEndDocument() - } - - file - } - def exportAsSQL(targetTables: Seq[String]): File = { val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") val file = File.createTempFile("gitbucket-export-", ".sql") diff --git a/src/main/twirl/gitbucket/core/admin/data.scala.html b/src/main/twirl/gitbucket/core/admin/data.scala.html index ed4364d..fadc0f3 100644 --- a/src/main/twirl/gitbucket/core/admin/data.scala.html +++ b/src/main/twirl/gitbucket/core/admin/data.scala.html @@ -13,22 +13,12 @@ } -
- -
-
- -
-
Import (only XML)
+
Import
@@ -42,10 +32,10 @@ $(function(){ $('#import-form').submit(function(){ if($('#file').val() == ''){ - alert('Choose an import XML file.'); + alert('Choose an import SQL file.'); return false; - } else if(!$('#file').val().endsWith(".xml")){ - alert('Import is available for only the XML file.'); + } else if(!$('#file').val().endsWith(".sql")){ + alert('Import is available for only the SQL file.'); return false; } return confirm('All existing data is deleted before importing.\nAre you sure?'); diff --git a/src/main/twirl/gitbucket/core/helper/attached.scala.html b/src/main/twirl/gitbucket/core/helper/attached.scala.html index 9bea4f1..02624f2 100644 --- a/src/main/twirl/gitbucket/core/helper/attached.scala.html +++ b/src/main/twirl/gitbucket/core/helper/attached.scala.html @@ -46,18 +46,11 @@ @if(generateScript){ try { + $([$('#@textareaId')[0]]).dropzone({ + @dropzone(false, textareaId) + }); $([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({ - url: '@context.path/upload/file/@repository.owner/@repository.name', - maxFilesize: 10, - acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")), - dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.', - previewTemplate: "
\n
Uploading your files...
\n
\n
", - success: function(file, id) { - var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + - '](@context.baseUrl/@repository.owner/@repository.name/_attached/' + id + ')'; - $('#@textareaId').val($('#@textareaId').val() + attachFile); - $(file.previewElement).prevAll('div.dz-preview').addBack().remove(); - } + @dropzone(true, textareaId) }); } catch(e) { if (e.message !== "Dropzone already attached.") { @@ -68,3 +61,17 @@ }); } +@dropzone(clickable: Boolean, textareaId: Option[String]) = { + url: '@context.path/upload/file/@repository.owner/@repository.name', + maxFilesize: 10, + clickable: false, + acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")), + dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.', + previewTemplate: "
\n
Uploading your files...
\n
\n
", + success: function(file, id) { + var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + + '](@context.baseUrl/@repository.owner/@repository.name/_attached/' + id + ')'; + $('#@textareaId').val($('#@textareaId').val() + attachFile); + $(file.previewElement).prevAll('div.dz-preview').addBack().remove(); + } +} diff --git a/src/main/twirl/gitbucket/core/main.scala.html b/src/main/twirl/gitbucket/core/main.scala.html index 25064bc..779ae4c 100644 --- a/src/main/twirl/gitbucket/core/main.scala.html +++ b/src/main/twirl/gitbucket/core/main.scala.html @@ -15,8 +15,9 @@ - - + + + @@ -36,7 +37,7 @@ @repository.map { repository => } - +
@@ -47,8 +48,14 @@ @gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion
-