diff --git a/README.md b/README.md index eb3b260..b1ab25e 100644 --- a/README.md +++ b/README.md @@ -24,35 +24,30 @@ -------- GitBucket requires **Java8**. You have to install beforehand when it's not installed. -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 and logged-in with **root** / **root**. +1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`. +2. Access `http://[hostname]:8080/` and logged in with **root** / **root**. -or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options. +You can specify following options: -- --port=[NUMBER] -- --prefix=[CONTEXTPATH] -- --host=[HOSTNAME] -- --gitbucket.home=[DATA_DIR] +- `--port=[NUMBER]` +- `--prefix=[CONTEXTPATH]` +- `--host=[HOSTNAME]` +- `--gitbucket.home=[DATA_DIR]` -To upgrade GitBucket, only replace gitbucket.war after stop GitBucket. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk. +Of course, you can also deploy gitbucket.war to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc) About installation on Mac or Windows Server (with IIS), configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki). +To upgrade GitBucket, only replace gitbucket.war after stop GitBucket. All GitBucket data is stored in `HOME/.gitbucket` in default. So if you want to back up GitBucket data, copy this directory to the other disk. + Plug-ins -------- -GitBucket has the plug-in system to extend GitBucket from outside of GitBucket. Some plug-ins are available now: +GitBucket has the plug-in system to extend GitBucket from outside of GitBucket. We are providing some official plug-ins: - [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) -- [gitbucket-asciidoctor-plugin](https://github.com/lefou/gitbucket-asciidoctor-plugin) -- [gitbucket-network-plugin](https://github.com/mrkm4ntr/gitbucket-network-plugin) - [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin) -You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/). +You can find more plugins made by community at [gitbucket community plugins](http://gitbucket-plugins.github.io/). Support -------- @@ -65,6 +60,24 @@ Release Notes ------------- +## 4.8 - 23 Dec 2016 +- Search for repository names from the global header +- Filter repositories on the sidebar of the dashboard +- Search issues and wiki +- Keep pull request comments after new commits are pushed +- New web API to get a single issue +- Performance improvement for the repository viewer + +### 4.7.1 - 28 Nov 2016 +- Bug fix: group repositories are not shown in the your repositories list on the sidebar +- Small performance improvement of the dashboard + +### 4.7 - 26 Nov 2016 +- New permission system +- Dropdown filter for issue labels, milestones and assignees +- Keep sidebar folding status +- Link from milestone label to the issue list + ### 4.6 - 29 Oct 2016 - Add disable option for forking - Add History button to wiki page @@ -101,7 +114,7 @@ - [List group repositories](https://developer.github.com/v3/repos/#list-organization-repositories) - Add new extension points - `assetsMapping` : Supplies resources in plugin classpath as web assets - - `suggestionProvider` : Provides suggestion in the Markdown editing textarea + - `suggestionProvider` : Provides suggestion in the Markdown editing textarea - `textDecorator` : Decorate text nodes in HTML which is converted from Markdown ### 4.2.1 - 3 Jul 2016 diff --git a/build.sbt b/build.sbt index 4accb94..5ca0854 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ val Organization = "io.github.gitbucket" val Name = "gitbucket" -val GitBucketVersion = "4.6.0" -val ScalatraVersion = "2.5.0-RC1" +val GitBucketVersion = "4.8" +val ScalatraVersion = "2.5.0" val JettyVersion = "9.3.9.v20160517" lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin) @@ -15,6 +15,7 @@ // dependency settings resolvers ++= Seq( Classpaths.typesafeReleases, + Resolver.jcenterRepo, "amateras" at "http://amateras.sourceforge.jp/mvn/", "sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/", "amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/" @@ -46,7 +47,9 @@ "com.typesafe" % "config" % "1.3.0", "com.typesafe.akka" %% "akka-actor" % "2.4.12", "fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0", - "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"), + "com.github.bkromhout" % "java-diff-utils" % "2.1.1", + "org.cache2k" % "cache2k-all" % "1.0.0.CR1", + "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"), "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "junit" % "junit" % "4.12" % "test", diff --git a/src/main/resources/update/gitbucket-core_4.7.xml b/src/main/resources/update/gitbucket-core_4.7.xml index a128800..4eb2f03 100644 --- a/src/main/resources/update/gitbucket-core_4.7.xml +++ b/src/main/resources/update/gitbucket-core_4.7.xml @@ -1,7 +1,7 @@ - + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index c04a687..3fadd2d 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -22,5 +22,7 @@ new Version("4.7.0", new LiquibaseMigration("update/gitbucket-core_4.7.xml"), new SqlMigration("update/gitbucket-core_4.7.sql") - ) + ), + new Version("4.7.1"), + new Version("4.8") ) diff --git a/src/main/scala/gitbucket/core/api/ApiPullRequest.scala b/src/main/scala/gitbucket/core/api/ApiPullRequest.scala index 52bb9a7..3ae6ac0 100644 --- a/src/main/scala/gitbucket/core/api/ApiPullRequest.scala +++ b/src/main/scala/gitbucket/core/api/ApiPullRequest.scala @@ -1,7 +1,6 @@ package gitbucket.core.api -import gitbucket.core.model.{Issue, PullRequest} - +import gitbucket.core.model.{Account, Issue, IssueComment, PullRequest} import java.util.Date @@ -15,6 +14,9 @@ head: ApiPullRequest.Commit, base: ApiPullRequest.Commit, mergeable: Option[Boolean], + merged: Boolean, + merged_at: Option[Date], + merged_by: Option[ApiUser], title: String, body: String, user: ApiUser) { @@ -31,7 +33,14 @@ } object ApiPullRequest{ - def apply(issue: Issue, pullRequest: PullRequest, headRepo: ApiRepository, baseRepo: ApiRepository, user: ApiUser): ApiPullRequest = + def apply( + issue: Issue, + pullRequest: PullRequest, + headRepo: ApiRepository, + baseRepo: ApiRepository, + user: ApiUser, + mergedComment: Option[(IssueComment, Account)] + ): ApiPullRequest = ApiPullRequest( number = issue.issueId, updated_at = issue.updatedDate, @@ -45,6 +54,9 @@ ref = pullRequest.branch, repo = baseRepo)(issue.userName), mergeable = None, // TODO: need check mergeable. + merged = mergedComment.isDefined, + merged_at = mergedComment.map { case (comment, _) => comment.registeredDate }, + merged_by = mergedComment.map { case (_, account) => ApiUser(account) }, title = issue.title, body = issue.content.getOrElse(""), user = user diff --git a/src/main/scala/gitbucket/core/api/CreateARepository.scala b/src/main/scala/gitbucket/core/api/CreateARepository.scala index 7085559..7247c9f 100644 --- a/src/main/scala/gitbucket/core/api/CreateARepository.scala +++ b/src/main/scala/gitbucket/core/api/CreateARepository.scala @@ -11,7 +11,7 @@ auto_init: Boolean = false ) { def isValid: Boolean = { - name.length<=40 && + name.length <= 100 && name.matches("[a-zA-Z0-9\\-\\+_.]+") && !name.startsWith("_") && !name.startsWith("-") diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index eddd941..d118fa4 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -22,6 +22,7 @@ with IssuesService with LabelsService with PullRequestService + with CommitsService with CommitStatusService with RepositoryCreationService with HandleCommentService @@ -102,7 +103,7 @@ defaultBranch = repository.repository.defaultBranch, origin = repository.repository.originUserName.isEmpty ).map { br => - ApiBranchForList(br.name, ApiBranchCommit(br.commitId)) + ApiBranchForList(br.name, ApiBranchCommit(br.commitId)) }) }) @@ -132,9 +133,12 @@ val largeFile = params.get("large_file").exists(s => s.equals("true")) val content = getContentFromId(git, f.id, largeFile) request.getHeader("Accept") match { - case "application/vnd.github.v3.raw" => + case "application/vnd.github.v3.raw" => { + contentType = "application/vnd.github.v3.raw" content - case "application/vnd.github.v3.html" if isRenderable(f.name) => + } + case "application/vnd.github.v3.html" if isRenderable(f.name) => { + contentType = "application/vnd.github.v3.html" content.map(c => List( "
", "
", @@ -142,7 +146,9 @@ "
", "
" ).mkString ) - case "application/vnd.github.v3.html" => + } + case "application/vnd.github.v3.html" => { + contentType = "application/vnd.github.v3.html" content.map(c => List( "
", "
", "
",
@@ -150,6 +156,7 @@
                   "
