diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 899bc34..d7234da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,7 @@ # Guideline for Issues -- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/takezoe/gitbucket) before raise an issue. +- 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/takezoe/gitbucket_ja). +- 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 b98d67f..f596073 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. @@ -20,12 +20,12 @@ - Gravatar support - Plug-in system -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. @@ -42,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 @@ -65,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, generate `gitbucket.plist` by [this script](https://raw.githubusercontent.com/gitbucket/gitbucket/master/contrib/macosx/makePlist) and copy it to `~/Library/LaunchAgents/` Run the following commands in `Terminal` to @@ -81,18 +81,28 @@ - [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) +- [gitbucket-asciidoctor-plugin](https://github.com/lefou/gitbucket-asciidoctor-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/takezoe/gitbucket) before raise an issue. +- 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/takezoe/gitbucket_ja). +- 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.8 - 31 Oct 2015 +- Moved to GitHub organization +- Omit diff view for large differences +- Repository creation API +- Render url as link in repository description +- Expand attachable file types + ### 3.7 - 3 Oct 2015 - Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown - Clone in desktop button 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 ef1e1f7..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 [gitbucket.core.servlet.AutoUpdate](https://github.com/takezoe/gitbucket/blob/master/src/main/scala/gitbucket/core/servlet/AutoUpdate.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/project/build.scala b/project/build.scala index 1337898..f17efbe 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.7.0" + val Version = "3.8.0" val ScalaVersion = "2.11.6" val ScalatraVersion = "2.3.1" @@ -51,11 +51,12 @@ "org.json4s" %% "json4s-jackson" % "3.2.11", "jp.sf.amateras" %% "scalatra-forms" % "0.1.0", "commons-io" % "commons-io" % "2.4", - "io.github.gitbucket" % "markedj" % "1.0.4-SNAPSHOT", + "io.github.gitbucket" % "markedj" % "1.0.4", "org.apache.commons" % "commons-compress" % "1.9", "org.apache.commons" % "commons-email" % "1.3.3", "org.apache.httpcomponents" % "httpclient" % "4.3.6", "org.apache.sshd" % "apache-sshd" % "0.11.0", + "org.apache.tika" % "tika-core" % "1.10", "com.typesafe.slick" %% "slick" % "2.1.0", "com.novell.ldap" % "jldap" % "2009-10-07", "com.h2database" % "h2" % "1.4.180", 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/ApiCommit.scala b/src/main/scala/gitbucket/core/api/ApiCommit.scala index 95128c1..9229d0c 100644 --- a/src/main/scala/gitbucket/core/api/ApiCommit.scala +++ b/src/main/scala/gitbucket/core/api/ApiCommit.scala @@ -20,13 +20,21 @@ removed: List[String], modified: List[String], author: ApiPersonIdent, - 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}") + committer: ApiPersonIdent)(repositoryName:RepositoryName, urlIsHtmlUrl: Boolean) extends FieldSerializable{ + val url = if(urlIsHtmlUrl){ + ApiPath(s"/${repositoryName.fullName}/commit/${id}") + }else{ + ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}") + } + val html_url = if(urlIsHtmlUrl){ + None + }else{ + Some(ApiPath(s"/${repositoryName.fullName}/commit/${id}")) + } } object ApiCommit{ - def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = { + def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo, urlIsHtmlUrl: Boolean = false): ApiCommit = { val diffs = JGitUtil.getDiffs(git, commit.id, false) ApiCommit( id = commit.id, @@ -43,6 +51,7 @@ }, author = ApiPersonIdent.author(commit), committer = ApiPersonIdent.committer(commit) - )(repositoryName) + )(repositoryName, urlIsHtmlUrl) } + def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true) } diff --git a/src/main/scala/gitbucket/core/api/ApiPushCommit.scala b/src/main/scala/gitbucket/core/api/ApiPushCommit.scala deleted file mode 100644 index 46a99e0..0000000 --- a/src/main/scala/gitbucket/core/api/ApiPushCommit.scala +++ /dev/null @@ -1,39 +0,0 @@ -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 ee97ea3..dce8443 100644 --- a/src/main/scala/gitbucket/core/api/ApiRepository.scala +++ b/src/main/scala/gitbucket/core/api/ApiRepository.scala @@ -13,10 +13,14 @@ forks: Int, `private`: Boolean, default_branch: String, - owner: ApiUser) { + owner: ApiUser)(urlIsHtmlUrl: Boolean) { val forks_count = forks val watchers_count = watchers - val url = ApiPath(s"/api/v3/repos/${full_name}") + val url = if(urlIsHtmlUrl){ + ApiPath(s"/${full_name}") + }else{ + 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") val html_url = ApiPath(s"/${full_name}") @@ -27,7 +31,8 @@ repository: Repository, owner: ApiUser, forkedCount: Int =0, - watchers: Int = 0): ApiRepository = + watchers: Int = 0, + urlIsHtmlUrl: Boolean = false): ApiRepository = ApiRepository( name = repository.repositoryName, full_name = s"${repository.userName}/${repository.repositoryName}", @@ -37,7 +42,7 @@ `private` = repository.isPrivate, default_branch = repository.defaultBranch, owner = owner - ) + )(urlIsHtmlUrl) def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository = ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount) @@ -45,4 +50,7 @@ def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository = this(repositoryInfo.repository, ApiUser(owner)) + def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository = + ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true) + } diff --git a/src/main/scala/gitbucket/core/api/CreateARepository.scala b/src/main/scala/gitbucket/core/api/CreateARepository.scala new file mode 100644 index 0000000..7085559 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/CreateARepository.scala @@ -0,0 +1,19 @@ +package gitbucket.core.api + +/** + * https://developer.github.com/v3/repos/#create + * api form + */ +case class CreateARepository( + name: String, + description: Option[String], + `private`: Boolean = false, + auto_init: Boolean = false +) { + def isValid: Boolean = { + name.length<=40 && + name.matches("[a-zA-Z0-9\\-\\+_.]+") && + !name.startsWith("_") && + !name.startsWith("-") + } +} diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 5383eaa..e516a03 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -366,56 +366,7 @@ post("/new", newRepositoryForm)(usersOnly { form => LockUtil.lock(s"${form.owner}/${form.name}"){ if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ - val ownerAccount = getAccountByUserName(form.owner).get - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - - // Insert to the database at first - createRepository(form.name, form.owner, form.description, form.isPrivate) - - // Add collaborators for group repository - if(ownerAccount.isGroupAccount){ - getGroupMembers(form.owner).foreach { member => - addCollaborator(form.owner, form.name, member.userName) - } - } - - // Insert default labels - insertDefaultLabels(form.owner, form.name) - - // Create the actual repository - val gitdir = getRepositoryDir(form.owner, form.name) - JGitUtil.initRepository(gitdir) - - if(form.createReadme){ - using(Git.open(gitdir)){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - val content = if(form.description.nonEmpty){ - form.name + "\n" + - "===============\n" + - "\n" + - form.description.get - } else { - form.name + "\n" + - "===============\n" - } - - builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, - inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) - builder.finish() - - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), - Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") - } - } - - // Create Wiki repository - createWikiRepository(loginAccount, form.owner, form.name) - - // Record activity - recordCreateRepositoryActivity(form.owner, form.name, loginUserName) + createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme) } // redirect to the repository @@ -423,6 +374,54 @@ } }) + /** + * Create user repository + * https://developer.github.com/v3/repos/#create + */ + post("/api/v3/user/repos")(usersOnly { + val owner = context.loginAccount.get.userName + (for { + data <- extractFromJsonBody[CreateARepository] if data.isValid + } yield { + LockUtil.lock(s"${owner}/${data.name}") { + if(getRepository(owner, data.name, context.baseUrl).isEmpty){ + createRepository(owner, data.name, data.description, data.`private`, data.auto_init) + val repository = getRepository(owner, data.name, context.baseUrl).get + JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get))) + } else { + ApiError( + "A repository with this name already exists on this account", + Some("https://developer.github.com/v3/repos/#create") + ) + } + } + }) getOrElse NotFound + }) + + /** + * Create group repository + * https://developer.github.com/v3/repos/#create + */ + post("/api/v3/orgs/:org/repos")(managersOnly { + val groupName = params("org") + (for { + data <- extractFromJsonBody[CreateARepository] if data.isValid + } yield { + LockUtil.lock(s"${groupName}/${data.name}") { + if(getRepository(groupName, data.name, context.baseUrl).isEmpty){ + createRepository(groupName, data.name, data.description, data.`private`, data.auto_init) + val repository = getRepository(groupName, data.name, context.baseUrl).get + JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get))) + } else { + ApiError( + "A repository with this name already exists for this group", + Some("https://developer.github.com/v3/repos/#create") + ) + } + } + }) getOrElse NotFound + }) + get("/:owner/:repository/fork")(readableUsersOnly { repository => val loginAccount = context.loginAccount.get val loginUserName = loginAccount.userName @@ -496,6 +495,59 @@ } }) + private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) { + val ownerAccount = getAccountByUserName(owner).get + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + + // Insert to the database at first + createRepository(name, owner, description, isPrivate) + + // Add collaborators for group repository + if(ownerAccount.isGroupAccount){ + getGroupMembers(owner).foreach { member => + addCollaborator(owner, name, member.userName) + } + } + + // Insert default labels + insertDefaultLabels(owner, name) + + // Create the actual repository + val gitdir = getRepositoryDir(owner, name) + JGitUtil.initRepository(gitdir) + + if(createReadme){ + using(Git.open(gitdir)){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + val content = if(description.nonEmpty){ + name + "\n" + + "===============\n" + + "\n" + + description.get + } else { + name + "\n" + + "===============\n" + } + + builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, + inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) + builder.finish() + + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") + } + } + + // Create Wiki repository + createWikiRepository(loginAccount, owner, name) + + // Record activity + recordCreateRepositoryActivity(owner, name, loginUserName) + } + private def insertDefaultLabels(userName: String, repositoryName: String): Unit = { createLabel(userName, repositoryName, "bug", "fc2929") createLabel(userName, repositoryName, "duplicate", "cccccc") diff --git a/src/main/scala/gitbucket/core/controller/FileUploadController.scala b/src/main/scala/gitbucket/core/controller/FileUploadController.scala index c3503ac..d6213a2 100644 --- a/src/main/scala/gitbucket/core/controller/FileUploadController.scala +++ b/src/main/scala/gitbucket/core/controller/FileUploadController.scala @@ -17,22 +17,22 @@ configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) post("/image"){ - execute { (file, fileId) => + execute({ (file, fileId) => FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get) session += Keys.Session.Upload(fileId) -> file.name - } + }, FileUtil.isImage) } - post("/image/:owner/:repository"){ - execute { (file, fileId) => + post("/file/:owner/:repository"){ + execute({ (file, fileId) => FileUtils.writeByteArrayToFile(new java.io.File( getAttachedDir(params("owner"), params("repository")), fileId + "." + FileUtil.getExtension(file.getName)), file.get) - } + }, FileUtil.isUploadableType) } - private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match { - case Some(file) if(FileUtil.isImage(file.name)) => + private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match { + case Some(file) if(mimeTypeChcker(file.name)) => defining(FileUtil.generateFileId){ fileId => f(file, fileId) diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index ddd49fc..21821a5 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -332,6 +332,7 @@ (Directory.getAttachedDir(repository.owner, repository.name) match { case dir if(dir.exists && dir.isDirectory) => dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => + response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""") RawData(FileUtil.getMimeType(file.getName), file) } case _ => None @@ -352,6 +353,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 diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 6cf7d4e..058b1eb 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -294,6 +294,9 @@ originRepositoryName <- if(originOwner == forkedOwner) { // Self repository Some(forkedRepository.name) + } else if(forkedRepository.repository.originUserName.isEmpty){ + // when ForkedRepository is the original repository + getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) } else if(Some(originOwner) == forkedRepository.repository.originUserName){ // Original repository forkedRepository.repository.originRepositoryName @@ -436,8 +439,11 @@ // call web hook callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get) - // notifications 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}") } @@ -447,6 +453,19 @@ } }) + // 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") + } + } + } + } + /** * Parses branch identifier and extracts owner and branch name as tuple. * diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 2f0b742..916f6d4 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -15,6 +15,7 @@ import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Constants import scala.util.{Success, Failure} +import org.eclipse.jgit.lib.ObjectId class RepositorySettingsController extends RepositorySettingsControllerBase @@ -182,11 +183,21 @@ import scala.concurrent.ExecutionContext.Implicits.global val url = params("url") - val commits = if(repository.commitCount == 0) List.empty else git.log - .add(git.getRepository.resolve(repository.repository.defaultBranch)) - .setMaxCount(3) - .call.iterator.asScala.map(new CommitInfo(_)) - val ownerAccount = getAccountByUserName(repository.owner).get + val dummyPayload = { + val ownerAccount = getAccountByUserName(repository.owner).get + val commits = if(repository.commitCount == 0) List.empty else git.log + .add(git.getRepository.resolve(repository.repository.defaultBranch)) + .setMaxCount(4) + .call.iterator.asScala.map(new CommitInfo(_)).toList + val pushedCommit = commits.drop(1) + WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, pushedCommit, ownerAccount, + oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()), + newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId())) + } + val dummyWebHookInfo = WebHook(repository.owner, repository.name, url) + + val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head + def headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map{ h => Array(h.getName, h.getValue) } val toErrorMap:PartialFunction[Throwable, Map[String,String]] = { case e:java.net.UnknownHostException => Map("error"-> ("Unknown host "+ e.getMessage)) @@ -194,10 +205,6 @@ case e:org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url")) case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage)) } - val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, - List(WebHook(repository.owner, repository.name, url)), - WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount) - ).head contentType = formats("json") var result = Map( "url" -> url, diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index e83d334..c6bcad7 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -293,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, @@ -665,7 +669,8 @@ val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) callWebHookOf(repository.owner, repository.name, WebHook.Push) { getAccountByUserName(repository.owner).map{ ownerAccount => - WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount) + WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount, + oldId = headTip, newId = commitId) } } } diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 67dde49..f1a6fde 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -92,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)]] { diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 843cc3a..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 diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index c212158..c168395 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -12,6 +12,7 @@ import org.apache.http.client.entity.UrlEncodedFormEntity import org.apache.http.message.BasicNameValuePair import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.ObjectId import org.slf4j.LoggerFactory import scala.concurrent._ import org.apache.http.HttpRequest @@ -244,21 +245,33 @@ case class WebHookPushPayload( pusher: ApiUser, ref: String, - commits: List[ApiPushCommit], + before: String, + after: String, + commits: List[ApiCommit], repository: ApiRepository - ) extends WebHookPayload + ) extends FieldSerializable with WebHookPayload { + val compare = commits.size match { + case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initalied repository + case 1 => ApiPath(s"/${repository.full_name}/commit/${after}") + case _ if before.filterNot(_=='0').isEmpty => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}") + case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}") + } + val head_commit = commits.lastOption + } object WebHookPushPayload { def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo, - commits: List[CommitInfo], repositoryOwner: Account): WebHookPushPayload = + commits: List[CommitInfo], repositoryOwner: Account, + newId: ObjectId, oldId: ObjectId): WebHookPushPayload = WebHookPushPayload( - ApiUser(pusher), - refName, - commits.map{ commit => ApiPushCommit(git, RepositoryName(repositoryInfo), commit) }, - ApiRepository( + pusher = ApiUser(pusher), + ref = refName, + before = ObjectId.toString(oldId), + after = ObjectId.toString(newId), + commits = commits.map{ commit => ApiCommit.forPushPayload(git, RepositoryName(repositoryInfo), commit) }, + repository = ApiRepository.forPushPayload( repositoryInfo, - owner= ApiUser(repositoryOwner) - ) + owner= ApiUser(repositoryOwner)) ) } diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index 677ad8c..a2866f1 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -203,7 +203,8 @@ callWebHookOf(owner, repository, WebHook.Push){ for(pusherAccount <- getAccountByUserName(pusher); ownerAccount <- getAccountByUserName(owner)) yield { - WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount) + WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount, + newId = command.getNewId(), oldId = command.getOldId()) } } } 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/FileUtil.scala b/src/main/scala/gitbucket/core/util/FileUtil.scala index d3428fb..f753a60 100644 --- a/src/main/scala/gitbucket/core/util/FileUtil.scala +++ b/src/main/scala/gitbucket/core/util/FileUtil.scala @@ -1,7 +1,7 @@ package gitbucket.core.util import org.apache.commons.io.FileUtils -import java.net.URLConnection +import org.apache.tika.Tika import java.io.File import ControlUtil._ import scala.util.Random @@ -9,8 +9,8 @@ object FileUtil { def getMimeType(name: String): String = - defining(URLConnection.getFileNameMap()){ fileNameMap => - fileNameMap.getContentTypeFor(name) match { + defining(new Tika()){ tika => + tika.detect(name) match { case null => "application/octet-stream" case mimeType => mimeType } @@ -28,6 +28,8 @@ def isImage(name: String): Boolean = getMimeType(name).startsWith("image/") + def isUploadableType(name: String): Boolean = mimeTypeWhiteList contains getMimeType(name) + def isLarge(size: Long): Boolean = (size > 1024 * 1000) def isText(content: Array[Byte]): Boolean = !content.contains(0) @@ -50,4 +52,14 @@ FileUtils.deleteDirectory(dir) } } + + val mimeTypeWhiteList: Array[String] = Array( + "application/pdf", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "image/gif", + "image/jpeg", + "image/png", + "text/plain") } diff --git a/src/main/scala/gitbucket/core/util/Implicits.scala b/src/main/scala/gitbucket/core/util/Implicits.scala index 5b6f71b..131bd98 100644 --- a/src/main/scala/gitbucket/core/util/Implicits.scala +++ b/src/main/scala/gitbucket/core/util/Implicits.scala @@ -65,6 +65,7 @@ def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{ case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */) + case path if path.startsWith("api/v3/orgs/") => path.substring(12/* "/api/v3/orgs".length */) case path => path }).split("/") 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/view/LinkConverter.scala b/src/main/scala/gitbucket/core/view/LinkConverter.scala index 36d55f5..3f2abf8 100644 --- a/src/main/scala/gitbucket/core/view/LinkConverter.scala +++ b/src/main/scala/gitbucket/core/view/LinkConverter.scala @@ -7,13 +7,31 @@ 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, + 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 username/project@SHA to link @@ -26,10 +44,12 @@ // 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)}""") + 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)}""") } } @@ -43,10 +63,12 @@ // 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)}""") + 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)}""") } } @@ -54,10 +76,12 @@ .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)}""") + 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)}""") } } diff --git a/src/main/scala/gitbucket/core/view/Markdown.scala b/src/main/scala/gitbucket/core/view/Markdown.scala index 354998d..a801ff4 100644 --- a/src/main/scala/gitbucket/core/view/Markdown.scala +++ b/src/main/scala/gitbucket/core/view/Markdown.scala @@ -110,7 +110,7 @@ } override def link(href: String, title: String, text: String): String = { - super.link(fixUrl(href, true), title, text) + super.link(fixUrl(href, false), title, text) } override def image(href: String, title: String, text: String): String = { diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala index 0d1a9a5..3ee2c91 100644 --- a/src/main/scala/gitbucket/core/view/helpers.scala +++ b/src/main/scala/gitbucket/core/view/helpers.scala @@ -93,6 +93,10 @@ pages: List[String] = Nil)(implicit context: Context): Html = 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/helper/attached.scala.html b/src/main/twirl/gitbucket/core/helper/attached.scala.html index 33bd58e..0ceb228 100644 --- a/src/main/twirl/gitbucket/core/helper/attached.scala.html +++ b/src/main/twirl/gitbucket/core/helper/attached.scala.html @@ -1,22 +1,24 @@ @(owner: String, repository: String)(textarea: Html)(implicit context: gitbucket.core.controller.Context) @import context._ +@import gitbucket.core.util.FileUtil
@textarea -
Attach images by dragging & dropping, or selecting them.
+
Attach images or documents by dragging & dropping, or selecting them.
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>