diff --git a/.travis.yml b/.travis.yml index 6caeabf..b85cad5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,5 @@ sudo: false script: - sbt test +jdk: + - oraclejdk8 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d7234da --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Guideline for Issues + +- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue. +- Make sure check whether there is a same question or request in the past. +- When raise a new issue, write subject in **English** at least. +- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja). +- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it. diff --git a/README.md b/README.md index 5f8eaa2..e111e68 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://travis-ci.org/takezoe/gitbucket.svg?branch=master)](https://travis-ci.org/takezoe/gitbucket) +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) ========= GitBucket is the easily installable GitHub clone powered by Scala. @@ -16,28 +16,20 @@ - Fork / Pull request - Email notification - Activity timeline -- User management (for Administrators) -- Group (like Organization in GitHub) -- LDAP integration +- Simple user and group management with LDAP integration - Gravatar support - Plug-in system -Following features are not implemented, but we will make them in the future release! - -- Network graph -- Statistics -- Watch / Star - -If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki). +If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/gitbucket/gitbucket/wiki). Installation -------- -1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases). +1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases). 2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher. 3. Access **http://[hostname]:[port]/gitbucket/** using your web browser. -If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx) +If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nginx) The default administrator account is **root** and password is **root**. @@ -50,7 +42,7 @@ To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk. -For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo) +For Installation on Windows Server with IIS see [this wiki page](https://github.com/gitbucket/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo) ### Mac OS X #### Installing Via Homebrew @@ -73,7 +65,7 @@ ``` #### Manual Installation -On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/` +On OS X, copy the [gitbucket.plist](https://raw.github.com/gitbucket/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/` Run the following commands in `Terminal` to @@ -84,16 +76,41 @@ -------- GitBucket has the plug-in system to extend GitBucket from outside of GitBucket. Some plug-ins are available now: -- [gitbucket-gist-plugin](https://github.com/takezoe/gitbucket-gist-plugin) -- [gitbucket-announce-plugin](https://github.com/McFoggy/gitbucket-announce-plugin) +- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) +- [gitbucket-announce-plugin](https://github.com/gitbucket-plugins/gitbucket-announce-plugin) +- [gitbucket-h2-backup-plugin](https://github.com/gitbucket-plugins/gitbucket-h2-backup-plugin) +- [gitbucket-desktopnotify-plugin](https://github.com/yoshiyoshifujii/gitbucket-desktopnotify-plugin) +- [gitbucket-commitgraphs-plugin](https://github.com/yoshiyoshifujii/gitbucket-commitgraphs-plugin) + +You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/). + +Support +-------- + +- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue. +- Make sure check whether there is a same question or request in the past. +- When raise a new issue, write subject in **English** at least. +- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja). +- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it. Release Notes -------- +### 3.7 - 3 Oct 2015 +- Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown +- Clone in desktop button +- Providing MD5 and SHA-1 checksum for `gitbucket.war` has started + +### 3.6 - 30 Aug 2015 +- User interface Improvements: Especially, commit list, issues and pull request have been updated largely. +- Installed plugins list has been available at the system administration console. +- Pages and repository list in the sidebar have been limited and more pages and repositories link is available. +- More reference link notation in Markdown has been supported. + ### 3.5 - 1 Aug 2015 - Octicons has been applied - Global header has been enhanced. Now it's further similar to GitHub. - Default compare / pull request target has been changed to the parent repository -- A lot of updates for [gitbucket-gist-plugin](https://github.com/takezoe/gitbucket-gist-plugin) +- A lot of updates for [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) ### 3.4 - 27 Jun 2015 - Declarative style plug-in definition diff --git a/contrib/install b/contrib/install index 860b02f..97097fb 100755 --- a/contrib/install +++ b/contrib/install @@ -38,7 +38,7 @@ createDir "$GITBUCKET_LOG_DIR" echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE" -sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war +sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/gitbucket/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war sudo rm -f "$GITBUCKET_LOG_DIR/run.log" diff --git a/contrib/linux/redhat/gitbucket.spec b/contrib/linux/redhat/gitbucket.spec index d634b1d..1d480cc 100644 --- a/contrib/linux/redhat/gitbucket.spec +++ b/contrib/linux/redhat/gitbucket.spec @@ -3,7 +3,7 @@ Version: 2.6 Release: 1%{?dist} License: Apache -URL: https://github.com/takezoe/gitbucket +URL: https://github.com/gitbucket/gitbucket Group: System/Servers Source0: %{name}.war Source1: %{name}.init diff --git a/doc/auto_update.md b/doc/auto_update.md index 83aa711..220a884 100644 --- a/doc/auto_update.md +++ b/doc/auto_update.md @@ -2,7 +2,7 @@ ======== GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading. -To release a new version of GitBucket, add the version definition to the [servlet.AutoUpdate](https://github.com/takezoe/gitbucket/blob/master/src/main/scala/servlet/AutoUpdateListener.scala) at first. +To release a new version of GitBucket, add the version definition to the [gitbucket.core.servlet.AutoUpdate](https://github.com/gitbucket/gitbucket/blob/master/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala) at first. ```scala object AutoUpdate { @@ -16,7 +16,7 @@ ... ``` -Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/takezoe/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```. +Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/gitbucket/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```. GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version. diff --git a/gitbucket-assembly.iml b/gitbucket-assembly.iml deleted file mode 100644 index 3f0a572..0000000 --- a/gitbucket-assembly.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/project/build.scala b/project/build.scala index 762c9a7..1337898 100644 --- a/project/build.scala +++ b/project/build.scala @@ -10,7 +10,7 @@ object MyBuild extends Build { val Organization = "gitbucket" val Name = "gitbucket" - val Version = "3.5.0" + val Version = "3.7.0" val ScalaVersion = "2.11.6" val ScalatraVersion = "2.3.1" @@ -38,7 +38,8 @@ scalaVersion := ScalaVersion, resolvers ++= Seq( Classpaths.typesafeReleases, - "amateras-repo" at "http://amateras.sourceforge.jp/mvn/" + "amateras-repo" at "http://amateras.sourceforge.jp/mvn/", + "amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/" ), scalacOptions := Seq("-deprecation", "-language:postfixOps"), libraryDependencies ++= Seq( @@ -50,7 +51,7 @@ "org.json4s" %% "json4s-jackson" % "3.2.11", "jp.sf.amateras" %% "scalatra-forms" % "0.1.0", "commons-io" % "commons-io" % "2.4", - "org.pegdown" % "pegdown" % "1.5.0", + "io.github.gitbucket" % "markedj" % "1.0.4-SNAPSHOT", "org.apache.commons" % "commons-compress" % "1.9", "org.apache.commons" % "commons-email" % "1.3.3", "org.apache.httpcomponents" % "httpclient" % "4.3.6", diff --git a/release/build.xml b/release/build.xml index 2e6b8da..ee0865e 100644 --- a/release/build.xml +++ b/release/build.xml @@ -55,7 +55,12 @@ tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/> - + + + + + + diff --git a/sbt.bat b/sbt.bat index 7e90f12..3b0c31e 100644 --- a/sbt.bat +++ b/sbt.bat @@ -1,2 +1,2 @@ set SCRIPT_DIR=%~dp0 -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %* +java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %* diff --git a/sbt.sh b/sbt.sh index eae1ce3..a9247ab 100755 --- a/sbt.sh +++ b/sbt.sh @@ -1,2 +1,2 @@ #!/bin/sh -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.8.jar "$@" +java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.8.jar "$@" diff --git a/src/main/resources/update/1_0.sql b/src/main/resources/update/1_0.sql index 5067ada..7d64af6 100644 --- a/src/main/resources/update/1_0.sql +++ b/src/main/resources/update/1_0.sql @@ -128,7 +128,7 @@ 'root@localhost', 'dc76e9f0c0006e8f919e0c515c66dbba3982f785', true, - 'https://github.com/takezoe/gitbucket', + 'https://github.com/gitbucket/gitbucket', SYSDATE, SYSDATE, NULL diff --git a/src/main/scala/gitbucket/core/api/ApiComment.scala b/src/main/scala/gitbucket/core/api/ApiComment.scala index 47244f2..62bcd3c 100644 --- a/src/main/scala/gitbucket/core/api/ApiComment.scala +++ b/src/main/scala/gitbucket/core/api/ApiComment.scala @@ -14,16 +14,16 @@ user: ApiUser, body: String, created_at: Date, - updated_at: Date)(repositoryName: RepositoryName, issueId: Int){ - val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${issueId}#comment-${id}") + updated_at: Date)(repositoryName: RepositoryName, issueId: Int, isPullRequest: Boolean){ + val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${issueId}#comment-${id}") } object ApiComment{ - def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser): ApiComment = + def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser, isPullRequest: Boolean): ApiComment = ApiComment( id = comment.commentId, user = user, body = comment.content, created_at = comment.registeredDate, - updated_at = comment.updatedDate)(repositoryName, issueId) + updated_at = comment.updatedDate)(repositoryName, issueId, isPullRequest) } diff --git a/src/main/scala/gitbucket/core/api/ApiCommit.scala b/src/main/scala/gitbucket/core/api/ApiCommit.scala index 15b41d4..95128c1 100644 --- a/src/main/scala/gitbucket/core/api/ApiCommit.scala +++ b/src/main/scala/gitbucket/core/api/ApiCommit.scala @@ -20,7 +20,7 @@ removed: List[String], modified: List[String], author: ApiPersonIdent, - committer: ApiPersonIdent)(repositoryName:RepositoryName){ + committer: ApiPersonIdent)(repositoryName:RepositoryName) extends FieldSerializable{ val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}") val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}") } diff --git a/src/main/scala/gitbucket/core/api/ApiIssue.scala b/src/main/scala/gitbucket/core/api/ApiIssue.scala index 45b5d62..a73a335 100644 --- a/src/main/scala/gitbucket/core/api/ApiIssue.scala +++ b/src/main/scala/gitbucket/core/api/ApiIssue.scala @@ -17,9 +17,9 @@ state: String, created_at: Date, updated_at: Date, - body: String)(repositoryName: RepositoryName){ + body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){ val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments") - val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${number}") + val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}") } object ApiIssue{ @@ -31,5 +31,5 @@ state = if(issue.closed){ "closed" }else{ "open" }, body = issue.content.getOrElse(""), created_at = issue.registeredDate, - updated_at = issue.updatedDate)(repositoryName) + updated_at = issue.updatedDate)(repositoryName, issue.isPullRequest) } diff --git a/src/main/scala/gitbucket/core/api/ApiPushCommit.scala b/src/main/scala/gitbucket/core/api/ApiPushCommit.scala new file mode 100644 index 0000000..46a99e0 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiPushCommit.scala @@ -0,0 +1,39 @@ +package gitbucket.core.api + +import gitbucket.core.util.JGitUtil +import gitbucket.core.util.JGitUtil.CommitInfo +import gitbucket.core.util.RepositoryName + +import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.api.Git + +import java.util.Date + +/** + * https://developer.github.com/v3/activity/events/types/#pushevent + */ +case class ApiPushCommit( + id: String, + message: String, + timestamp: Date, + added: List[String], + removed: List[String], + modified: List[String], + author: ApiPersonIdent, + committer: ApiPersonIdent)(repositoryName:RepositoryName) extends FieldSerializable { + val url = ApiPath(s"/${repositoryName.fullName}/commit/${id}") +} + +object ApiPushCommit{ + def apply(commit: ApiCommit, repositoryName: RepositoryName): ApiPushCommit = ApiPushCommit( + id = commit.id, + message = commit.message, + timestamp = commit.timestamp, + added = commit.added, + removed = commit.removed, + modified = commit.modified, + author = commit.author, + committer = commit.committer)(repositoryName) + def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiPushCommit = + ApiPushCommit(ApiCommit(git, repositoryName, commit), repositoryName) +} diff --git a/src/main/scala/gitbucket/core/api/ApiRepository.scala b/src/main/scala/gitbucket/core/api/ApiRepository.scala index 0911882..ee97ea3 100644 --- a/src/main/scala/gitbucket/core/api/ApiRepository.scala +++ b/src/main/scala/gitbucket/core/api/ApiRepository.scala @@ -15,7 +15,7 @@ default_branch: String, owner: ApiUser) { val forks_count = forks - val watchers_coun = watchers + val watchers_count = watchers val url = ApiPath(s"/api/v3/repos/${full_name}") val http_url = ApiPath(s"/git/${full_name}.git") val clone_url = ApiPath(s"/git/${full_name}.git") diff --git a/src/main/scala/gitbucket/core/api/FieldSerializable.scala b/src/main/scala/gitbucket/core/api/FieldSerializable.scala new file mode 100644 index 0000000..706bf9c --- /dev/null +++ b/src/main/scala/gitbucket/core/api/FieldSerializable.scala @@ -0,0 +1,4 @@ +package gitbucket.core.api + +/** export fields for json */ +trait FieldSerializable diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index a14a116..fe9e3e5 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -22,7 +22,7 @@ ) ) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() + FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() + - FieldSerializer[ApiCommitStatus]() + FieldSerializer[ApiCommit]() + FieldSerializer[ApiCombinedCommitStatus]() + + FieldSerializer[ApiCommitStatus]() + FieldSerializer[FieldSerializable]() + FieldSerializer[ApiCombinedCommitStatus]() + FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]() diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala index 09cdc85..b57f6c4 100644 --- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -181,6 +181,13 @@ val currentPath = request.getRequestURI.substring(request.getContextPath.length) val baseUrl = settings.baseUrl(request) val host = new java.net.URL(baseUrl).getHost + val platform = request.getHeader("User-Agent") match { + case null => null + case agent if agent.contains("Mac") => "mac" + case agent if agent.contains("Linux") => "linux" + case agent if agent.contains("Win") => "windows" + case _ => null + } /** * Get object from cache. diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index cd3edc3..b15cd16 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -86,7 +86,7 @@ issueId <- params("id").toIntOpt comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt) } yield { - JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user)) }) + JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) }).getOrElse(NotFound) }) @@ -190,7 +190,7 @@ (issue, id) <- handleComment(issueId, Some(body), repository)() issueComment <- getComment(repository.owner, repository.name, id.toString()) } yield { - JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get))) + JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest)) }) getOrElse NotFound }) @@ -233,7 +233,7 @@ org.json4s.jackson.Serialization.write( Map("title" -> x.title, "content" -> Markdown.toHtml(x.content getOrElse "No description given.", - repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName)) + repository, false, true, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName)) )) } } else Unauthorized @@ -257,6 +257,12 @@ } getOrElse NotFound }) + ajaxPost("/:owner/:repository/issues/new/label")(collaboratorsOnly { repository => + val labelNames = params("labelNames").split(",") + val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName)) + html.labellist(labels) + }) + ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => defining(params("id").toInt){ issueId => registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) @@ -346,6 +352,7 @@ } } + // TODO Same method exists in PullRequestController. Should it moved to IssueService? private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { StringUtil.extractIssueId(message).foreach { issueId => val content = fromIssue.issueId + ":" + fromIssue.title @@ -459,7 +466,11 @@ "issues", searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), page, - (getCollaborators(owner, repoName) :+ owner).sorted, + if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ + (getCollaborators(owner, repoName) :+ owner).sorted + } else { + getCollaborators(owner, repoName) + }, getMilestones(owner, repoName), getLabels(owner, repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName), diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 1becfbb..ce7a353 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -46,7 +46,10 @@ "requestRepositoryName" -> trim(text(required, maxlength(100))), "requestBranch" -> trim(text(required, maxlength(100))), "commitIdFrom" -> trim(text(required, maxlength(40))), - "commitIdTo" -> trim(text(required, maxlength(40))) + "commitIdTo" -> trim(text(required, maxlength(40))), + "assignedUserName" -> trim(optional(text())), + "milestoneId" -> trim(optional(number())), + "labelNames" -> trim(optional(text())) )(PullRequestForm.apply) val mergeForm = mapping( @@ -62,7 +65,11 @@ requestRepositoryName: String, requestBranch: String, commitIdFrom: String, - commitIdTo: String) + commitIdTo: String, + assignedUserName: Option[String], + milestoneId: Option[Int], + labelNames: Option[String] + ) case class MergeForm(message: String) @@ -176,7 +183,7 @@ pullreq, statuses, repository, - s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") + getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get) } } getOrElse NotFound }) @@ -232,6 +239,9 @@ } closeIssuesFromMessage(form.message, loginAccount.userName, owner, name) } + + updatePullRequests(owner, name, pullreq.branch) + // call web hook callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get) @@ -310,32 +320,44 @@ originRepository.owner, originRepository.name, originId, forkedRepository.owner, forkedRepository.name, forkedId) - (oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId)) + (Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId))) } else { // Commit id - (oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId)) + (Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId))) } - val (commits, diffs) = getRequestCompareInfo( - originRepository.owner, originRepository.name, oldId.getName, - forkedRepository.owner, forkedRepository.name, newId.getName) + (oldId, newId) match { + case (Some(oldId), Some(newId)) => { + val (commits, diffs) = getRequestCompareInfo( + originRepository.owner, originRepository.name, oldId.getName, + forkedRepository.owner, forkedRepository.name, newId.getName) - html.compare( - commits, - diffs, - (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { - case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName) - case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name) - }, - commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList, - originId, - forkedId, - oldId.getName, - newId.getName, - forkedRepository, - originRepository, - forkedRepository, - hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount)) + html.compare( + commits, + diffs, + (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { + case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName) + case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name) + }, + commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList, + originId, + forkedId, + oldId.getName, + newId.getName, + forkedRepository, + originRepository, + forkedRepository, + hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount), + (getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.isGroupAccount) Nil else List(originRepository.owner))).sorted, + getMilestones(originRepository.owner, originRepository.name), + getLabels(originRepository.owner, originRepository.name) + ) + } + case (oldId, newId) => + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" + + s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." + + s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}") + } } }) getOrElse NotFound }) @@ -371,47 +393,78 @@ }) post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) => - val loginUserName = context.loginAccount.get.userName + defining(repository.owner, repository.name){ case (owner, name) => + val writable = hasWritePermission(owner, name, context.loginAccount) + val loginUserName = context.loginAccount.get.userName - val issueId = createIssue( - owner = repository.owner, - repository = repository.name, - loginUser = loginUserName, - title = form.title, - content = form.content, - assignedUserName = None, - milestoneId = None, - isPullRequest = true) + val issueId = createIssue( + owner = repository.owner, + repository = repository.name, + loginUser = loginUserName, + title = form.title, + content = form.content, + assignedUserName = if(writable) form.assignedUserName else None, + milestoneId = if(writable) form.milestoneId else None, + isPullRequest = true) - createPullRequest( - originUserName = repository.owner, - originRepositoryName = repository.name, - issueId = issueId, - originBranch = form.targetBranch, - requestUserName = form.requestUserName, - requestRepositoryName = form.requestRepositoryName, - requestBranch = form.requestBranch, - commitIdFrom = form.commitIdFrom, - commitIdTo = form.commitIdTo) + createPullRequest( + originUserName = repository.owner, + originRepositoryName = repository.name, + issueId = issueId, + originBranch = form.targetBranch, + requestUserName = form.requestUserName, + requestRepositoryName = form.requestRepositoryName, + requestBranch = form.requestBranch, + commitIdFrom = form.commitIdFrom, + commitIdTo = form.commitIdTo) - // fetch requested branch - fetchAsPullRequest(repository.owner, repository.name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId) + // insert labels + if(writable){ + form.labelNames.map { value => + val labels = getLabels(owner, name) + value.split(",").foreach { labelName => + labels.find(_.labelName == labelName).map { label => + registerIssueLabel(repository.owner, repository.name, issueId, label.labelId) + } + } + } + } - // record activity - recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title) + // fetch requested branch + fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId) - // call web hook - callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get) + // record activity + recordPullRequestActivity(owner, name, loginUserName, issueId, form.title) - // notifications - getIssue(repository.owner, repository.name, issueId.toString) foreach { issue => - Notifier().toNotify(repository, issue, form.content.getOrElse("")){ - Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}") + // call web hook + callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get) + + getIssue(owner, name, issueId.toString) foreach { issue => + // extract references and create refer comment + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + + // notifications + Notifier().toNotify(repository, issue, form.content.getOrElse("")){ + Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") + } + } + + redirect(s"/${owner}/${name}/pull/${issueId}") + } + }) + + // TODO Same method exists in IssueController. Should it moved to IssueService? + private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { + StringUtil.extractIssueId(message).foreach { issueId => + val content = fromIssue.issueId + ":" + fromIssue.title + if(getIssue(owner, repository, issueId).isDefined){ + // Not add if refer comment already exist. + if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) { + createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer") + } } } - - redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") - }) + } /** * Parses branch identifier and extracts owner and branch name as tuple. @@ -462,7 +515,11 @@ "pulls", searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), page, - (getCollaborators(owner, repoName) :+ owner).sorted, + if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ + (getCollaborators(owner, repoName) :+ owner).sorted + } else { + getCollaborators(owner, repoName) + }, getMilestones(owner, repoName), getLabels(owner, repoName), countIssue(condition.copy(state = "open" ), true, owner -> repoName), diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 970e380..27afd47 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -22,6 +22,7 @@ import org.eclipse.jgit.api.{ArchiveCommand, Git} import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} import org.eclipse.jgit.dircache.DirCache +import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.lib._ import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.treewalk._ @@ -249,7 +250,7 @@ ) redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${ - if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}" + if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}" }") }) @@ -270,7 +271,7 @@ ) redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${ - if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}" + if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}" }") }) @@ -292,8 +293,12 @@ getPathObjectId(git, path, revCommit).map { objectId => if(raw){ // Download - JGitUtil.getContentFromId(git, objectId, true).map { bytes => - RawData("application/octet-stream", bytes) + JGitUtil.getObjectLoaderFromId(git, objectId){ loader => + //RawData("application/octet-stream", bytes) + contentType = "application/octet-stream" + response.setContentLength(loader.getSize.toInt) + loader.copyTo(response.getOutputStream) + () } getOrElse NotFound } else { html.blob(id, repository, path.split("/").toList, @@ -344,16 +349,21 @@ get("/:owner/:repository/commit/:id")(referrersOnly { repository => val id = params("id") - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit => - JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) => - html.commit(id, new JGitUtil.CommitInfo(revCommit), - JGitUtil.getBranchesOfCommit(git, revCommit.getName), - JGitUtil.getTagsOfCommit(git, revCommit.getName), - getCommitComments(repository.owner, repository.name, id, false), - repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + try { + using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => + defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit => + JGitUtil.getDiffs(git, id) match { + case (diffs, oldCommitId) => + html.commit(id, new JGitUtil.CommitInfo(revCommit), + JGitUtil.getBranchesOfCommit(git, revCommit.getName), + JGitUtil.getTagsOfCommit(git, revCommit.getName), + getCommitComments(repository.owner, repository.name, id, false), + repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + } } } + } catch { + case e:MissingObjectException => NotFound } }) @@ -517,10 +527,11 @@ /** * Displays the file find of branch. */ - get("/:owner/:repository/find/:ref")(referrersOnly { repository => + get("/:owner/:repository/find/*")(referrersOnly { repository => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - JGitUtil.getTreeId(git, params("ref")).map{ treeId => - html.find(params("ref"), + val ref = multiParams("splat").head + JGitUtil.getTreeId(git, ref).map{ treeId => + html.find(ref, treeId, repository, context.loginAccount match { diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 1c1271d..d06ef88 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -24,7 +24,8 @@ "activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))), "ssh" -> trim(label("SSH access", boolean())), "sshPort" -> trim(label("SSH port", optional(number()))), - "smtp" -> optionalIfNotChecked("notification", mapping( + "useSMTP" -> trim(label("SMTP", boolean())), + "smtp" -> optionalIfNotChecked("useSMTP", mapping( "host" -> trim(label("SMTP Host", text(required))), "port" -> trim(label("SMTP Port", optional(number()))), "user" -> trim(label("SMTP User", optional(text()))), diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 251abbd..f1a6fde 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -22,11 +22,13 @@ def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = IssueComments filter (_.byIssue(owner, repository, issueId)) list - /** @return IssueComment and commentedUser */ - def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account)] = + /** @return IssueComment and commentedUser and Issue */ + def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account, Issue)] = IssueComments.filter(_.byIssue(owner, repository, issueId)) .filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment")) .innerJoin(Accounts).on( (t1, t2) => t1.commentedUserName === t2.userName ) + .innerJoin(Issues).on{ case ((t1, t2), t3) => t3.byIssue(t1.userName, t1.repositoryName, t1.issueId) } + .map{ case ((t1, t2), t3) => (t1, t2, t3) } .list def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) = @@ -90,7 +92,7 @@ def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={ if(issueList.isEmpty){ Map.empty - }else{ + } else { import scala.slick.jdbc._ val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ") implicit val qset = SetParameter[Seq[(String, String, Int)]] { @@ -474,9 +476,11 @@ * Restores IssueSearchCondition instance from filter query. */ def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = { - val conditions = filter.split("[  \t]+").map { x => - val dim = x.split(":") - dim(0) -> dim(1) + 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 } diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 9473722..6c3263f 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -66,10 +66,6 @@ (t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) - PullRequests.filter { t => - t.requestRepositoryName === oldRepositoryName.bind - }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName) - // Updates activity fk before deleting repository because activity is sorted by activityId // and it can't be changed by deleting-and-inserting record. Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity => @@ -98,6 +94,11 @@ CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + // Update source repository of pull requests + PullRequests.filter { t => + (t.requestUserName === oldUserName.bind) && (t.requestRepositoryName === oldRepositoryName.bind) + }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName) + // Convert labelId val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap @@ -383,6 +384,12 @@ def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git" + def sshOpenRepoUrl(platform: String, port: Int, userName: String) = openRepoUrl(platform, sshUrl(port, userName)) + + def httpOpenRepoUrl(platform: String) = openRepoUrl(platform, httpUrl) + + def openRepoUrl(platform: String, openUrl: String) = s"github-${platform}://openRepo/${openUrl}" + /** * Creates instance with issue count and pull request count. */ diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index af4a6ef..3346bec 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -22,7 +22,8 @@ settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString)) props.setProperty(Ssh, settings.ssh.toString) settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) - if(settings.notification) { + props.setProperty(UseSMTP, settings.useSMTP.toString) + if(settings.useSMTP) { settings.smtp.foreach { smtp => props.setProperty(SmtpHost, smtp.host) smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) @@ -75,7 +76,8 @@ getOptionValue[Int](props, ActivityLogLimit, None), getValue(props, Ssh, false), getOptionValue(props, SshPort, Some(DefaultSshPort)), - if(getValue(props, Notification, false)){ + getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP + if(getValue(props, UseSMTP, getValue(props, Notification, false))){ Some(Smtp( getValue(props, SmtpHost, ""), getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)), @@ -125,6 +127,7 @@ activityLogLimit: Option[Int], ssh: Boolean, sshPort: Option[Int], + useSMTP: Boolean, smtp: Option[Smtp], ldapAuthentication: Boolean, ldap: Option[Ldap]){ @@ -172,6 +175,7 @@ private val ActivityLogLimit = "activity_log_limit" private val Ssh = "ssh" private val SshPort = "ssh.port" + private val UseSMTP = "useSMTP" private val SmtpHost = "smtp.host" private val SmtpPort = "smtp.port" private val SmtpUser = "smtp.user" diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index 04d4d50..25c8e73 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -192,7 +192,7 @@ case class WebHookPushPayload( pusher: ApiUser, ref: String, - commits: List[ApiCommit], + commits: List[ApiPushCommit], repository: ApiRepository ) extends WebHookPayload @@ -202,7 +202,7 @@ WebHookPushPayload( ApiUser(pusher), refName, - commits.map{ commit => ApiCommit(git, RepositoryName(repositoryInfo), commit) }, + commits.map{ commit => ApiPushCommit(git, RepositoryName(repositoryInfo), commit) }, ApiRepository( repositoryInfo, owner= ApiUser(repositoryOwner) @@ -273,7 +273,7 @@ action = "created", repository = ApiRepository(repository, repositoryUser), issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)), - comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser)), + comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser), issue.isPullRequest), sender = ApiUser(sender)) } } diff --git a/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala b/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala index 377b219..4169e77 100644 --- a/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala +++ b/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala @@ -21,6 +21,16 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + new Version(3, 7) with SystemSettingsService { + override def update(conn: Connection, cl: ClassLoader): Unit = { + super.update(conn, cl) + val settings = loadSystemSettings() + if(settings.notification){ + saveSystemSettings(settings.copy(useSMTP = true)) + } + } + }, + new Version(3, 6), new Version(3, 5), new Version(3, 4), new Version(3, 3), diff --git a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala index 1288eb1..27f46ac 100644 --- a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala +++ b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala @@ -14,7 +14,7 @@ private def configure(port: Int, baseUrl: String) = { server.setPort(port) - server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser")) + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser", "RSA")) server.setPublickeyAuthenticator(new PublicKeyAuthenticator) server.setCommandFactory(new GitCommandFactory(baseUrl)) server.setShellFactory(new NoShell) diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index 3eef33d..f4c35a8 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -100,8 +100,18 @@ def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress } - case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String], - oldIsImage: Boolean, newIsImage: Boolean, oldObjectId: Option[String], newObjectId: Option[String]) + case class DiffInfo( + changeType: ChangeType, + oldPath: String, + newPath: String, + oldContent: Option[String], + newContent: Option[String], + oldIsImage: Boolean, + newIsImage: Boolean, + oldObjectId: Option[String], + newObjectId: Option[String], + tooLarge: Boolean + ) /** * The file content data for the file content view of the repository viewer. @@ -495,11 +505,31 @@ while(treeWalk.next){ val newIsImage = FileUtil.isImage(treeWalk.getPathString) buffer.append((if(!fetchContent){ - DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None, false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name)) + DiffInfo( + changeType = ChangeType.ADD, + oldPath = null, + newPath = treeWalk.getPathString, + oldContent = None, + newContent = None, + oldIsImage = false, + newIsImage = newIsImage, + oldObjectId = None, + newObjectId = Option(treeWalk.getObjectId(0)).map(_.name), + tooLarge = false + ) } else { - DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, - JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray), - false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name)) + DiffInfo( + changeType = ChangeType.ADD, + oldPath = null, + newPath = treeWalk.getPathString, + oldContent = None, + newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray), + oldIsImage = false, + newIsImage = newIsImage, + oldObjectId = None, + newObjectId = Option(treeWalk.getObjectId(0)).map(_.name), + tooLarge = false + ) })) } (buffer.toList, None) @@ -518,16 +548,52 @@ import scala.collection.JavaConverters._ git.getRepository.getConfig.setString("diff", null, "renames", "copies") - git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff => - val oldIsImage = FileUtil.isImage(diff.getOldPath) - val newIsImage = FileUtil.isImage(diff.getNewPath) - if(!fetchContent || oldIsImage || newIsImage){ - DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None, oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name)) + + val diffs = git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala + diffs.map { diff => + if(diffs.size > 100){ + DiffInfo( + changeType = diff.getChangeType, + oldPath = diff.getOldPath, + newPath = diff.getNewPath, + oldContent = None, + newContent = None, + oldIsImage = false, + newIsImage = false, + oldObjectId = Option(diff.getOldId).map(_.name), + newObjectId = Option(diff.getNewId).map(_.name), + tooLarge = true + ) } else { - DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, - JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), - JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), - oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name)) + val oldIsImage = FileUtil.isImage(diff.getOldPath) + val newIsImage = FileUtil.isImage(diff.getNewPath) + if(!fetchContent || oldIsImage || newIsImage){ + DiffInfo( + changeType = diff.getChangeType, + oldPath = diff.getOldPath, + newPath = diff.getNewPath, + oldContent = None, + newContent = None, + oldIsImage = oldIsImage, + newIsImage = newIsImage, + oldObjectId = Option(diff.getOldId).map(_.name), + newObjectId = Option(diff.getNewId).map(_.name), + tooLarge = false + ) + } else { + DiffInfo( + changeType = diff.getChangeType, + oldPath = diff.getOldPath, + newPath = diff.getNewPath, + oldContent = JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), + newContent = JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), + oldIsImage = oldIsImage, + newIsImage = newIsImage, + oldObjectId = Option(diff.getOldId).map(_.name), + newObjectId = Option(diff.getNewId).map(_.name), + tooLarge = false + ) + } } }.toList } @@ -713,7 +779,7 @@ def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try { using(git.getRepository.getObjectDatabase){ db => val loader = db.open(id) - if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){ + if(loader.isLarge || (fetchLargeFile == false && FileUtil.isLarge(loader.getSize))){ None } else { Some(loader.getBytes) @@ -724,6 +790,22 @@ } /** + * Get objectLoader of the given object id from the Git repository. + * + * @param git the Git object + * @param id the object id + * @param f the function process ObjectLoader + * @return None if object does not exist + */ + def getObjectLoaderFromId[A](git: Git, id: ObjectId)(f: ObjectLoader => A):Option[A] = try { + using(git.getRepository.getObjectDatabase){ db => + Some(f(db.open(id))) + } + } catch { + case e: MissingObjectException => None + } + + /** * Returns all commit id in the specified repository. */ def getAllCommitIds(git: Git): Seq[String] = if(isEmpty(git)) { diff --git a/src/main/scala/gitbucket/core/util/Notifier.scala b/src/main/scala/gitbucket/core/util/Notifier.scala index 7eed798..b851c7b 100644 --- a/src/main/scala/gitbucket/core/util/Notifier.scala +++ b/src/main/scala/gitbucket/core/util/Notifier.scala @@ -37,7 +37,7 @@ object Notifier { // TODO We want to be able to switch to mock. def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { - case settings if settings.notification => new Mailer(settings.smtp.get) + case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get) case _ => new MockMailer } diff --git a/src/main/scala/gitbucket/core/util/StringUtil.scala b/src/main/scala/gitbucket/core/util/StringUtil.scala index 5633598..d288d49 100644 --- a/src/main/scala/gitbucket/core/util/StringUtil.scala +++ b/src/main/scala/gitbucket/core/util/StringUtil.scala @@ -20,7 +20,7 @@ md.digest.map(b => "%02x".format(b)).mkString } - def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8") + def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8").replace("+", "%20") def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8") diff --git a/src/main/scala/gitbucket/core/view/LinkConverter.scala b/src/main/scala/gitbucket/core/view/LinkConverter.scala index 59bebe6..3f2abf8 100644 --- a/src/main/scala/gitbucket/core/view/LinkConverter.scala +++ b/src/main/scala/gitbucket/core/view/LinkConverter.scala @@ -7,31 +7,92 @@ trait LinkConverter { self: RequestCache => /** - * Converts issue id, username and commit id to link. + * Creates a link to the issue or the pull request from the issue id. */ - protected def convertRefsLinks(value: String, repository: RepositoryService.RepositoryInfo, - issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = { + protected def createIssueLink(repository: RepositoryService.RepositoryInfo, issueId: Int)(implicit context: Context): String = { + val userName = repository.repository.userName + val repositoryName = repository.repository.repositoryName + + getIssue(userName, repositoryName, issueId.toString) match { + case Some(issue) if (issue.isPullRequest) => + s"""Pull #${issueId}""" + case Some(_) => + s"""Issue #${issueId}""" + case None => + s"Unknown #${issueId}" + } + } + + + /** + * Converts issue id, username and commit id to link in the given text. + */ + protected def convertRefsLinks(text: String, repository: RepositoryService.RepositoryInfo, + issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = { // escape HTML tags - val escaped = if(escapeHtml) value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) else value + val escaped = if(escapeHtml) text.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) else text escaped - // convert issue id to link - .replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m => - getIssue(repository.owner, repository.name, m.group(2)) match { - case Some(issue) if(issue.isPullRequest) - => Some(s"""#${m.group(2)}""") - case Some(_) => Some(s"""#${m.group(2)}""") - case None => Some(s"""#${m.group(2)}""") + // convert username/project@SHA to link + .replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r){ m => + getAccountByUserName(m.group(2)).map { _ => + s"""${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}""" } } + + // convert username/project#Num to link + .replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m => + getIssue(m.group(2), m.group(3), m.group(4)) match { + case Some(issue) if (issue.isPullRequest) => + Some(s"""${m.group(2)}/${m.group(3)}#${m.group(4)}""") + case Some(_) => + Some(s"""${m.group(2)}/${m.group(3)}#${m.group(4)}""") + case None => + Some(s"""${m.group(2)}/${m.group(3)}#${m.group(4)}""") + } + } + + // convert username@SHA to link + .replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r ) { m => + getAccountByUserName(m.group(2)).map { _ => + s"""${m.group(2)}@${m.group(3).substring(0, 7)}""" + } + } + + // convert username#Num to link + .replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r ) { m => + getIssue(m.group(2), repository.name, m.group(3)) match { + case Some(issue) if(issue.isPullRequest) => + Some(s"""${m.group(2)}#${m.group(3)}""") + case Some(_) => + Some(s"""${m.group(2)}#${m.group(3)}""") + case None => + Some(s"""${m.group(2)}#${m.group(3)}""") + } + } + + // convert issue id to link + .replaceBy(("(?<=(^|\\W))(GH-|" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r){ m => + val prefix = if(m.group(2) == "issue:") "#" else m.group(2) + getIssue(repository.owner, repository.name, m.group(3)) match { + case Some(issue) if(issue.isPullRequest) => + Some(s"""${prefix}${m.group(3)}""") + case Some(_) => + Some(s"""${prefix}${m.group(3)}""") + case None => + Some(s"""${m.group(2)}${m.group(3)}""") + } + } + // convert @username to link - .replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m => + .replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_\\.]+)(?=(\\W|$))".r){ m => getAccountByUserName(m.group(2)).map { _ => s"""@${m.group(2)}""" } } + // convert commit id to link - .replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""$$2""") + .replaceAll("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))", s"""$$2""") } } diff --git a/src/main/scala/gitbucket/core/view/Markdown.scala b/src/main/scala/gitbucket/core/view/Markdown.scala index 3a80b8f..a801ff4 100644 --- a/src/main/scala/gitbucket/core/view/Markdown.scala +++ b/src/main/scala/gitbucket/core/view/Markdown.scala @@ -1,18 +1,14 @@ package gitbucket.core.view import java.text.Normalizer -import java.util.Locale import java.util.regex.Pattern +import java.util.Locale import gitbucket.core.controller.Context -import gitbucket.core.service.{RepositoryService, RequestCache, WikiService} +import gitbucket.core.service.{RepositoryService, RequestCache} import gitbucket.core.util.StringUtil -import org.parboiled.common.StringUtils -import org.pegdown.LinkRenderer.Rendering -import org.pegdown._ -import org.pegdown.ast._ - -import scala.collection.JavaConverters._ +import io.github.gitbucket.markedj._ +import io.github.gitbucket.markedj.Utils._ object Markdown { @@ -24,7 +20,7 @@ * @param enableRefsLink if true then issue reference (e.g. #123) is rendered as link * @param enableAnchor if true then anchor for headline is generated * @param enableTaskList if true then task list syntax is available - * @param hasWritePermission + * @param hasWritePermission true if user has writable to ths given repository * @param pages the list of existing Wiki pages */ def toHtml(markdown: String, @@ -35,7 +31,6 @@ enableTaskList: Boolean = false, hasWritePermission: Boolean = false, pages: List[String] = Nil)(implicit context: Context): String = { - // escape issue id val s = if(enableRefsLink){ markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") @@ -43,252 +38,146 @@ // escape task list val source = if(enableTaskList){ - GitBucketHtmlSerializer.escapeTaskList(s) + escapeTaskList(s) } else s - val rootNode = new PegDownProcessor( - Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | - Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML | Extensions.STRIKETHROUGH - ).parseMarkdown(source.toCharArray) - - new GitBucketHtmlSerializer( - markdown, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, - hasWritePermission, pages - ).toHtml(rootNode) + val options = new Options() + options.setSanitize(true) + val renderer = new GitBucketMarkedRenderer(options, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages) + Marked.marked(source, options, renderer) } -} -class GitBucketLinkRender( - context: Context, - repository: RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, - pages: List[String]) extends LinkRenderer with WikiService { + /** + * Extends markedj Renderer for GitBucket + */ + class GitBucketMarkedRenderer(options: Options, repository: RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean, + pages: List[String]) + (implicit val context: Context) extends Renderer(options) with LinkConverter with RequestCache { - override def render(node: WikiLinkNode): Rendering = { - if(enableWikiLink){ - try { - val text = node.getText - val (label, page) = if(text.contains('|')){ - val i = text.indexOf('|') - (text.substring(0, i), text.substring(i + 1)) + override def heading(text: String, level: Int, raw: String): String = { + val id = generateAnchorName(text) + val out = new StringBuilder() + + out.append("") + + if(enableAnchor){ + out.append("") + out.append("") + } + + out.append(text) + out.append("\n") + out.toString() + } + + override def code(code: String, lang: String, escaped: Boolean): String = { + "
" +
+        (if(escaped) code else escape(code, true)) + "
" + } + + override def list(body: String, ordered: Boolean): String = { + var listType: String = null + if (ordered) { + listType = "ol" + } + else { + listType = "ul" + } + if(body.contains("""class="task-list-item-checkbox"""")){ + return "<" + listType + " class=\"task-list\">\n" + body + "\n" + } else { + return "<" + listType + ">\n" + body + "\n" + } + } + + override def listitem(text: String): String = { + if(text.contains("""class="task-list-item-checkbox" """)){ + return "
  • " + text + "
  • \n" + } else { + return "
  • " + text + "
  • \n" + } + } + + override def text(text: String): String = { + // convert commit id and username to link. + val t1 = if(enableRefsLink) convertRefsLinks(text, repository, "issue:", false) else text + + // convert task list to checkbox. + val t2 = if(enableTaskList) convertCheckBox(t1, hasWritePermission) else t1 + + t2 + } + + override def link(href: String, title: String, text: String): String = { + super.link(fixUrl(href, false), title, text) + } + + override def image(href: String, title: String, text: String): String = { + super.image(fixUrl(href, true), title, text) + } + + override def nolink(text: String): String = { + if(enableWikiLink && text.startsWith("[[") && text.endsWith("]]")){ + val link = text.replaceAll("(^\\[\\[|\\]\\]$)", "") + + val (label, page) = if(link.contains('|')){ + val i = link.indexOf('|') + (link.substring(0, i), link.substring(i + 1)) } else { - (text, text) + (link, link) } val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page) - if(pages.contains(page)){ - new Rendering(url, label) + "" + escape(label) + "" } else { - new Rendering(url, label).withAttribute("class", "absent") + "" + escape(label) + "" } - } catch { - case e: java.io.UnsupportedEncodingException => throw new IllegalStateException - } - } else { - super.render(node) - } - } -} - -class GitBucketVerbatimSerializer extends VerbatimSerializer { - def serialize(node: VerbatimNode, printer: Printer): Unit = { - printer.println.print("") - var text: String = node.getText - while (text.charAt(0) == '\n') { - printer.print("
    ") - text = text.substring(1) - } - printer.printEncoded(text) - printer.print("") - } -} - -class GitBucketHtmlSerializer( - markdown: String, - repository: RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, - enableRefsLink: Boolean, - enableAnchor: Boolean, - enableTaskList: Boolean, - hasWritePermission: Boolean, - pages: List[String] - )(implicit val context: Context) extends ToHtmlSerializer( - new GitBucketLinkRender(context, repository, enableWikiLink, pages), - Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava - ) with LinkConverter with RequestCache { - - override protected def printImageTag(rendering: LinkRenderer.Rendering): Unit = { - printer.print("") - .print("\"").printEncoded(rendering.text).print("\"/") - } - - override protected def printLink(rendering: LinkRenderer.Rendering): Unit = { - printer.print('<').print('a') - printAttribute("href", fixUrl(rendering.href)) - for (attr <- rendering.attributes.asScala) { - printAttribute(attr.name, attr.value) - } - printer.print('>').print(rendering.text).print("") - } - - private def fixUrl(url: String, isImage: Boolean = false): String = { - if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){ - url - } else if(url.startsWith("#")){ - ("#" + GitBucketHtmlSerializer.generateAnchorName(url.substring(1))) - } else if(!enableWikiLink){ - if(context.currentPath.contains("/blob/")){ - url + (if(isImage) "?raw=true" else "") - } else if(context.currentPath.contains("/tree/")){ - val paths = context.currentPath.split("/") - val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") } else { - val paths = context.currentPath.split("/") - val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + escape(text) } - } else { - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url - } - } - - private def printAttribute(name: String, value: String): Unit = { - printer.print(' ').print(name).print('=').print('"').print(value).print('"') - } - - private def printHeaderTag(node: HeaderNode): Unit = { - val tag = s"h${node.getLevel}" - val child = node.getChildren.asScala.headOption - val anchorName = child match { - case Some(x: AnchorLinkNode) => x.getName - case Some(x: TextNode) => x.getText - case _ => GitBucketHtmlSerializer.generateAnchorName(extractText(node)) // TODO } - printer.print(s"""<$tag class="markdown-head">""") - if(enableAnchor){ - printer.print(s"""""") - printer.print(s"""""") + private def fixUrl(url: String, isImage: Boolean = false): String = { + if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){ + url + } else if(url.startsWith("#")){ + ("#" + generateAnchorName(url.substring(1))) + } else if(!enableWikiLink){ + if(context.currentPath.contains("/blob/")){ + url + (if(isImage) "?raw=true" else "") + } else if(context.currentPath.contains("/tree/")){ + val paths = context.currentPath.split("/") + val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + } else { + val paths = context.currentPath.split("/") + val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + } + } else { + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url + } } - child match { - case Some(x: AnchorLinkNode) => printer.print(x.getText) - case _ => visitChildren(node) - } - printer.print(s"") - } - private def extractText(node: Node): String = { - val sb = new StringBuilder() - node.getChildren.asScala.map { - case x: TextNode => sb.append(x.getText) - case x: Node => sb.append(extractText(x)) - } - sb.toString() - } - - override def visit(node: HeaderNode): Unit = { - printHeaderTag(node) - } - - override def visit(node: TextNode): Unit = { - // convert commit id and username to link. - val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText - - // convert task list to checkbox. - val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t - - if (abbreviations.isEmpty) { - printer.print(text) - } else { - printWithAbbreviations(text) - } - } - - override def visit(node: VerbatimNode) { - val printer = new Printer() - val serializer = verbatimSerializers.get(VerbatimSerializer.DEFAULT) - serializer.serialize(node, printer) - val html = printer.getString - - // convert commit id and username to link. - val t = if(enableRefsLink) convertRefsLinks(html, repository, "issue:", escapeHtml = false) else html - - this.printer.print(t) - } - - override def visit(node: BulletListNode): Unit = { - if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { - printer.println().print("""") - } else { - printIndentedTag(node, "ul") - } - } - - override def visit(node: ListItemNode): Unit = { - if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { - printer.println() - printer.print("""
  • """) - visitChildren(node) - printer.print("
  • ") - } else { - printer.println() - printTag(node, "li") - } - } - - override def visit(node: ExpLinkNode) { - printLink(linkRenderer.render(node, printLinkChildrenToString(node))) - } - - def printLinkChildrenToString(node: SuperNode) = { - val priorPrinter = printer - printer = new Printer() - visitLinkChildren(node) - val result = printer.getString() - printer = priorPrinter - result - } - - def visitLinkChildren(node: SuperNode) { - import scala.collection.JavaConversions._ - node.getChildren.foreach(child => child match { - case node: ExpImageNode => visitLinkChild(node) - case node: SuperNode => visitLinkChildren(node) - case _ => child.accept(this) - }) - } - - def visitLinkChild(node: ExpImageNode) { - printer.print("\"").printEncoded(printChildrenToString(node)).print("\"/") - } -} - -object GitBucketHtmlSerializer { - - private val Whitespace = "[\\s]".r - - def generateAnchorName(text: String): String = { - val noWhitespace = Whitespace.replaceAllIn(text, "-") - val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD) - val noSpecialChars = StringUtil.urlEncode(normalized) - noSpecialChars.toLowerCase(Locale.ENGLISH) } def escapeTaskList(text: String): String = { Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ") } + def generateAnchorName(text: String): String = { + val normalized = Normalizer.normalize(text.replaceAll("<.*>", "").replaceAll("[\\s]", "-"), Normalizer.Form.NFD) + val encoded = StringUtil.urlEncode(normalized) + encoded.toLowerCase(Locale.ENGLISH) + } + def convertCheckBox(text: String, hasWritePermission: Boolean): String = { val disabled = if (hasWritePermission) "" else "disabled" text.replaceAll("task:x:", """") - .replaceAll("task: :", """") + .replaceAll("task: :", """") } + } + diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala index 6284248..3ee2c91 100644 --- a/src/main/scala/gitbucket/core/view/helpers.scala +++ b/src/main/scala/gitbucket/core/view/helpers.scala @@ -91,8 +91,12 @@ enableTaskList: Boolean = false, hasWritePermission: Boolean = false, pages: List[String] = Nil)(implicit context: Context): Html = - Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, true, hasWritePermission, pages)) + Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, true, enableTaskList, hasWritePermission, pages)) + /** + * Render the given source (only markdown is supported in default) as HTML. + * You can test if a file is renderable in this method by [[isRenderable()]]. + */ def renderMarkup(filePath: List[String], fileContent: String, branch: String, repository: RepositoryService.RepositoryInfo, enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = { @@ -103,11 +107,21 @@ renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context)) } + /** + * Tests whether the given file is renderable. It's tested by the file extension. + */ def isRenderable(fileName: String): Boolean = { PluginRegistry().renderableExtensions.exists(extension => fileName.toLowerCase.endsWith("." + extension)) } /** + * Creates a link to the issue or the pull request from the issue id. + */ + def issueLink(repository: RepositoryService.RepositoryInfo, issueId: Int)(implicit context: Context): Html = { + Html(createIssueLink(repository, issueId)) + } + + /** * Returns <img> which displays the avatar icon for the given user name. * This method looks up Gravatar if avatar icon has not been configured in user settings. */ @@ -275,4 +289,11 @@ case CommitState.ERROR => "Failed" case CommitState.FAILURE => "Failed" } + + // This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string) + private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r + + def detectAndRenderLinks(text: String): Html = { + Html(detectAndRenderLinksRegex.replaceAllIn(text, m => s"""${m.group(0)}""")) + } } diff --git a/src/main/twirl/gitbucket/core/account/main.scala.html b/src/main/twirl/gitbucket/core/account/main.scala.html index b4467f9..22e9c28 100644 --- a/src/main/twirl/gitbucket/core/account/main.scala.html +++ b/src/main/twirl/gitbucket/core/account/main.scala.html @@ -14,9 +14,9 @@
    @if(account.url.isDefined){ -
    @account.url
    +
    @account.url
    } -
    Joined on @date(account.registeredDate)
    +
    Joined on @date(account.registeredDate)
    @if(groupNames.nonEmpty){
    diff --git a/src/main/twirl/gitbucket/core/account/newrepo.scala.html b/src/main/twirl/gitbucket/core/account/newrepo.scala.html index 0a0869b..2fe1915 100644 --- a/src/main/twirl/gitbucket/core/account/newrepo.scala.html +++ b/src/main/twirl/gitbucket/core/account/newrepo.scala.html @@ -13,7 +13,7 @@
    -
    +
    -
    + } } @@ -292,8 +306,16 @@ $('.ssh input').prop('disabled', !$(this).prop('checked')); }).change(); - $('#notification').change(function(){ - $('.notification input').prop('disabled', !$(this).prop('checked')); + $('#useSMTP').change(function(){ + $('.useSMTP input').prop('disabled', !$(this).prop('checked')); + + // With only SMTP in current version, notification cannot be enabled if no communication channel exists + $('#notification').prop('disabled', !$(this).prop('checked')); + + if (!$(this).prop('checked')) { + // With only SMTP in current version, if SMTP is unchecked then we disable notification + $('#notification').prop('checked', false); + } }).change(); $('#ldapAuthentication').change(function(){ diff --git a/src/main/twirl/gitbucket/core/admin/users/list.scala.html b/src/main/twirl/gitbucket/core/admin/users/list.scala.html index 1c8f17b..d18f4c6 100644 --- a/src/main/twirl/gitbucket/core/admin/users/list.scala.html +++ b/src/main/twirl/gitbucket/core/admin/users/list.scala.html @@ -43,10 +43,10 @@

    @if(!account.isGroupAccount){ - @account.mailAddress + @account.mailAddress } @account.url.map { url => - @url + @url }
    diff --git a/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html b/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html index f97e166..5451506 100644 --- a/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html +++ b/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html @@ -8,7 +8,7 @@ @helper.html.dropdown( value = if(branch.length == 40) branch.substring(0, 10) else branch, prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", - mini = true + mini = false ) {
  • Switch branches
  • diff --git a/src/main/twirl/gitbucket/core/helper/checkicon.scala.html b/src/main/twirl/gitbucket/core/helper/checkicon.scala.html index b9726a7..308ee41 100644 --- a/src/main/twirl/gitbucket/core/helper/checkicon.scala.html +++ b/src/main/twirl/gitbucket/core/helper/checkicon.scala.html @@ -1,6 +1,6 @@ @(condition: => Boolean) @if(condition){ - + } else { } \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html b/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html index d9ff70c..bc5c531 100644 --- a/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html +++ b/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html @@ -5,10 +5,13 @@ @import context._ @import gitbucket.core._ @import gitbucket.core.view.helpers._ -
    +
    @avatar(comment.commentedUserName, 48)
    -
    +
    @user(comment.commentedUserName, styleClass="username strong") commented @@ -24,12 +27,12 @@ @if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){ -   - +   + }
    -
    +
    @markdown(comment.content, repository, false, true, true, hasWritePermission)
    diff --git a/src/main/twirl/gitbucket/core/helper/copy.scala.html b/src/main/twirl/gitbucket/core/helper/copy.scala.html index 7a09a52..55c4d0d 100644 --- a/src/main/twirl/gitbucket/core/helper/copy.scala.html +++ b/src/main/twirl/gitbucket/core/helper/copy.scala.html @@ -1,7 +1,7 @@ -@(id: String, value: String)(html: Html) -
    +@(id: String, value: String, prepend: Boolean = false)(html: Html) +
    @html - +
    \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/issues/commentform.scala.html b/src/main/twirl/gitbucket/core/issues/commentform.scala.html index d2bbb2f..040b9e0 100644 --- a/src/main/twirl/gitbucket/core/issues/commentform.scala.html +++ b/src/main/twirl/gitbucket/core/issues/commentform.scala.html @@ -17,15 +17,16 @@ enableRefsLink = true, enableTaskList = true, hasWritePermission = hasWritePermission, - style = "width: 635px; height: 100px; max-height: 150px;", - elastic = true + style = "", + elastic = true, + tabIndex = 1 )
    - @if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){ - + } +
    diff --git a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html index 84ffb7f..bed5ea8 100644 --- a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html +++ b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html @@ -13,7 +13,7 @@ @user(issue.get.openedUserName, styleClass="username strong") commented @helper.html.datetimeago(issue.get.registeredDate) @if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){ - + }
    @@ -41,8 +41,8 @@ @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ -   - +   + }
    @@ -55,7 +55,7 @@ } else { @if(comment.action == "refer"){ @defining(comment.content.split(":")){ case Array(issueId, rest @ _*) => - Issue #@issueId: @rest.mkString(":") + @issueLink(repository, issueId.toInt): @rest.mkString(":") } } else {
    @markdown(comment.content, repository, false, true, true, hasWritePermission)
    @@ -78,8 +78,8 @@
    } @if(comment.action == "close" || comment.action == "close_comment"){ -
    - Closed +
    + @avatar(comment.commentedUserName, 20) @if(issue.isDefined && issue.get.isPullRequest){ @user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate) @@ -89,14 +89,14 @@
    } @if(comment.action == "reopen" || comment.action == "reopen_comment"){ -
    - Reopened +
    + @avatar(comment.commentedUserName, 20) @user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
    } @if(comment.action == "delete_branch"){ -
    +
    Deleted @avatar(comment.commentedUserName, 20) @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @helper.html.datetimeago(comment.registeredDate) @@ -110,7 +110,7 @@ diff --git a/src/main/twirl/gitbucket/core/issues/editcomment.scala.html b/src/main/twirl/gitbucket/core/issues/editcomment.scala.html index 07a815d..9c6aa73 100644 --- a/src/main/twirl/gitbucket/core/issues/editcomment.scala.html +++ b/src/main/twirl/gitbucket/core/issues/editcomment.scala.html @@ -2,7 +2,7 @@ @import context._ @helper.html.attached(owner, repository){ - + }
    diff --git a/src/main/twirl/gitbucket/core/issues/editissue.scala.html b/src/main/twirl/gitbucket/core/issues/editissue.scala.html index 5dfb18f..9494542 100644 --- a/src/main/twirl/gitbucket/core/issues/editissue.scala.html +++ b/src/main/twirl/gitbucket/core/issues/editissue.scala.html @@ -1,7 +1,7 @@ @(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: gitbucket.core.controller.Context) @import context._ @helper.html.attached(owner, repository){ - + }
    diff --git a/src/main/twirl/gitbucket/core/issues/issue.scala.html b/src/main/twirl/gitbucket/core/issues/issue.scala.html index 2ab4958..c5aef70 100644 --- a/src/main/twirl/gitbucket/core/issues/issue.scala.html +++ b/src/main/twirl/gitbucket/core/issues/issue.scala.html @@ -45,13 +45,14 @@


    +
    @commentlist(Some(issue), comments, hasWritePermission, repository) @commentform(issue, true, hasWritePermission, repository)
    - @issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) + @issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
    } diff --git a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html index 2c5063a..9f790c3 100644 --- a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html +++ b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html @@ -1,4 +1,4 @@ -@(issue: gitbucket.core.model.Issue, +@(issue: Option[gitbucket.core.model.Issue], comments: List[gitbucket.core.model.Comment], issueLabels: List[gitbucket.core.model.Label], collaborators: List[String], @@ -6,6 +6,7 @@ labels: List[gitbucket.core.model.Label], hasWritePermission: Boolean, repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ @import gitbucket.core.view.helpers._
    Labels @@ -22,6 +23,9 @@ } } + @if(issue.isEmpty){ + + }
    }
    @@ -34,11 +38,14 @@ @if(hasWritePermission){ - @issue.assignedUserName.map { userName => + @issue.flatMap(_.assignedUserName).map { userName => @avatar(userName, 20) @user(userName, styleClass="username strong small") }.getOrElse{ No one } -
    -
    - @defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants => -
    @participants.size @plural(participants.size, "participant")
    - @participants.map { participant => @avatarLink(participant, 20, tooltip = true) } - } -
    +@if(issue.isEmpty){ + +} +@issue.map { issue => +
    +
    + @defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants => +
    @participants.size @plural(participants.size, "participant")
    + @participants.map { participant => + @avatarLink(participant, 20, tooltip = true) + } + } +
    +} diff --git a/src/main/twirl/gitbucket/core/issues/listparts.scala.html b/src/main/twirl/gitbucket/core/issues/listparts.scala.html index 1ea3e34..bd2aafe 100644 --- a/src/main/twirl/gitbucket/core/issues/listparts.scala.html +++ b/src/main/twirl/gitbucket/core/issues/listparts.scala.html @@ -138,7 +138,7 @@ } } @helper.html.dropdown("Assignee", flat = true) { -
  • Clear assignee
  • +
  • Clear assignee
  • @collaborators.map { collaborator =>
  • @avatar(collaborator, 20) @collaborator
  • } diff --git a/src/main/twirl/gitbucket/core/menu.scala.html b/src/main/twirl/gitbucket/core/menu.scala.html index e8777d1..7e71382 100644 --- a/src/main/twirl/gitbucket/core/menu.scala.html +++ b/src/main/twirl/gitbucket/core/menu.scala.html @@ -11,7 +11,6 @@ @sidemenu(path: String, name: String, icon: String, label: String, count: Int = 0) = {
  • -
    @if(expand){ @label} @@ -40,7 +39,7 @@ @repository.forkedCount
  • @if(loginAccount.isDefined && isNoGroup){ -
    +
    } @@ -86,19 +85,26 @@
    } @id.map { id => + @if(context.platform != "linux" && context.platform != null){ + + } + @* + *@ } }
    @if(expand){ @repository.repository.description.map { description => -

    @description

    +

    @detectAndRenderLinks(description)

    }
    @@ -171,12 +177,14 @@ $('#repository-url-http').click(function(){ $('#repository-url-proto').text('HTTP'); $('#repository-url').val('@repository.httpUrl'); + $('#repository-clone-url').attr('href', '@repository.httpOpenRepoUrl(context.platform)') $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); }); $('#repository-url-ssh').click(function(){ $('#repository-url-proto').text('SSH'); $('#repository-url').val('@repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)'); + $('#repository-clone-url').attr('href', '@repository.sshOpenRepoUrl(context.platform, settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)'); $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); }); } diff --git a/src/main/twirl/gitbucket/core/pulls/commits.scala.html b/src/main/twirl/gitbucket/core/pulls/commits.scala.html index 43ed945..8a9a4cc 100644 --- a/src/main/twirl/gitbucket/core/pulls/commits.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/commits.scala.html @@ -4,32 +4,42 @@ @import context._ @import gitbucket.core.view.helpers._ @import gitbucket.core.model._ -
    -
    - @commits.map { day => - - - - @day.map { commit => - - - - - - - } +
    + @commits.map { day => +
    + Commits on @date(day.head.commitTime) +
    +
    + @day.map { commit => +
    + +
    +
    @avatar(commit, 40)
    +
    + @link(commit.summary, repository) + @if(commit.description.isDefined){ + ... + } +
    + @if(commit.description.isDefined){ + + } +
    + @user(commit.authorName, commit.authorEmailAddress, "username") + authored @helper.html.datetimeago(commit.authorTime) + @if(commit.isDifferentFromAuthor) { + + @user(commit.committerName, commit.committerEmailAddress, "username") + committed @helper.html.datetimeago(commit.authorTime) + } +
    +
    +
    +
    } -
    @date(day.head.commitTime)
    - @avatar(commit, 20) - @user(commit.authorName, commit.authorEmailAddress, "username") - @commit.shortMessage - @if(comments.isDefined){ - @comments.get.flatMap @{ - case comment: CommitComment => Some(comment) - case other => None - }.count(t => t.commitId == commit.id && !t.pullRequest) - } - - @commit.id.substring(0, 7) -
    +
    + }
    diff --git a/src/main/twirl/gitbucket/core/pulls/compare.scala.html b/src/main/twirl/gitbucket/core/pulls/compare.scala.html index 2b728fd..d68a129 100644 --- a/src/main/twirl/gitbucket/core/pulls/compare.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/compare.scala.html @@ -9,18 +9,16 @@ repository: gitbucket.core.service.RepositoryService.RepositoryInfo, originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo, forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) + hasOriginWritePermission: Boolean, + collaborators: List[String], + milestones: List[gitbucket.core.model.Milestone], + labels: List[gitbucket.core.model.Label])(implicit context: gitbucket.core.controller.Context) @import context._ @import gitbucket.core.view.helpers._ @html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){ @html.menu("pulls", repository){
    -
    - Edit - @originRepository.owner:@originId ... @forkedRepository.owner:@forkedId -
    - - @if(commits.nonEmpty && hasWritePermission){ -
    - Create pull request + -