", "
", "
" ).mkString ) + } case _ => Some(JsonFormat(ApiContents(f, content))) } @@ -254,7 +261,7 @@ patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository => import gitbucket.core.api._ (for{ - branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined + branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) } yield { if(protection.enabled){ @@ -277,12 +284,25 @@ } /** + * https://developer.github.com/v3/issues/#get-a-single-issue + */ + get("/api/v3/repos/:owner/:repository/issues/:id")(referrersOnly { repository => + (for{ + issueId <- params("id").toIntOpt + issue <- getIssue(repository.owner, repository.name, issueId.toString) + openedUser <- getAccountByUserName(issue.openedUserName) + } yield { + JsonFormat(ApiIssue(issue, RepositoryName(repository), ApiUser(openedUser))) + }) getOrElse NotFound() + }) + + /** * https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue */ get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository => (for{ - issueId <- params("id").toIntOpt - comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt) + issueId <- params("id").toIntOpt + comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt) } yield { JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) }) getOrElse NotFound() @@ -363,12 +383,14 @@ updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color) JsonFormat(ApiLabel( getLabel(repository.owner, repository.name, label.labelId).get, - RepositoryName(repository))) + RepositoryName(repository) + )) } else { // TODO ApiError should support errors field to enhance compatibility of GitHub API UnprocessableEntity(ApiError( "Validation Failed", - Some("https://developer.github.com/v3/issues/labels/#create-a-label"))) + Some("https://developer.github.com/v3/issues/labels/#create-a-label") + )) } } getOrElse NotFound() } @@ -407,11 +429,12 @@ JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) => ApiPullRequest( - issue, - pullRequest, - ApiRepository(headRepo, ApiUser(headOwner)), - ApiRepository(repository, ApiUser(baseOwner)), - ApiUser(issueUser) + issue = issue, + pullRequest = pullRequest, + headRepo = ApiRepository(headRepo, ApiUser(headOwner)), + baseRepo = ApiRepository(repository, ApiUser(baseOwner)), + user = ApiUser(issueUser), + mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) ) }) }) @@ -421,20 +444,22 @@ */ get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository => (for{ - issueId <- params("id").toIntOpt + issueId <- params("id").toIntOpt (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId) - users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set()) + users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set.empty) baseOwner <- users.get(repository.owner) headOwner <- users.get(pullRequest.requestUserName) issueUser <- users.get(issue.openedUserName) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) } yield { JsonFormat(ApiPullRequest( - issue, - pullRequest, - ApiRepository(headRepo, ApiUser(headOwner)), - ApiRepository(repository, ApiUser(baseOwner)), - ApiUser(issueUser))) + issue = issue, + pullRequest = pullRequest, + headRepo = ApiRepository(headRepo, ApiUser(headOwner)), + baseRepo = ApiRepository(repository, ApiUser(baseOwner)), + user = ApiUser(issueUser), + mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) + )) }) getOrElse NotFound() }) @@ -450,7 +475,7 @@ val oldId = git.getRepository.resolve(pullreq.commitIdFrom) val newId = git.getRepository.resolve(pullreq.commitIdTo) val repoFullName = RepositoryName(repository) - val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList + val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map { c => ApiCommitListItem(new CommitInfo(c), repoFullName) }.toList JsonFormat(commits) } } @@ -469,14 +494,14 @@ */ post("/api/v3/repos/:owner/:repo/statuses/:sha")(writableUsersOnly { repository => (for{ - ref <- params.get("sha") - sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) - data <- extractFromJsonBody[CreateAStatus] if data.isValid - creator <- context.loginAccount - state <- CommitState.valueOf(data.state) - statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"), - state, data.target_url, data.description, new java.util.Date(), creator) - status <- getCommitStatus(repository.owner, repository.name, statusId) + ref <- params.get("sha") + sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) + data <- extractFromJsonBody[CreateAStatus] if data.isValid + creator <- context.loginAccount + state <- CommitState.valueOf(data.state) + statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"), + state, data.target_url, data.description, new java.util.Date(), creator) + status <- getCommitStatus(repository.owner, repository.name, statusId) } yield { JsonFormat(ApiCommitStatus(status, ApiUser(creator))) }) getOrElse NotFound() @@ -514,9 +539,9 @@ */ get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository => (for{ - ref <- params.get("ref") + ref <- params.get("ref") owner <- getAccountByUserName(repository.owner) - sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) + sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) } yield { val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha) JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner))) @@ -524,7 +549,7 @@ }) private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = - hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName } diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala index ef1a634..eeedd1c 100644 --- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -191,6 +191,7 @@ case agent if agent.contains("Win") => "windows" case _ => null } + val sidebarCollapse = request.getSession.getAttribute("sidebar-collapse") != null /** * Get object from cache. diff --git a/src/main/scala/gitbucket/core/controller/DashboardController.scala b/src/main/scala/gitbucket/core/controller/DashboardController.scala index 1227f24..81e3085 100644 --- a/src/main/scala/gitbucket/core/controller/DashboardController.scala +++ b/src/main/scala/gitbucket/core/controller/DashboardController.scala @@ -1,13 +1,13 @@ package gitbucket.core.controller import gitbucket.core.dashboard.html -import gitbucket.core.service.{RepositoryService, PullRequestService, AccountService, IssuesService} -import gitbucket.core.util.{StringUtil, Keys, UsersAuthenticator} +import gitbucket.core.service._ +import gitbucket.core.util.{Keys, UsersAuthenticator} import gitbucket.core.util.Implicits._ import gitbucket.core.service.IssuesService._ class DashboardController extends DashboardControllerBase - with IssuesService with PullRequestService with RepositoryService with AccountService + with IssuesService with PullRequestService with RepositoryService with AccountService with CommitsService with UsersAuthenticator trait DashboardControllerBase extends ControllerBase { @@ -76,7 +76,7 @@ }, filter, getGroupNames(userName), - getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true), + Nil, getUserRepositories(userName, withoutPhysicalInfo = true)) } @@ -101,7 +101,7 @@ }, filter, getGroupNames(userName), - getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true), + Nil, getUserRepositories(userName, withoutPhysicalInfo = true)) } diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 0d69ebe..6b4db67 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -5,9 +5,9 @@ import gitbucket.core.service._ import gitbucket.core.util.Implicits._ import gitbucket.core.util.ControlUtil._ -import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil} - +import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, StringUtil, UsersAuthenticator} import io.github.gitbucket.scalatra.forms._ +import org.scalatra.Ok class IndexController extends IndexControllerBase @@ -36,23 +36,11 @@ get("/"){ - val loginAccount = context.loginAccount - if(loginAccount.isEmpty) { - gitbucket.core.html.index(getRecentActivities(), - getVisibleRepositories(loginAccount, withoutPhysicalInfo = true), - loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil) - ) - } else { - val loginUserName = loginAccount.get.userName - val loginUserGroups = getGroupsByUserName(loginUserName) - var visibleOwnerSet : Set[String] = Set(loginUserName) - - visibleOwnerSet ++= loginUserGroups - - gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet), - getVisibleRepositories(loginAccount, withoutPhysicalInfo = true), - loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil) - ) + context.loginAccount.map { account => + val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName) + gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet), Nil, getUserRepositories(account.userName, withoutPhysicalInfo = true)) + }.getOrElse { + gitbucket.core.html.index(getRecentActivities(), getVisibleRepositories(None, withoutPhysicalInfo = true), Nil) } } @@ -81,6 +69,15 @@ xml.feed(getRecentActivities()) } + get("/sidebar-collapse"){ + if(params("collapse") == "true"){ + session.setAttribute("sidebar-collapse", "true") + } else { + session.setAttribute("sidebar-collapse", null) + } + Ok() + } + /** * Set account information into HttpSession and redirect. */ @@ -134,14 +131,9 @@ }) // TODO Move to RepositoryViwerController? - post("/search", searchForm){ form => - redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}") - } - - // TODO Move to RepositoryViwerController? get("/:owner/:repository/search")(referrersOnly { repository => - defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) => - val page = try { + defining(params.getOrElse("q", "").trim, params.getOrElse("type", "code")){ case (query, target) => + val page = try { val i = params.getOrElse("page", "1").toInt if(i <= 0) 1 else i } catch { @@ -150,23 +142,31 @@ target.toLowerCase match { case "issue" => gitbucket.core.search.html.issues( - countFiles(repository.owner, repository.name, query), - searchIssues(repository.owner, repository.name, query), - countWikiPages(repository.owner, repository.name, query), + if(query.nonEmpty) searchIssues(repository.owner, repository.name, query) else Nil, query, page, repository) case "wiki" => gitbucket.core.search.html.wiki( - countFiles(repository.owner, repository.name, query), - countIssues(repository.owner, repository.name, query), - searchWikiPages(repository.owner, repository.name, query), + if(query.nonEmpty) searchWikiPages(repository.owner, repository.name, query) else Nil, query, page, repository) case _ => gitbucket.core.search.html.code( - searchFiles(repository.owner, repository.name, query), - countIssues(repository.owner, repository.name, query), - countWikiPages(repository.owner, repository.name, query), + if(query.nonEmpty) searchFiles(repository.owner, repository.name, query) else Nil, query, page, repository) } } }) + + get("/search"){ + val query = params.getOrElse("query", "").trim.toLowerCase + val visibleRepositories = getVisibleRepositories(context.loginAccount, None) + val repositories = visibleRepositories.filter { repository => + repository.name.toLowerCase.indexOf(query) >= 0 || repository.owner.toLowerCase.indexOf(query) >= 0 + } + context.loginAccount.map { account => + gitbucket.core.search.html.repositories(query, repositories, Nil, getUserRepositories(account.userName, withoutPhysicalInfo = true)) + }.getOrElse { + gitbucket.core.search.html.repositories(query, repositories, visibleRepositories, Nil) + } + } + } diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index 90aca13..4222dba 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -14,12 +14,33 @@ class IssuesController extends IssuesControllerBase - with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService - with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with WebHookIssueCommentService + with IssuesService + with RepositoryService + with AccountService + with LabelsService + with MilestonesService + with ActivityService + with HandleCommentService + with ReadableUsersAuthenticator + with ReferrerAuthenticator + with WritableUsersAuthenticator + with PullRequestService + with WebHookIssueCommentService + with CommitsService trait IssuesControllerBase extends ControllerBase { - self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService - with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with WebHookIssueCommentService => + self: IssuesService + with RepositoryService + with AccountService + with LabelsService + with MilestonesService + with ActivityService + with HandleCommentService + with ReadableUsersAuthenticator + with ReferrerAuthenticator + with WritableUsersAuthenticator + with PullRequestService + with WebHookIssueCommentService => case class IssueCreateForm(title: String, content: Option[String], assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) @@ -84,7 +105,7 @@ getAssignableUserNames(owner, name), getMilestones(owner, name), getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), + isManageable(repository), repository) } } else Unauthorized() @@ -386,7 +407,7 @@ * Tests whether an logged-in user can manage issues. */ private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = { - hasWritePermission(repository.owner, repository.name, context.loginAccount) + hasDeveloperRole(repository.owner, repository.name, context.loginAccount) } /** @@ -394,8 +415,9 @@ */ private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { repository.repository.options.issuesOption match { - case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) - case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined + case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount) case "DISABLE" => false } } @@ -404,7 +426,7 @@ * Tests whether an issue or a comment is editable by a logged-in user. */ private def isEditableContent(owner: String, repository: String, author: String)(implicit context: Context): Boolean = { - hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName } } diff --git a/src/main/scala/gitbucket/core/controller/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala index ab658eb..08c0aaa 100644 --- a/src/main/scala/gitbucket/core/controller/LabelsController.scala +++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala @@ -29,7 +29,7 @@ getLabels(repository.owner, repository.name), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) + hasDeveloperRole(repository.owner, repository.name, context.loginAccount)) }) ajaxGet("/:owner/:repository/issues/labels/new")(writableUsersOnly { repository => @@ -43,7 +43,7 @@ // TODO futility countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) + hasDeveloperRole(repository.owner, repository.name, context.loginAccount)) }) ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(writableUsersOnly { repository => @@ -59,7 +59,7 @@ // TODO futility countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) + hasDeveloperRole(repository.owner, repository.name, context.loginAccount)) }) ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(writableUsersOnly { repository => diff --git a/src/main/scala/gitbucket/core/controller/MilestonesController.scala b/src/main/scala/gitbucket/core/controller/MilestonesController.scala index f75ea60..de81c73 100644 --- a/src/main/scala/gitbucket/core/controller/MilestonesController.scala +++ b/src/main/scala/gitbucket/core/controller/MilestonesController.scala @@ -27,7 +27,7 @@ params.getOrElse("state", "open"), getMilestonesWithIssueCount(repository.owner, repository.name), repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) + hasDeveloperRole(repository.owner, repository.name, context.loginAccount)) }) get("/:owner/:repository/issues/milestones/new")(writableUsersOnly { diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 548da42..671d9f1 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -11,10 +11,7 @@ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Directory._ import gitbucket.core.util.Implicits._ -import gitbucket.core.util.JGitUtil._ import gitbucket.core.util._ -import gitbucket.core.view -import gitbucket.core.view.helpers import io.github.gitbucket.scalatra.forms._ import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.PersonIdent @@ -115,7 +112,7 @@ val hasConflict = LockUtil.lock(s"${owner}/${name}"){ checkConflict(owner, name, pullreq.branch, issueId) } - val hasMergePermission = hasWritePermission(owner, name, context.loginAccount) + val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount) val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch) val mergeStatus = PullRequestService.MergeStatus( hasConflict = hasConflict, @@ -125,7 +122,7 @@ needStatusCheck = context.loginAccount.map{ u => branchProtection.needStatusCheck(u.userName) }.getOrElse(true), - hasUpdatePermission = hasWritePermission(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) && + hasUpdatePermission = hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) && context.loginAccount.map{ u => !getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName) }.getOrElse(false), @@ -156,24 +153,24 @@ } getOrElse NotFound() }) - post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository => + post("/:owner/:repository/pull/:id/update_branch")(writableUsersOnly { baseRepository => (for { issueId <- params("id").toIntOpt loginAccount <- context.loginAccount (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId) owner = pullreq.requestUserName name = pullreq.requestRepositoryName - if hasWritePermission(owner, name, context.loginAccount) + if hasDeveloperRole(owner, name, context.loginAccount) } yield { + val repository = getRepository(owner, name).get val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch) if(branchProtection.needStatusCheck(loginAccount.userName)){ flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check." } else { - val repository = getRepository(owner, name).get LockUtil.lock(s"${owner}/${name}"){ val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){ pullreq.branch - }else{ + } else { s"${pullreq.userName}:${pullreq.branch}" } val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet @@ -187,11 +184,10 @@ using(Git.open(Directory.getRepositoryDir(owner, name))) { git => // after update branch - val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}") val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList - commits.foreach{ commit => + commits.foreach { commit => if(!existIds.contains(commit.id)){ createIssueComment(owner, name, commit) } @@ -220,8 +216,9 @@ flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}" } } - redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") } + redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") + }) getOrElse NotFound() }) @@ -374,7 +371,7 @@ forkedRepository, originRepository, forkedRepository, - hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount), + hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount), getAssignableUserNames(originRepository.owner, originRepository.name), getMilestones(originRepository.owner, originRepository.name), getLabels(originRepository.owner, originRepository.name) @@ -389,7 +386,7 @@ }) getOrElse NotFound() }) - ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(writableUsersOnly { forkedRepository => + ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository => val Seq(origin, forked) = multiParams("splat") val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner) @@ -498,26 +495,6 @@ (defaultOwner, value) } - private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, - requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = - using( - Git.open(getRepositoryDir(userName, repositoryName)), - Git.open(getRepositoryDir(requestUserName, requestRepositoryName)) - ){ (oldGit, newGit) => - val oldId = oldGit.getRepository.resolve(branch) - val newId = newGit.getRepository.resolve(requestCommitId) - - val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => - new CommitInfo(revCommit) - }.toList.splitWith { (commit1, commit2) => - helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) - } - - val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) - - (commits, diffs) - } - private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = defining(repository.owner, repository.name){ case (owner, repoName) => val page = IssueSearchCondition.page(request) @@ -544,7 +521,7 @@ * Tests whether an logged-in user can manage pull requests. */ private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = { - hasWritePermission(repository.owner, repository.name, context.loginAccount) + hasDeveloperRole(repository.owner, repository.name, context.loginAccount) } /** @@ -552,8 +529,9 @@ */ private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { repository.repository.options.issuesOption match { - case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) - case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined + case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount) case "DISABLE" => false } } diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 5a2ca91..b0b30f5 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -39,7 +39,7 @@ ) val optionsForm = mapping( - "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(40), identifier, renameRepositoryName))), + "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), identifier, renameRepositoryName))), "description" -> trim(label("Description" , optional(text()))), "isPrivate" -> trim(label("Repository Type" , boolean())), "issuesOption" -> trim(label("Issues Option" , text(required, featureOption))), @@ -179,8 +179,8 @@ val collaborators = params("collaborators") removeCollaborators(repository.owner, repository.name) collaborators.split(",").withFilter(_.nonEmpty).map { collaborator => - val userName :: permission :: Nil = collaborator.split(":").toList - addCollaborator(repository.owner, repository.name, userName, permission) + val userName :: role :: Nil = collaborator.split(":").toList + addCollaborator(repository.owner, repository.name, userName, role) } redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") }) @@ -238,7 +238,7 @@ val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, ctype, token) val dummyPayload = { val ownerAccount = getAccountByUserName(repository.owner).get - val commits = if(repository.commitCount == 0) List.empty else git.log + val commits = if(JGitUtil.isEmpty(git)) List.empty else git.log .add(git.getRepository.resolve(repository.repository.defaultBranch)) .setMaxCount(4) .call.iterator.asScala.map(new CommitInfo(_)).toList @@ -416,7 +416,7 @@ */ private def featureOption: Constraint = new Constraint(){ override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - if(Seq("DISABLE", "PRIVATE", "PUBLIC").contains(value)) None else Some("Option is invalid.") + if(Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.") } diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index ad8b091..d08c0fe 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -102,16 +102,28 @@ */ post("/:owner/:repository/_preview")(referrersOnly { repository => contentType = "text/html" - helpers.markdown( - markdown = params("content"), - repository = repository, - enableWikiLink = params("enableWikiLink").toBoolean, - enableRefsLink = params("enableRefsLink").toBoolean, - enableLineBreaks = params("enableLineBreaks").toBoolean, - enableTaskList = params("enableTaskList").toBoolean, - enableAnchor = false, - hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount) - ) + val filename = params.get("filename") + filename match { + case Some(f) => helpers.renderMarkup( + filePath = List(f), + fileContent = params("content"), + branch = "master", + repository = repository, + enableWikiLink = params("enableWikiLink").toBoolean, + enableRefsLink = params("enableRefsLink").toBoolean, + enableAnchor = false + ) + case None => helpers.markdown( + markdown = params("content"), + repository = repository, + enableWikiLink = params("enableWikiLink").toBoolean, + enableRefsLink = params("enableRefsLink").toBoolean, + enableLineBreaks = params("enableLineBreaks").toBoolean, + enableTaskList = params("enableTaskList").toBoolean, + enableAnchor = false, + hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount) + ) + } }) /** @@ -151,7 +163,7 @@ html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, logs.splitWith{ (commit1, commit2) => view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) - }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + }, page, hasNext, hasDeveloperRole(repository.owner, repository.name, context.loginAccount)) case Left(_) => NotFound() } } @@ -275,7 +287,7 @@ html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)), - hasWritePermission(repository.owner, repository.name, context.loginAccount), + hasDeveloperRole(repository.owner, repository.name, context.loginAccount), request.paths(2) == "blame") } } getOrElse NotFound() @@ -328,8 +340,8 @@ 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)) + getCommitComments(repository.owner, repository.name, id, true), + repository, diffs, oldCommitId, hasDeveloperRole(repository.owner, repository.name, context.loginAccount)) } } } @@ -358,7 +370,7 @@ html.commentform( commitId = id, fileName, oldLineNumber, newLineNumber, issueId, - hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount), + hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository = repository ) }) @@ -374,7 +386,7 @@ callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get) case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content) } - helper.html.commitcomment(comment, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) + helper.html.commitcomment(comment, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository) }) ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository => @@ -393,7 +405,7 @@ enableRefsLink = true, enableAnchor = true, enableLineBreaks = true, - hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName) + hasWritePermission = true ) )) } @@ -437,7 +449,7 @@ .map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name))) .reverse - html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) + html.branches(branches, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository) }) /** @@ -546,10 +558,10 @@ * @return HTML of the file list */ private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = { - if(repository.commitCount == 0){ - html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) - } else { - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + if(JGitUtil.isEmpty(git)){ + html.guide(repository, hasDeveloperRole(repository.owner, repository.name, context.loginAccount)) + } else { // get specified commit JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => @@ -569,9 +581,14 @@ html.files(revision, repository, if(path == ".") Nil else path.split("/").toList, // current path new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit - files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount), + JGitUtil.getCommitCount(repository.owner, repository.name, revision), + files, + readme, + hasDeveloperRole(repository.owner, repository.name, context.loginAccount), getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch), - flash.get("info"), flash.get("error")) + flash.get("info"), + flash.get("error") + ) } } getOrElse NotFound() } @@ -691,7 +708,7 @@ } private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = - hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = { e.printStackTrace() diff --git a/src/main/scala/gitbucket/core/controller/WikiController.scala b/src/main/scala/gitbucket/core/controller/WikiController.scala index 64d0b27..f46b4be 100644 --- a/src/main/scala/gitbucket/core/controller/WikiController.scala +++ b/src/main/scala/gitbucket/core/controller/WikiController.scala @@ -242,9 +242,9 @@ private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { repository.repository.options.wikiOption match { -// case "ALL" => repository.repository.isPrivate == false || hasReadPermission(repository.owner, repository.name, context.loginAccount) - case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) - case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined + case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount) case "DISABLE" => false } } diff --git a/src/main/scala/gitbucket/core/model/Collaborator.scala b/src/main/scala/gitbucket/core/model/Collaborator.scala index 0838165..3cee46b 100644 --- a/src/main/scala/gitbucket/core/model/Collaborator.scala +++ b/src/main/scala/gitbucket/core/model/Collaborator.scala @@ -7,8 +7,8 @@ class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate { val collaboratorName = column[String]("COLLABORATOR_NAME") - val permission = column[String]("PERMISSION") - def * = (userName, repositoryName, collaboratorName, permission) <> (Collaborator.tupled, Collaborator.unapply) + val role = column[String]("ROLE") + def * = (userName, repositoryName, collaboratorName, role) <> (Collaborator.tupled, Collaborator.unapply) def byPrimaryKey(owner: String, repository: String, collaborator: String) = byRepository(owner, repository) && (collaboratorName === collaborator.bind) @@ -19,15 +19,15 @@ userName: String, repositoryName: String, collaboratorName: String, - permission: String + role: String ) -sealed abstract class Permission(val name: String) +sealed abstract class Role(val name: String) -object Permission { - object ADMIN extends Permission("ADMIN") - object WRITE extends Permission("WRITE") - object READ extends Permission("READ") +object Role { + object ADMIN extends Role("ADMIN") + object DEVELOPER extends Role("DEVELOPER") + object GUEST extends Role("GUEST") // val values: Vector[Permission] = Vector(ADMIN, WRITE, READ) // diff --git a/src/main/scala/gitbucket/core/plugin/SuggestionProvider.scala b/src/main/scala/gitbucket/core/plugin/SuggestionProvider.scala index 95b9bbc..3aafa11 100644 --- a/src/main/scala/gitbucket/core/plugin/SuggestionProvider.scala +++ b/src/main/scala/gitbucket/core/plugin/SuggestionProvider.scala @@ -23,5 +23,5 @@ override def values(repository: RepositoryInfo): Seq[String] = Nil override def template(implicit context: Context): String = "'@' + value" override def additionalScript(implicit context: Context): String = - s"""$$.get('${context.path}/_user/proposals', { query: '' }, function (data) { user = data.options; });""" + s"""$$.get('${context.path}/_user/proposals', { query: '', user: true, group: false }, function (data) { user = data.options; });""" } \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/service/CommitsService.scala b/src/main/scala/gitbucket/core/service/CommitsService.scala index b9aaea0..747f3d2 100644 --- a/src/main/scala/gitbucket/core/service/CommitsService.scala +++ b/src/main/scala/gitbucket/core/service/CommitsService.scala @@ -37,6 +37,12 @@ updatedDate = currentDate, issueId = issueId) + def updateCommitCommentPosition(commentId: Int, commitId: String, oldLine: Option[Int], newLine: Option[Int])(implicit s: Session): Unit = + CommitComments.filter(_.byPrimaryKey(commentId)) + .map { t => + (t.commitId, t.oldLine, t.newLine) + }.update(commitId, oldLine, newLine) + def updateCommitComment(commentId: Int, content: String)(implicit s: Session) = { CommitComments .filter (_.byPrimaryKey(commentId)) diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 69d6f23..97bbc6c 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -10,6 +10,7 @@ import gitbucket.core.model.Profile.dateColumnType + trait IssuesService { self: AccountService with RepositoryService => import IssuesService._ @@ -31,6 +32,10 @@ .map{ case ((t1, t2), t3) => (t1, t2, t3) } .list + def getMergedComment(owner: String, repository: String, issueId: Int)(implicit s: Session): Option[(IssueComment, Account)] = { + getCommentsForApi(owner, repository, issueId).collectFirst { case (comment, account, _) if comment.action == "merged" => (comment, account) } + } + def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) = if (commentId forall (_.isDigit)) IssueComments filter { t => @@ -388,8 +393,8 @@ } def getAssignableUserNames(owner: String, repository: String)(implicit s: Session): List[String] = { - (getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE)) ::: - (if (getAccountByUserName(owner).get.isGroupAccount) getGroupMembers(owner).map(_.userName) else List(owner))).sorted + (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)) ::: + (if (getAccountByUserName(owner).get.isGroupAccount) getGroupMembers(owner).map(_.userName) else List(owner))).distinct.sorted } } diff --git a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala index 3552762..6b9e98d 100644 --- a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala +++ b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala @@ -87,7 +87,7 @@ def getStopReason(isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = { if(enabled){ command.getType() match { - case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards => + case ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards => Some("Cannot force-push to a protected branch") case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) => unSuccessedContexts(command.getNewId.name) match { @@ -99,7 +99,7 @@ Some("Cannot delete a protected branch") case _ => None } - }else{ + } else { None } } diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala index e5b3e23..d9acb3c 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -1,12 +1,24 @@ package gitbucket.core.service import gitbucket.core.model.{Issue, PullRequest, CommitStatus, CommitState} -import gitbucket.core.util.JGitUtil import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile._ import gitbucket.core.model.Profile.profile.blockingApi._ +import difflib.{Delta, DiffUtils} +import gitbucket.core.model.{Session => _, _} +import gitbucket.core.model.Profile._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.JGitUtil +import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo} +import gitbucket.core.view +import gitbucket.core.view.helpers +import org.eclipse.jgit.api.Git +import scala.collection.JavaConverters._ -trait PullRequestService { self: IssuesService => + +trait PullRequestService { self: IssuesService with CommitsService => import PullRequestService._ def getPullRequest(owner: String, repository: String, issueId: Int) @@ -111,10 +123,26 @@ def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit = getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){ - //if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){ + // Update the git repository val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest( pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.issueId, pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch) + + // Collect comment positions + val positions = getCommitComments(pullreq.userName, pullreq.repositoryName, pullreq.commitIdTo, true) + .collect { + case CommitComment(_, _, _, commentId, _, _, Some(file), None, Some(newLine), _, _, _) => (file, commentId, Right(newLine)) + case CommitComment(_, _, _, commentId, _, _, Some(file), Some(oldLine), None, _, _, _) => (file, commentId, Left(oldLine)) + } + .groupBy { case (file, _, _) => file } + .map { case (file, comments) => file -> + comments.map { case (_, commentId, lineNumber) => (commentId, lineNumber) } + } + + // Update comments position + updatePullRequestCommentPositions(positions, pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo, commitIdTo) + + // Update commit id in the PULL_REQUEST table updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom) } } @@ -138,6 +166,78 @@ .firstOption } } + + private def updatePullRequestCommentPositions(positions: Map[String, Seq[(Int, Either[Int, Int])]], userName: String, repositoryName: String, + oldCommitId: String, newCommitId: String)(implicit s: Session): Unit = { + + val (_, diffs) = getRequestCompareInfo(userName, repositoryName, oldCommitId, userName, repositoryName, newCommitId) + + val patchs = positions.map { case (file, _) => + diffs.find(x => x.oldPath == file).map { diff => + (diff.oldContent, diff.newContent) match { + case (Some(oldContent), Some(newContent)) => { + val oldLines = oldContent.replace("\r\n", "\n").split("\n") + val newLines = newContent.replace("\r\n", "\n").split("\n") + file -> Option(DiffUtils.diff(oldLines.toList.asJava, newLines.toList.asJava)) + } + case _ => + file -> None + } + }.getOrElse { + file -> None + } + } + + positions.foreach { case (file, comments) => + patchs(file) match { + case Some(patch) => file -> comments.foreach { case (commentId, lineNumber) => lineNumber match { + case Left(oldLine) => updateCommitCommentPosition(commentId, newCommitId, Some(oldLine), None) + case Right(newLine) => + var counter = newLine + patch.getDeltas.asScala.filter(_.getOriginal.getPosition < newLine).foreach { delta => + delta.getType match { + case Delta.TYPE.CHANGE => + if(delta.getOriginal.getPosition <= newLine - 1 && newLine <= delta.getOriginal.getPosition + delta.getRevised.getLines.size){ + counter = -1 + } else { + counter = counter + (delta.getRevised.getLines.size - delta.getOriginal.getLines.size) + } + case Delta.TYPE.INSERT => counter = counter + delta.getRevised.getLines.size + case Delta.TYPE.DELETE => counter = counter - delta.getOriginal.getLines.size + } + } + if(counter >= 0){ + updateCommitCommentPosition(commentId, newCommitId, None, Some(counter)) + } + }} + case _ => comments.foreach { case (commentId, lineNumber) => lineNumber match { + case Right(oldLine) => updateCommitCommentPosition(commentId, newCommitId, Some(oldLine), None) + case Left(newLine) => updateCommitCommentPosition(commentId, newCommitId, None, Some(newLine)) + }} + } + } + } + + def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = + using( + Git.open(getRepositoryDir(userName, repositoryName)), + Git.open(getRepositoryDir(requestUserName, requestRepositoryName)) + ){ (oldGit, newGit) => + val oldId = oldGit.getRepository.resolve(branch) + val newId = newGit.getRepository.resolve(requestCommitId) + + val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => + new CommitInfo(revCommit) + }.toList.splitWith { (commit1, commit2) => + helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) + } + + val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) + + (commits, diffs) + } + } object PullRequestService { diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 1b1fcad..dfb0f71 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -2,7 +2,7 @@ import gitbucket.core.controller.Context import gitbucket.core.util.JGitUtil -import gitbucket.core.model.{Collaborator, Repository, RepositoryOptions, Account, Permission} +import gitbucket.core.model.{Collaborator, Repository, RepositoryOptions, Account, Role} import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile._ import gitbucket.core.model.Profile.profile.blockingApi._ @@ -230,7 +230,7 @@ } /** - * Returns the repositories without private repository that user does not have access right. + * Returns the repositories except private repository that user does not have access right. * Include public repository, private own repository and private but collaborator repository. * * @param userName the user name of collaborator @@ -239,8 +239,10 @@ def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = { Repositories.filter { t1 => (t1.isPrivate === false.bind) || - (t1.userName === userName.bind) || - (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists) + (t1.userName === userName.bind) || (t1.userName in (GroupMembers.filter(_.userName === userName.bind).map(_.groupName))) || + (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && + ((t2.collaboratorName === userName.bind) || (t2.collaboratorName in GroupMembers.filter(_.userName === userName.bind).map(_.groupName))) + } exists) }.sortBy(_.lastActivityDate desc).map{ t => (t.userName, t.repositoryName) }.list @@ -248,8 +250,10 @@ def getUserRepositories(userName: String, withoutPhysicalInfo: Boolean = false)(implicit s: Session): List[RepositoryInfo] = { Repositories.filter { t1 => - (t1.userName === userName.bind) || - (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists) + (t1.userName === userName.bind) || (t1.userName in (GroupMembers.filter(_.userName === userName.bind).map(_.groupName))) || + (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && + ((t2.collaboratorName === userName.bind) || (t2.collaboratorName in GroupMembers.filter(_.userName === userName.bind).map(_.groupName))) + } exists) }.sortBy(_.lastActivityDate desc).list.map{ repository => new RepositoryInfo( if(withoutPhysicalInfo){ @@ -284,8 +288,13 @@ case Some(x) if(x.isAdmin) => Repositories // for Normal Users case Some(x) if(!x.isAdmin) => - Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) || - (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists) + Repositories filter { t => + (t.isPrivate === false.bind) || (t.userName === x.userName) || + (t.userName in GroupMembers.filter(_.userName === x.userName.bind).map(_.groupName)) || + (Collaborators.filter { t2 => + t2.byRepository(t.userName, t.repositoryName) && + ((t2.collaboratorName === x.userName.bind) || (t2.collaboratorName in GroupMembers.filter(_.userName === x.userName.bind).map(_.groupName))) + } exists) } // for Guests case None => Repositories filter(_.isPrivate === false.bind) @@ -342,8 +351,8 @@ /** * Add collaborator (user or group) to the repository. */ - def addCollaborator(userName: String, repositoryName: String, collaboratorName: String, permission: String)(implicit s: Session): Unit = - Collaborators insert Collaborator(userName, repositoryName, collaboratorName, permission) + def addCollaborator(userName: String, repositoryName: String, collaboratorName: String, role: String)(implicit s: Session): Unit = + Collaborators insert Collaborator(userName, repositoryName, collaboratorName, role) /** * Remove all collaborators from the repository. @@ -366,38 +375,38 @@ * Returns the list of all collaborator name and permission which is sorted with ascending order. * If a group is added as a collaborator, this method returns users who are belong to that group. */ - def getCollaboratorUserNames(userName: String, repositoryName: String, filter: Seq[Permission] = Nil)(implicit s: Session): List[String] = { + def getCollaboratorUserNames(userName: String, repositoryName: String, filter: Seq[Role] = Nil)(implicit s: Session): List[String] = { val q1 = Collaborators .join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === false.bind) } .filter { case (t1, t2) => t1.byRepository(userName, repositoryName) } - .map { case (t1, t2) => (t1.collaboratorName, t1.permission) } + .map { case (t1, t2) => (t1.collaboratorName, t1.role) } val q2 = Collaborators .join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === true.bind) } .join(GroupMembers).on { case ((t1, t2), t3) => t2.userName === t3.groupName } .filter { case ((t1, t2), t3) => t1.byRepository(userName, repositoryName) } - .map { case ((t1, t2), t3) => (t3.userName, t1.permission) } + .map { case ((t1, t2), t3) => (t3.userName, t1.role) } q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1) } - def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { + def hasDeveloperRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { loginAccount match { case Some(a) if(a.isAdmin) => true case Some(a) if(a.userName == owner) => true case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true - case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE)).contains(a.userName)) => true + case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName)) => true case _ => false } } - def hasReadPermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { + def hasGuestRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { loginAccount match { case Some(a) if(a.isAdmin) => true case Some(a) if(a.userName == owner) => true case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true - case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE, Permission.READ)).contains(a.userName)) => true + case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST)).contains(a.userName)) => true case _ => false } } @@ -419,26 +428,20 @@ object RepositoryService { case class RepositoryInfo(owner: String, name: String, repository: Repository, - issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, + issueCount: Int, pullCount: Int, forkedCount: Int, branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) { /** * Creates instance with issue count and pull request count. */ def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = - this( - repo.owner, repo.name, model, - issueCount, pullCount, repo.commitCount, forkedCount, - repo.branchList, repo.tags, managers) + this(repo.owner, repo.name, model, issueCount, pullCount, forkedCount, repo.branchList, repo.tags, managers) /** * Creates instance without issue count and pull request count. */ def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = - this( - repo.owner, repo.name, model, - 0, 0, repo.commitCount, forkedCount, - repo.branchList, repo.tags, managers) + this(repo.owner, repo.name, model, 0, 0, forkedCount, repo.branchList, repo.tags, managers) def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name) def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name) @@ -452,7 +455,6 @@ (id, path.substring(id.length).stripPrefix("/")) } - } def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git" diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index d8ca928..1083868 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -1,9 +1,11 @@ package gitbucket.core.service +import java.util.Date + import fr.brouillard.oss.security.xhub.XHub -import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter} +import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest} import gitbucket.core.api._ -import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment} +import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, WebHookEvent} import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile._ import gitbucket.core.model.Profile.profile.blockingApi._ @@ -17,6 +19,7 @@ import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.ObjectId import org.slf4j.LoggerFactory + import scala.concurrent._ import scala.util.{Success, Failure} import org.apache.http.HttpRequest @@ -35,14 +38,14 @@ def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(WebHook, Set[WebHook.Event])] = WebHooks.filter(_.byRepository(owner, repository)) .join(WebHookEvents).on { (w, t) => t.byWebHook(w) } - .map{ case (w,t) => w -> t.event } + .map { case (w, t) => w -> t.event } .list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url) /** get All WebHook informations of repository event */ def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = WebHooks.filter(_.byRepository(owner, repository)) .join(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) } - .filter{ case (wh, whe) => whe.event === event.bind} + .filter { case (wh, whe) => whe.event === event.bind} .map{ case (wh, whe) => wh } .list.distinct @@ -51,12 +54,12 @@ WebHooks .filter(_.byPrimaryKey(owner, repository, url)) .join(WebHookEvents).on { (w, t) => t.byWebHook(w) } - .map{ case (w,t) => w -> t.event } + .map { case (w, t) => w -> t.event } .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption - def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { + def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { WebHooks insert WebHook(owner, repository, url, ctype, token) - events.toSet.map { event: WebHook.Event => + events.map { event: WebHook.Event => WebHookEvents insert WebHookEvent(owner, repository, url, event) } } @@ -64,7 +67,7 @@ def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token)) WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete - events.toSet.map { event: WebHook.Event => + events.map { event: WebHook.Event => WebHookEvents insert WebHookEvent(owner, repository, url, event) } } @@ -83,7 +86,7 @@ def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload) (implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = { import org.apache.http.impl.client.HttpClientBuilder - import ExecutionContext.Implicits.global + import ExecutionContext.Implicits.global // TODO Shouldn't use the default execution context import org.apache.http.protocol.HttpContext import org.apache.http.client.methods.HttpPost @@ -93,7 +96,7 @@ webHooks.map { webHook => val reqPromise = Promise[HttpRequest] val f = Future { - val itcp = new org.apache.http.HttpRequestInterceptor{ + val itcp = new org.apache.http.HttpRequestInterceptor { def process(res: HttpRequest, ctx: HttpContext): Unit = { reqPromise.success(res) } @@ -132,7 +135,7 @@ logger.debug(s"end web hook invocation for ${webHook}") res } catch { - case e:Throwable => { + case e: Throwable => { if(!reqPromise.isCompleted){ reqPromise.failure(e) } @@ -168,11 +171,11 @@ issueUser <- users.get(issue.openedUserName) } yield { WebHookIssuesPayload( - action = action, - number = issue.issueId, - repository = ApiRepository(repository, ApiUser(repoOwner)), - issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)), - sender = ApiUser(sender)) + action = action, + number = issue.issueId, + repository = ApiRepository(repository, ApiUser(repoOwner)), + issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)), + sender = ApiUser(sender)) } } } @@ -198,7 +201,9 @@ headOwner = headOwner, baseRepository = repository, baseOwner = baseOwner, - sender = sender) + sender = sender, + mergedComment = getMergedComment(repository.owner, repository.name, issueId) + ) } } } @@ -237,7 +242,10 @@ headOwner = headOwner, baseRepository = baseRepo, baseOwner = baseOwner, - sender = sender) + sender = sender, + mergedComment = getMergedComment(baseRepo.owner, baseRepo.name, issue.issueId) + ) + callWebHook(WebHook.PullRequest, webHooks, payload) } } @@ -267,7 +275,9 @@ headOwner = headOwner, baseRepository = repository, baseOwner = baseOwner, - sender = sender) + sender = sender, + mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) + ) } } } @@ -365,11 +375,21 @@ headOwner: Account, baseRepository: RepositoryInfo, baseOwner: Account, - sender: Account): WebHookPullRequestPayload = { + sender: Account, + mergedComment: Option[(IssueComment, Account)]): WebHookPullRequestPayload = { + val headRepoPayload = ApiRepository(headRepository, headOwner) val baseRepoPayload = ApiRepository(baseRepository, baseOwner) val senderPayload = ApiUser(sender) - val pr = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, ApiUser(issueUser)) + val pr = ApiPullRequest( + issue = issue, + pullRequest = pullRequest, + headRepo = headRepoPayload, + baseRepo = baseRepoPayload, + user = ApiUser(issueUser), + mergedComment = mergedComment + ) + WebHookPullRequestPayload( action = action, number = issue.issueId, @@ -389,7 +409,7 @@ sender: ApiUser ) extends WebHookPayload - object WebHookIssueCommentPayload{ + object WebHookIssueCommentPayload { def apply( issue: Issue, issueUser: Account, @@ -415,28 +435,42 @@ sender: ApiUser ) extends WebHookPayload - object WebHookPullRequestReviewCommentPayload{ + object WebHookPullRequestReviewCommentPayload { def apply( - action: String, - comment: CommitComment, - issue: Issue, - issueUser: Account, - pullRequest: PullRequest, - headRepository: RepositoryInfo, - headOwner: Account, - baseRepository: RepositoryInfo, - baseOwner: Account, - sender: Account - ) : WebHookPullRequestReviewCommentPayload = { + action: String, + comment: CommitComment, + issue: Issue, + issueUser: Account, + pullRequest: PullRequest, + headRepository: RepositoryInfo, + headOwner: Account, + baseRepository: RepositoryInfo, + baseOwner: Account, + sender: Account, + mergedComment: Option[(IssueComment, Account)] + ) : WebHookPullRequestReviewCommentPayload = { val headRepoPayload = ApiRepository(headRepository, headOwner) val baseRepoPayload = ApiRepository(baseRepository, baseOwner) val senderPayload = ApiUser(sender) + WebHookPullRequestReviewCommentPayload( - action = action, - comment = ApiPullRequestReviewComment(comment, senderPayload, RepositoryName(baseRepository), issue.issueId), - pull_request = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, ApiUser(issueUser)), - repository = baseRepoPayload, - sender = senderPayload) + action = action, + comment = ApiPullRequestReviewComment( + comment = comment, + commentedUser = senderPayload, + repositoryName = RepositoryName(baseRepository), + issueId = issue.issueId + ), + pull_request = ApiPullRequest( + issue = issue, + pullRequest = pullRequest, + headRepo = headRepoPayload, + baseRepo = baseRepoPayload, + user = ApiUser(issueUser), + mergedComment = mergedComment + ), + repository = baseRepoPayload, + sender = senderPayload) } } } diff --git a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala index c079eb8..d0b92bd 100644 --- a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala +++ b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala @@ -84,7 +84,7 @@ Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2) account <- authenticate(settings, username, password) } yield if(isUpdating || repository.repository.isPrivate){ - if(hasWritePermission(repository.owner, repository.name, Some(account))){ + if(hasDeveloperRole(repository.owner, repository.name, Some(account))){ request.setAttribute(Keys.Request.UserName, account.userName) true } else false diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index 1ceb76a..e85804e 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -110,7 +110,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session) extends PostReceiveHook with PreReceiveHook with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService - with WebHookPullRequestService with ProtectedBranchService { + with WebHookPullRequestService with CommitsService { private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private var existIds: Seq[String] = Nil @@ -139,6 +139,8 @@ def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { try { using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => + JGitUtil.removeCache(git) + val pushedIds = scala.collection.mutable.Set[String]() commands.asScala.foreach { command => logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index c7b4277..8897eb6 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -93,7 +93,7 @@ protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo) (implicit session: Session): Boolean = getAccountByUserName(username) match { - case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account)) + case Some(account) => hasDeveloperRole(repositoryInfo.owner, repositoryInfo.name, Some(account)) case None => false } diff --git a/src/main/scala/gitbucket/core/util/Authenticator.scala b/src/main/scala/gitbucket/core/util/Authenticator.scala index b4cfef5..bbcb339 100644 --- a/src/main/scala/gitbucket/core/util/Authenticator.scala +++ b/src/main/scala/gitbucket/core/util/Authenticator.scala @@ -2,13 +2,11 @@ import gitbucket.core.controller.ControllerBase import gitbucket.core.service.{AccountService, RepositoryService} -import gitbucket.core.model.Permission +import gitbucket.core.model.Role import RepositoryService.RepositoryInfo import Implicits._ import ControlUtil._ -import scala.collection.Searching.search - /** * Allows only oneself and administrators. */ @@ -45,7 +43,7 @@ case Some(x) if(repository.owner == x.userName) => action(repository) // TODO Repository management is allowed for only group managers? case Some(x) if(getGroupMembers(repository.owner).exists { m => m.userName == x.userName && m.isManager == true }) => action(repository) - case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Permission.ADMIN)).contains(x.userName)) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Role.ADMIN)).contains(x.userName)) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() @@ -156,7 +154,7 @@ case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository) case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository) - case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Permission.ADMIN, Permission.WRITE)).contains(x.userName)) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Role.ADMIN, Role.DEVELOPER)).contains(x.userName)) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() diff --git a/src/main/scala/gitbucket/core/util/JDBCUtil.scala b/src/main/scala/gitbucket/core/util/JDBCUtil.scala index 34bee29..edc41ec 100644 --- a/src/main/scala/gitbucket/core/util/JDBCUtil.scala +++ b/src/main/scala/gitbucket/core/util/JDBCUtil.scala @@ -9,7 +9,10 @@ /** * Provides implicit class which extends java.sql.Connection. - * This is used in automatic migration in [[servlet.AutoUpdateListener]]. + * This is used in following points: + * + * - Automatic migration in [[gitbucket.core.servlet.InitializeListener]] + * - Data importing / exporting in [[gitbucket.core.controller.SystemSettingsController]] and [[gitbucket.core.controller.FileUploadController]] */ object JDBCUtil { @@ -71,8 +74,6 @@ val bytes = new scala.Array[Byte](1024 * 8) var stringLiteral = false - var count = 0 - while({ length = in.read(bytes); length != -1 }){ for(i <- 0 to length - 1){ val c = bytes(i) @@ -81,13 +82,19 @@ } if(c == ';' && !stringLiteral){ val sql = new String(out.toByteArray, "UTF-8") - conn.update(sql) + conn.update(sql.trim) out = new ByteArrayOutputStream() } else { out.write(c) } } } + + val remain = out.toByteArray + if(remain.length != 0){ + val sql = new String(remain, "UTF-8") + conn.update(sql.trim) + } } conn.commit() diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index 7c5f687..786edca 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -5,6 +5,7 @@ import Directory._ import StringUtil._ import ControlUtil._ + import scala.annotation.tailrec import scala.collection.JavaConverters._ import org.eclipse.jgit.lib._ @@ -16,7 +17,11 @@ import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} import org.eclipse.jgit.transport.RefSpec import java.util.Date -import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException} +import java.util.concurrent.TimeUnit +import java.util.function.Consumer + +import org.cache2k.{Cache2kBuilder, CacheEntry} +import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException} import org.eclipse.jgit.dircache.DirCacheEntry import org.slf4j.LoggerFactory @@ -32,14 +37,11 @@ * * @param owner the user name of the repository owner * @param name the repository name - * @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001. * @param branchList the list of branch names * @param tags the list of tags */ - case class RepositoryInfo(owner: String, name: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){ - def this(owner: String, name: String) = { - this(owner, name, 0, Nil, Nil) - } + case class RepositoryInfo(owner: String, name: String, branchList: List[String], tags: List[TagInfo]){ + def this(owner: String, name: String) = this(owner, name, Nil, Nil) } /** @@ -169,20 +171,54 @@ revWalk.dispose revCommit } - + + private val cache = new Cache2kBuilder[String, Int]() {} + .name("commit-count") + .expireAfterWrite(24, TimeUnit.HOURS) + .entryCapacity(10000) + .build() + + def removeCache(git: Git): Unit = { + val dir = git.getRepository.getDirectory + val keyPrefix = dir.getAbsolutePath + "@" + + cache.forEach(new Consumer[CacheEntry[String, Int]] { + override def accept(entry: CacheEntry[String, Int]): Unit = { + if(entry.getKey.startsWith(keyPrefix)){ + cache.remove(entry.getKey) + } + } + }) + } + + /** + * Returns the number of commits in the specified branch or commit. + * If the specified branch has over 10000 commits, this method returns 100001. + */ + def getCommitCount(owner: String, repository: String, branch: String): Int = { + val dir = getRepositoryDir(owner, repository) + val key = dir.getAbsolutePath + "@" + branch + val entry = cache.getEntry(key) + + if(entry == null) { + using(Git.open(dir)) { git => + val commitId = git.getRepository.resolve(branch) + val commitCount = git.log.add(commitId).call.iterator.asScala.take(10001).size + cache.put(key, commitCount) + commitCount + } + } else { + entry.getValue + } + } + /** * Returns the repository information. It contains branch names and tag names. */ def getRepositoryInfo(owner: String, repository: String): RepositoryInfo = { using(Git.open(getRepositoryDir(owner, repository))){ git => try { - // get commit count - val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum - - RepositoryInfo( - owner, repository, - // commit count - commitCount, + RepositoryInfo(owner, repository, // branches git.branchList.call.asScala.map { ref => ref.getName.stripPrefix("refs/heads/") @@ -195,9 +231,7 @@ ) } catch { // not initialized - case e: NoHeadException => RepositoryInfo( - owner, repository, 0, Nil, Nil) - + case e: NoHeadException => RepositoryInfo(owner, repository, Nil, Nil) } } } @@ -212,8 +246,8 @@ */ def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { using(new RevWalk(git.getRepository)){ revWalk => - val objectId = git.getRepository.resolve(revision) - if(objectId==null) return Nil + val objectId = git.getRepository.resolve(revision) + if(objectId == null) return Nil val revCommit = revWalk.parseCommit(objectId) def useTreeWalk(rev:RevCommit)(f:TreeWalk => Any): Unit = if (path == ".") { @@ -255,14 +289,14 @@ revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, Option[String], RevCommit)] ={ if(restList.isEmpty){ result - }else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty - result ++ restList.map{ case (tuple, map) => tupleAdd(tuple, map.values.headOption.getOrElse(revCommit)) } - }else{ + } else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty + result ++ restList.map { case (tuple, map) => tupleAdd(tuple, map.values.headOption.getOrElse(revCommit)) } + } else { val newCommit = revIterator.next - val (thisTimeChecks,skips) = restList.partition{ case (tuple, parentsMap) => parentsMap.contains(newCommit) } + val (thisTimeChecks,skips) = restList.partition { case (tuple, parentsMap) => parentsMap.contains(newCommit) } if(thisTimeChecks.isEmpty){ findLastCommits(result, restList, revIterator) - }else{ + } else { var nextRest = skips var nextResult = result // Map[(name, oid), (tuple, parentsMap)] @@ -270,20 +304,20 @@ lazy val newParentsMap = newCommit.getParents.map(_ -> newCommit).toMap useTreeWalk(newCommit){ walk => while(walk.next){ - rest.remove(walk.getNameString -> walk.getObjectId(0)).map{ case (tuple, _) => + rest.remove(walk.getNameString -> walk.getObjectId(0)).map { case (tuple, _) => if(newParentsMap.isEmpty){ nextResult +:= tupleAdd(tuple, newCommit) - }else{ + } else { nextRest +:= tuple -> newParentsMap } } } } - rest.values.map{ case (tuple, parentsMap) => + rest.values.map { case (tuple, parentsMap) => val restParentsMap = parentsMap - newCommit if(restParentsMap.isEmpty){ nextResult +:= tupleAdd(tuple, parentsMap(newCommit)) - }else{ + } else { nextRest +:= tuple -> restParentsMap } } @@ -295,7 +329,7 @@ var fileList: List[(ObjectId, FileMode, String, Option[String])] = Nil useTreeWalk(revCommit){ treeWalk => while (treeWalk.next()) { - val linkUrl =if (treeWalk.getFileMode(0) == FileMode.GITLINK) { + val linkUrl = if (treeWalk.getFileMode(0) == FileMode.GITLINK) { getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url) } else None fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, linkUrl) @@ -345,7 +379,7 @@ def getTreeId(git: Git, revision: String): Option[String] = { using(new RevWalk(git.getRepository)){ revWalk => val objectId = git.getRepository.resolve(revision) - if(objectId==null) return None + if(objectId == null) return None val revCommit = revWalk.parseCommit(objectId) Some(revCommit.getTree.name) } @@ -357,7 +391,7 @@ def getAllFileListByTreeId(git: Git, treeId: String): List[String] = { using(new RevWalk(git.getRepository)){ revWalk => val objectId = git.getRepository.resolve(treeId+"^{tree}") - if(objectId==null) return Nil + if(objectId == null) return Nil using(new TreeWalk(git.getRepository)){ treeWalk => treeWalk.addTree(objectId) treeWalk.setRecursive(true) @@ -705,6 +739,8 @@ refUpdate.setNewObjectId(newHeadId) refUpdate.update() + removeCache(git) + newHeadId } @@ -877,6 +913,7 @@ /** * Returns the last modified commit of specified path + * * @param git the Git object * @param startCommit the search base commit id * @param path the path of target file or directory @@ -959,6 +996,7 @@ /** * Returns sha1 + * * @param owner repository owner * @param name repository name * @param revstr A git object references expression diff --git a/src/main/scala/gitbucket/core/util/Notifier.scala b/src/main/scala/gitbucket/core/util/Notifier.scala index 3980029..58b9d32 100644 --- a/src/main/scala/gitbucket/core/util/Notifier.scala +++ b/src/main/scala/gitbucket/core/util/Notifier.scala @@ -109,6 +109,9 @@ } smtp.ssl.foreach { ssl => email.setSSLOnConnect(ssl) + if(ssl == true) { + email.setSslSmtpPort(smtp.port.get.toString) + } } smtp.fromAddress .map (_ -> smtp.fromName.getOrElse(context.loginAccount.get.userName)) diff --git a/src/main/scala/gitbucket/core/view/LinkConverter.scala b/src/main/scala/gitbucket/core/view/LinkConverter.scala index 996fac2..00d4e88 100644 --- a/src/main/scala/gitbucket/core/view/LinkConverter.scala +++ b/src/main/scala/gitbucket/core/view/LinkConverter.scala @@ -73,7 +73,7 @@ } // convert issue id to link - .replaceBy(("(?<=(^|\\W))(GH-|" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r){ m => + .replaceBy(("(?<=(^|\\W))(GH-|(? 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) => diff --git a/src/main/scala/gitbucket/core/view/Markdown.scala b/src/main/scala/gitbucket/core/view/Markdown.scala index 6eb3641..46b39b2 100644 --- a/src/main/scala/gitbucket/core/view/Markdown.scala +++ b/src/main/scala/gitbucket/core/view/Markdown.scala @@ -44,7 +44,8 @@ val renderer = new GitBucketMarkedRenderer(options, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages) - helpers.decorateHtml(Marked.marked(source, options, renderer), repository) + //helpers.decorateHtml(Marked.marked(source, options, renderer), repository) + Marked.marked(source, options, renderer) } /** @@ -109,11 +110,10 @@ override def text(text: String): String = { // convert commit id and username to link. val t1 = if(enableRefsLink) convertRefsLinks(text, repository, "#", false) else text - // convert task list to checkbox. val t2 = if(enableTaskList) convertCheckBox(t1, hasWritePermission) else t1 - - t2 + // decorate by TextDecorator plugins + helpers.decorateHtml(t2, repository) } override def link(href: String, title: String, text: String): String = { diff --git a/src/main/twirl/gitbucket/core/account/application.scala.html b/src/main/twirl/gitbucket/core/account/application.scala.html index 22ff200..fd3b8e7 100644 --- a/src/main/twirl/gitbucket/core/account/application.scala.html +++ b/src/main/twirl/gitbucket/core/account/application.scala.html @@ -19,8 +19,8 @@ Delete
- @gitbucket.core.helper.html.copy("generated-token-copy", tokenString){ - + @gitbucket.core.helper.html.copy("generated-token", "generated-token-copy", tokenString){ + }

diff --git a/src/main/twirl/gitbucket/core/account/group.scala.html b/src/main/twirl/gitbucket/core/account/group.scala.html index 797c669..6be84c9 100644 --- a/src/main/twirl/gitbucket/core/account/group.scala.html +++ b/src/main/twirl/gitbucket/core/account/group.scala.html @@ -43,10 +43,10 @@
@if(account.isDefined){ } - + @if(account.isDefined){ Cancel } @@ -136,4 +136,4 @@ $('#members').val(members); } }); - \ No newline at end of file + diff --git a/src/main/twirl/gitbucket/core/account/main.scala.html b/src/main/twirl/gitbucket/core/account/main.scala.html index 69cc133..8d68399 100644 --- a/src/main/twirl/gitbucket/core/account/main.scala.html +++ b/src/main/twirl/gitbucket/core/account/main.scala.html @@ -38,7 +38,7 @@ @if(account.isGroupAccount){ Members } else { - Public Activity + Public activity } @gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab => @tab(account, context).map { link => @@ -48,14 +48,14 @@ @if(context.loginAccount.isDefined && context.loginAccount.get.userName == account.userName){
  • } @if(context.loginAccount.isDefined && account.isGroupAccount && isGroupManager){
  • } diff --git a/src/main/twirl/gitbucket/core/admin/menu.scala.html b/src/main/twirl/gitbucket/core/admin/menu.scala.html index 15eb6d0..c7e3ab0 100644 --- a/src/main/twirl/gitbucket/core/admin/menu.scala.html +++ b/src/main/twirl/gitbucket/core/admin/menu.scala.html @@ -3,10 +3,10 @@
    - + Cancel
    @@ -127,4 +127,4 @@ $('#members').val(members); } }); - \ No newline at end of file + diff --git a/src/main/twirl/gitbucket/core/admin/userlist.scala.html b/src/main/twirl/gitbucket/core/admin/userlist.scala.html index 7e4786a..71d52c1 100644 --- a/src/main/twirl/gitbucket/core/admin/userlist.scala.html +++ b/src/main/twirl/gitbucket/core/admin/userlist.scala.html @@ -3,8 +3,8 @@ @gitbucket.core.html.main("Manage Users"){ @gitbucket.core.admin.html.menu("users"){