diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 854e8068c..464249075 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ ### Before submitting an issue to Gitbucket I have first: -- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md) +- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md) - [] searched for similar already existing issue - [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 91947c642..c59bec80d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ### Before submitting a pull-request to Gitbucket I have first: -- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md) +- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md) - [] rebased my branch over master - [] verified that project is compiling - [] verified that tests are passing diff --git a/README.md b/README.md index 0f072eade..fc3338b44 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,13 @@ Support Release Notes -------- +### 3.12 - 27 Feb 2016 +- New GitHub UI +- Improve mobile view +- Improve printing style +- Individual URL for pull request tabs +- SSH host configuration is separated from HTTP base URL + ### 3.11 - 30 Jan 2016 - Upgrade Scalatra to 2.4 - Sidebar and Footer for Wiki diff --git a/build.sbt b/build.sbt index d0f0d6488..7da176e42 100644 --- a/build.sbt +++ b/build.sbt @@ -15,8 +15,7 @@ scalaVersion := "2.11.7" // dependency settings resolvers ++= Seq( Classpaths.typesafeReleases, - "amateras-repo" at "http://amateras.sourceforge.jp/mvn/", - "amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/" + "sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/" ) libraryDependencies ++= Seq( "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r", @@ -26,7 +25,7 @@ libraryDependencies ++= Seq( "org.json4s" %% "json4s-jackson" % "3.3.0", "io.github.gitbucket" %% "scalatra-forms" % "1.0.0", "commons-io" % "commons-io" % "2.4", - "io.github.gitbucket" % "markedj" % "1.0.6", + "io.github.gitbucket" % "markedj" % "1.0.7", "org.apache.commons" % "commons-compress" % "1.10", "org.apache.commons" % "commons-email" % "1.4", "org.apache.httpcomponents" % "httpclient" % "4.5.1", @@ -40,6 +39,7 @@ libraryDependencies ++= Seq( "com.typesafe" % "config" % "1.3.0", "com.typesafe.akka" %% "akka-actor" % "2.3.14", "io.getquill" %% "quill-jdbc" % "0.4.1", + "fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0", "com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"), "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", @@ -83,36 +83,36 @@ jrebelSettings // Create executable war file val executableConfig = config("executable").hide Keys.ivyConfigurations += executableConfig -libraryDependencies ++= Seq( - "org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable" +libraryDependencies ++= Seq( + "org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable" ) -val executableKey = TaskKey[File]("executable") -executableKey := { +val executableKey = TaskKey[File]("executable") +executableKey := { import org.apache.ivy.util.ChecksumHelper import java.util.jar.{ Manifest => JarManifest } import java.util.jar.Attributes.{ Name => AttrName } - val workDir = Keys.target.value / "executable" - val warName = Keys.name.value + ".war" + val workDir = Keys.target.value / "executable" + val warName = Keys.name.value + ".war" - val log = streams.value.log + val log = streams.value.log log info s"building executable webapp in ${workDir}" // initialize temp directory - val temp = workDir / "webapp" + val temp = workDir / "webapp" IO delete temp // include jetty classes - val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name) + val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name) jettyJars foreach { jar => IO unzip (jar, temp, (name:String) => (name startsWith "javax/") || @@ -121,31 +121,34 @@ executableKey := { } // include original war file - val warFile = (Keys.`package`).value + val warFile = (Keys.`package`).value IO unzip (warFile, temp) // include launcher classes - val classDir = (Keys.classDirectory in Compile).value - val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */) + val classDir = (Keys.classDirectory in Compile).value + val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */) launchClasses foreach { name => IO copyFile (classDir / name, temp / name) } // zip it up IO delete (temp / "META-INF" / "MANIFEST.MF") - val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp) - val manifest = new JarManifest - manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0") - manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher") - val outputFile = workDir / warName + val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp) + val manifest = new JarManifest + manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0") + manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher") + val outputFile = workDir / warName IO jar (contentMappings, outputFile, manifest) // generate checksums - Seq("md5", "sha1") foreach { algorithm => - IO.write( - workDir / (warName + "." + algorithm), - ChecksumHelper computeAsString (outputFile, algorithm) - ) + Seq( + "md5" -> "MD5", + "sha1" -> "SHA-1", + "sha256" -> "SHA-256" + ) + .foreach { case (extension, algorithm) => + val checksumFile = workDir / (warName + "." + extension) + Checksums generate (outputFile, checksumFile, algorithm) } // done @@ -154,7 +157,7 @@ executableKey := { } /* Keys.artifact in (Compile, executableKey) ~= { - _ copy (`type` = "war", extension = "war")) + _ copy (`type` = "war", extension = "war")) } addArtifact(Keys.artifact in (Compile, executableKey), executableKey) */ diff --git a/doc/how_to_run.md b/doc/how_to_run.md index 9c1506c19..ea3190923 100644 --- a/doc/how_to_run.md +++ b/doc/how_to_run.md @@ -32,3 +32,11 @@ $ sbt executable ``` at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact. + +Run tests spec +--------- +To run the full serie of tests, run the following command: + +``` +sbt test +``` diff --git a/doc/release.md b/doc/release.md index da0c0038c..98ba38328 100644 --- a/doc/release.md +++ b/doc/release.md @@ -6,15 +6,14 @@ Update version number Note to update version number in files below: -### project/build.scala +### build.sbt ```scala -object MyBuild extends Build { - val Organization = "gitbucket" - val Name = "gitbucket" - val Version = "3.3.0" // <---- update version!! - val ScalaVersion = "2.11.6" - val ScalatraVersion = "2.3.1" +val Organization = "gitbucket" +val Name = "gitbucket" +val GitBucketVersion = "3.12.0" // <---- update version!! +val ScalatraVersion = "2.4.0" +val JettyVersion = "9.3.6.v20151106" ``` ### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala @@ -26,8 +25,8 @@ object AutoUpdate { * The history of versions. A head of this sequence is the current GitBucket version. */ val versions = Seq( - new Version(3, 3), // <---- add this line!! - new Version(3, 2), + new Version(3, 12), // <---- add this line!! + new Version(3, 11), ``` Generate release files diff --git a/project/Checksums.scala b/project/Checksums.scala new file mode 100644 index 000000000..dc9d8491b --- /dev/null +++ b/project/Checksums.scala @@ -0,0 +1,34 @@ +import java.security.MessageDigest; +import scala.annotation._ +import sbt._ +import sbt.Using._ + +object Checksums { + private val bufferSize = 2048 + + def generate(source:File, target:File, algorithm:String):Unit = + IO write (target, compute(source, algorithm)) + + def compute(file:File, algorithm:String):String = + hex(raw(file, algorithm)) + + def raw(file:File, algorithm:String):Array[Byte] = + (Using fileInputStream file) { is => + val md = MessageDigest getInstance algorithm + val buf = new Array[Byte](bufferSize) + md.reset() + @tailrec + def loop() { + val len = is read buf + if (len != -1) { + md update (buf, 0, len) + loop() + } + } + loop() + md.digest() + } + + def hex(bytes:Array[Byte]):String = + bytes map { it => "%02x" format (it.toInt & 0xff) } mkString "" +} diff --git a/src/main/resources/update/3_13.sql b/src/main/resources/update/3_13.sql new file mode 100644 index 000000000..8efe1a3ab --- /dev/null +++ b/src/main/resources/update/3_13.sql @@ -0,0 +1 @@ +ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100); \ No newline at end of file diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index 554bb3258..30c507a5f 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -27,10 +27,10 @@ class ScalatraBootstrap extends LifeCycle { } context.mount(new IndexController, "/") + context.mount(new ApiController, "/api/v3") context.mount(new FileUploadController, "/upload") + context.mount(new SystemSettingsController, "/admin") context.mount(new DashboardController, "/*") - context.mount(new UserManagementController, "/*") - context.mount(new SystemSettingsController, "/*") context.mount(new AccountController, "/*") context.mount(new RepositoryViewerController, "/*") context.mount(new WikiController, "/*") diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 88d2877d7..65613773b 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -1,7 +1,6 @@ package gitbucket.core.controller import gitbucket.core.account.html -import gitbucket.core.api._ import gitbucket.core.helper import gitbucket.core.model.GroupMember import gitbucket.core.service._ @@ -14,22 +13,19 @@ import gitbucket.core.util._ import io.github.gitbucket.scalatra.forms._ import org.apache.commons.io.FileUtils -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.dircache.DirCache -import org.eclipse.jgit.lib.{FileMode, Constants} import org.scalatra.i18n.Messages class AccountController extends AccountControllerBase with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - with AccessTokenService with WebHookService + with AccessTokenService with WebHookService with RepositoryCreationService trait AccountControllerBase extends AccountManagementControllerBase { self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - with AccessTokenService with WebHookService => + with AccessTokenService with WebHookService with RepositoryCreationService => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, url: Option[String], fileId: Option[String]) @@ -156,25 +152,6 @@ trait AccountControllerBase extends AccountManagementControllerBase { } } - /** - * https://developer.github.com/v3/users/#get-a-single-user - */ - get("/api/v3/users/:userName") { - getAccountByUserName(params("userName")).map { account => - JsonFormat(ApiUser(account)) - } getOrElse NotFound - } - - /** - * https://developer.github.com/v3/users/#get-the-authenticated-user - */ - get("/api/v3/user") { - context.loginAccount.map { account => - JsonFormat(ApiUser(account)) - } getOrElse Unauthorized - } - - get("/:userName/_edit")(oneselfOnly { val userName = params("userName") getAccountByUserName(userName).map { x => @@ -367,7 +344,7 @@ trait AccountControllerBase extends AccountManagementControllerBase { post("/new", newRepositoryForm)(usersOnly { form => LockUtil.lock(s"${form.owner}/${form.name}"){ if(getRepository(form.owner, form.name).isEmpty){ - createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme) + createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme) } // redirect to the repository @@ -375,54 +352,6 @@ trait AccountControllerBase extends AccountManagementControllerBase { } }) - /** - * 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).isEmpty){ - createRepository(owner, data.name, data.description, data.`private`, data.auto_init) - val repository = getRepository(owner, data.name).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).isEmpty){ - createRepository(groupName, data.name, data.description, data.`private`, data.auto_init) - val repository = getRepository(groupName, data.name).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 @@ -456,7 +385,7 @@ trait AccountControllerBase extends AccountManagementControllerBase { val originUserName = repository.repository.originUserName.getOrElse(repository.owner) val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) - createRepository( + insertRepository( repositoryName = repository.name, userName = accountName, description = repository.repository.description, @@ -496,68 +425,6 @@ trait AccountControllerBase extends AccountManagementControllerBase { } }) - 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.groupAccount){ - 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") - createLabel(userName, repositoryName, "enhancement", "84b6eb") - createLabel(userName, repositoryName, "invalid", "e6e6e6") - createLabel(userName, repositoryName, "question", "cc317c") - createLabel(userName, repositoryName, "wontfix", "ffffff") - } - private def existsAccount: Constraint = new Constraint(){ override def validate(name: String, value: String, messages: Messages): Option[String] = if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala new file mode 100644 index 000000000..11d764bd3 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -0,0 +1,389 @@ +package gitbucket.core.controller + +import gitbucket.core.api._ +import gitbucket.core.model._ +import gitbucket.core.service.IssuesService.IssueSearchCondition +import gitbucket.core.service.PullRequestService._ +import gitbucket.core.service._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.JGitUtil.CommitInfo +import gitbucket.core.util._ +import gitbucket.core.util.Implicits._ +import org.eclipse.jgit.api.Git +import org.scalatra.{NoContent, UnprocessableEntity, Created} +import scala.collection.JavaConverters._ + +class ApiController extends ApiControllerBase + with RepositoryService + with AccountService + with ProtectedBranchService + with IssuesService + with LabelsService + with PullRequestService + with CommitStatusService + with RepositoryCreationService + with HandleCommentService + with WebHookService + with WebHookPullRequestService + with WebHookIssueCommentService + with WikiService + with ActivityService + with OwnerAuthenticator + with UsersAuthenticator + with GroupManagerAuthenticator + with ReferrerAuthenticator + with ReadableUsersAuthenticator + with CollaboratorsAuthenticator + +trait ApiControllerBase extends ControllerBase { + self: RepositoryService + with AccountService + with ProtectedBranchService + with IssuesService + with LabelsService + with PullRequestService + with CommitStatusService + with RepositoryCreationService + with HandleCommentService + with OwnerAuthenticator + with UsersAuthenticator + with GroupManagerAuthenticator + with ReferrerAuthenticator + with ReadableUsersAuthenticator + with CollaboratorsAuthenticator => + + /** + * https://developer.github.com/v3/users/#get-a-single-user + */ + get("/api/v3/users/:userName") { + getAccountByUserName(params("userName")).map { account => + JsonFormat(ApiUser(account)) + } getOrElse NotFound + } + + /** + * https://developer.github.com/v3/users/#get-the-authenticated-user + */ + get("/api/v3/user") { + context.loginAccount.map { account => + JsonFormat(ApiUser(account)) + } getOrElse Unauthorized + } + + /** + * 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).isEmpty){ + createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init) + val repository = getRepository(owner, data.name).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).isEmpty){ + createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init) + val repository = getRepository(groupName, data.name).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 + }) + + /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ + 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 + protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) + } yield { + if(protection.enabled){ + enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts) + } else { + disableBranchProtection(repository.owner, repository.name, branch) + } + JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) + }) getOrElse NotFound + }) + + /** + * @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status + * but not enabled. + */ + get("/api/v3/rate_limit"){ + contentType = formats("json") + // this message is same as github enterprise... + org.scalatra.NotFound(ApiError("Rate limiting is not enabled.")) + } + + /** + * 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) + } yield { + JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) + }).getOrElse(NotFound) + }) + + /** + * https://developer.github.com/v3/issues/comments/#create-a-comment + */ + post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository => + (for{ + issueId <- params("id").toIntOpt + issue <- getIssue(repository.owner, repository.name, issueId.toString) + body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty + action = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + (issue, id) <- handleComment(issue, Some(body), repository, action) + issueComment <- getComment(repository.owner, repository.name, id.toString()) + } yield { + JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest)) + }) getOrElse NotFound + }) + + /** + * List all labels for this repository + * https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository + */ + get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository => + JsonFormat(getLabels(repository.owner, repository.name).map { label => + ApiLabel(label, RepositoryName(repository)) + }) + }) + + /** + * Get a single label + * https://developer.github.com/v3/issues/labels/#get-a-single-label + */ + get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository => + getLabel(repository.owner, repository.name, params("labelName")).map { label => + JsonFormat(ApiLabel(label, RepositoryName(repository))) + } getOrElse NotFound() + }) + + /** + * Create a label + * https://developer.github.com/v3/issues/labels/#create-a-label + */ + post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository => + (for{ + data <- extractFromJsonBody[CreateALabel] if data.isValid + } yield { + LockUtil.lock(RepositoryName(repository).fullName) { + if (getLabel(repository.owner, repository.name, data.name).isEmpty) { + val labelId = createLabel(repository.owner, repository.name, data.name, data.color) + getLabel(repository.owner, repository.name, labelId).map { label => + Created(JsonFormat(ApiLabel(label, RepositoryName(repository)))) + } getOrElse NotFound() + } 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") + )) + } + } + }) getOrElse NotFound() + }) + + /** + * Update a label + * https://developer.github.com/v3/issues/labels/#update-a-label + */ + patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + (for{ + data <- extractFromJsonBody[CreateALabel] if data.isValid + } yield { + LockUtil.lock(RepositoryName(repository).fullName) { + getLabel(repository.owner, repository.name, params("labelName")).map { label => + if (getLabel(repository.owner, repository.name, data.name).isEmpty) { + updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color) + JsonFormat(ApiLabel( + getLabel(repository.owner, repository.name, label.labelId).get, + 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"))) + } + } getOrElse NotFound() + } + }) getOrElse NotFound() + }) + + /** + * Delete a label + * https://developer.github.com/v3/issues/labels/#delete-a-label + */ + delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + LockUtil.lock(RepositoryName(repository).fullName) { + getLabel(repository.owner, repository.name, params("labelName")).map { label => + deleteLabel(repository.owner, repository.name, label.labelId) + NoContent() + } getOrElse NotFound() + } + }) + + /** + * https://developer.github.com/v3/pulls/#list-pull-requests + */ + get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository => + val page = IssueSearchCondition.page(request) + // TODO: more api spec condition + val condition = IssueSearchCondition(request) + val baseOwner = getAccountByUserName(repository.owner).get + val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name) + JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) => + ApiPullRequest( + issue, + pullRequest, + ApiRepository(headRepo, ApiUser(headOwner)), + ApiRepository(repository, ApiUser(baseOwner)), + ApiUser(issueUser)) }) + }) + + /** + * https://developer.github.com/v3/pulls/#get-a-single-pull-request + */ + get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository => + (for{ + issueId <- params("id").toIntOpt + (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId) + users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set()) + 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))) + }).getOrElse(NotFound) + }) + + /** + * https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request + */ + get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository => + val owner = repository.owner + val name = repository.name + params("id").toIntOpt.flatMap{ issueId => + getPullRequest(owner, name, issueId) map { case(issue, pullreq) => + using(Git.open(getRepositoryDir(owner, name))){ git => + 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 + JsonFormat(commits) + } + } + } getOrElse NotFound + }) + + /** + * https://developer.github.com/v3/repos/#get + */ + get("/api/v3/repos/:owner/:repository")(referrersOnly { repository => + JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get))) + }) + + /** + * https://developer.github.com/v3/repos/statuses/#create-a-status + */ + post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { 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) + } yield { + JsonFormat(ApiCommitStatus(status, ApiUser(creator))) + }) getOrElse NotFound + }) + + /** + * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref + * + * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name. + */ + val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository => + (for{ + ref <- params.get("ref") + sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) + } yield { + JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) => + ApiCommitStatus(status, ApiUser(creator)) + }) + }) getOrElse NotFound + }) + + /** + * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref + * + * legacy route + */ + get("/api/v3/repos/:owner/:repo/statuses/:ref"){ + listStatusesRoute.action() + } + + /** + * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref + * + * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name. + */ + get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository => + (for{ + ref <- params.get("ref") + owner <- getAccountByUserName(repository.owner) + sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) + } yield { + val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha) + JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner))) + }) getOrElse NotFound + }) + + private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = + hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + +} + diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 194bcae3f..5a6e00685 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -121,16 +121,6 @@ trait IndexControllerBase extends ControllerBase { getAccountByUserName(params("userName")).isDefined }) - /** - * @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status - * but not enabled. - */ - get("/api/v3/rate_limit"){ - contentType = formats("json") - // this message is same as github enterprise... - org.scalatra.NotFound(ApiError("Rate limiting is not enabled.")) - } - // TODO Move to RepositoryViwerController? post("/search", searchForm){ form => redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}") diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index e74d9e10a..a1aa362c6 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -1,8 +1,6 @@ package gitbucket.core.controller -import gitbucket.core.api._ import gitbucket.core.issues.html -import gitbucket.core.model.Issue import gitbucket.core.service.IssuesService._ import gitbucket.core.service._ import gitbucket.core.util.ControlUtil._ @@ -16,11 +14,11 @@ import org.scalatra.Ok class IssuesController extends IssuesControllerBase - with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService + with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService trait IssuesControllerBase extends ControllerBase { - self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService + self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService => case class IssueCreateForm(title: String, content: Option[String], @@ -78,18 +76,6 @@ trait IssuesControllerBase extends ControllerBase { } }) - /** - * 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) - } yield { - JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) - }).getOrElse(NotFound) - }) - get("/:owner/:repository/issues/new")(readableUsersOnly { repository => defining(repository.owner, repository.name){ case (owner, name) => html.create( @@ -128,7 +114,7 @@ trait IssuesControllerBase extends ControllerBase { getIssue(owner, name, issueId.toString).foreach { issue => // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) // call web hooks callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get) @@ -150,7 +136,7 @@ trait IssuesControllerBase extends ControllerBase { // update issue updateIssue(owner, name, issue.issueId, title, issue.content) // extract references and create refer comment - createReferComment(owner, name, issue.copy(title = title), title) + createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get) redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") } else Unauthorized @@ -165,7 +151,7 @@ trait IssuesControllerBase extends ControllerBase { // update issue updateIssue(owner, name, issue.issueId, issue.title, content) // extract references and create refer comment - createReferComment(owner, name, issue, content.getOrElse("")) + createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get) redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") } else Unauthorized @@ -174,30 +160,22 @@ trait IssuesControllerBase extends ControllerBase { }) post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => + val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } } getOrElse NotFound }) - /** - * https://developer.github.com/v3/issues/comments/#create-a-comment - */ - post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository => - (for{ - issueId <- params("id").toIntOpt - body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty - (issue, id) <- handleComment(issueId, Some(body), repository)() - issueComment <- getComment(repository.owner, repository.name, id.toString()) - } yield { - JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest)) - }) getOrElse NotFound - }) - post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, form.content, repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => + val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } } getOrElse NotFound }) @@ -315,8 +293,16 @@ trait IssuesControllerBase extends ControllerBase { post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => defining(params.get("value")){ action => action match { - case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) } - case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) } + case Some("open") => executeBatch(repository) { issueId => + getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => + handleComment(issue, None, repository, Some("reopen")) + } + } + case Some("close") => executeBatch(repository) { issueId => + getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => + handleComment(issue, None, repository, Some("close")) + } + } case _ => // TODO BadRequest } } @@ -373,99 +359,6 @@ trait IssuesControllerBase extends ControllerBase { } } - // 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 - 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") - } - } - } - } - - /** - * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] - */ - private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) - (getAction: Issue => Option[String] = - p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { - - defining(repository.owner, repository.name){ case (owner, name) => - val userName = context.loginAccount.get.userName - - getIssue(owner, name, issueId.toString) flatMap { issue => - val (action, recordActivity) = - getAction(issue) - .collect { - case "close" if(!issue.closed) => true -> - (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) - case "reopen" if(issue.closed) => false -> - (Some("reopen") -> Some(recordReopenIssueActivity _)) - } - .map { case (closed, t) => - updateClosed(owner, name, issueId, closed) - t - } - .getOrElse(None -> None) - - val commentId = (content, action) match { - case (None, None) => None - case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action)) - case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment"))) - } - - // record comment activity if comment is entered - content foreach { - (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) - (owner, name, userName, issueId, _) - } - recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) - - // extract references and create refer comment - content.map { content => - createReferComment(owner, name, issue, content) - } - - // call web hooks - action match { - case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) } - case Some(act) => val webHookAction = act match { - case "open" => "opened" - case "reopen" => "reopened" - case "close" => "closed" - case _ => act - } - if(issue.isPullRequest){ - callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get) - } else { - callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get) - } - } - - // notifications - Notifier() match { - case f => - content foreach { - f.toNotify(repository, issue, _){ - Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}") - } - } - action foreach { - f.toNotify(repository, issue, _){ - Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") - } - } - } - - commentId.map( issue -> _ ) - } - } - } - private def searchIssues(repository: RepositoryService.RepositoryInfo) = { defining(repository.owner, repository.name){ case (owner, repoName) => val page = IssueSearchCondition.page(request) diff --git a/src/main/scala/gitbucket/core/controller/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala index 4074c6108..9ebdf6e16 100644 --- a/src/main/scala/gitbucket/core/controller/LabelsController.scala +++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala @@ -1,13 +1,12 @@ package gitbucket.core.controller -import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat} import gitbucket.core.issues.labels.html import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService} -import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator} import gitbucket.core.util.Implicits._ import io.github.gitbucket.scalatra.forms._ import org.scalatra.i18n.Messages -import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok} +import org.scalatra.Ok class LabelsController extends LabelsControllerBase with LabelsService with IssuesService with RepositoryService with AccountService @@ -32,26 +31,6 @@ trait LabelsControllerBase extends ControllerBase { hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - /** - * List all labels for this repository - * https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository - */ - get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository => - JsonFormat(getLabels(repository.owner, repository.name).map { label => - ApiLabel(label, RepositoryName(repository)) - }) - }) - - /** - * Get a single label - * https://developer.github.com/v3/issues/labels/#get-a-single-label - */ - get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository => - getLabel(repository.owner, repository.name, params("labelName")).map { label => - JsonFormat(ApiLabel(label, RepositoryName(repository))) - } getOrElse NotFound() - }) - ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => html.edit(None, repository) }) @@ -66,31 +45,6 @@ trait LabelsControllerBase extends ControllerBase { hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - /** - * Create a label - * https://developer.github.com/v3/issues/labels/#create-a-label - */ - post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository => - (for{ - data <- extractFromJsonBody[CreateALabel] if data.isValid - } yield { - LockUtil.lock(RepositoryName(repository).fullName) { - if (getLabel(repository.owner, repository.name, data.name).isEmpty) { - val labelId = createLabel(repository.owner, repository.name, data.name, data.color) - getLabel(repository.owner, repository.name, labelId).map { label => - Created(JsonFormat(ApiLabel(label, RepositoryName(repository)))) - } getOrElse NotFound() - } 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") - )) - } - } - }) getOrElse NotFound() - }) - ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => html.edit(Some(label), repository) @@ -107,50 +61,11 @@ trait LabelsControllerBase extends ControllerBase { hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - /** - * Update a label - * https://developer.github.com/v3/issues/labels/#update-a-label - */ - patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => - (for{ - data <- extractFromJsonBody[CreateALabel] if data.isValid - } yield { - LockUtil.lock(RepositoryName(repository).fullName) { - getLabel(repository.owner, repository.name, params("labelName")).map { label => - if (getLabel(repository.owner, repository.name, data.name).isEmpty) { - updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color) - JsonFormat(ApiLabel( - getLabel(repository.owner, repository.name, label.labelId).get, - 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"))) - } - } getOrElse NotFound() - } - }) getOrElse NotFound() - }) - ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => deleteLabel(repository.owner, repository.name, params("labelId").toInt) Ok() }) - /** - * Delete a label - * https://developer.github.com/v3/issues/labels/#delete-a-label - */ - delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => - LockUtil.lock(RepositoryName(repository).fullName) { - getLabel(repository.owner, repository.name, params("labelName")).map { label => - deleteLabel(repository.owner, repository.name, label.labelId) - NoContent() - } getOrElse NotFound() - } - }) - /** * Constraint for the identifier such as user name, repository name or page name. */ diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 5ed6859a9..16a7a1c57 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -1,7 +1,6 @@ package gitbucket.core.controller -import gitbucket.core.api._ -import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook} +import gitbucket.core.model.WebHook import gitbucket.core.pulls.html import gitbucket.core.service.CommitStatusService import gitbucket.core.service.MergeService @@ -82,24 +81,6 @@ trait PullRequestsControllerBase extends ControllerBase { } }) - /** - * https://developer.github.com/v3/pulls/#list-pull-requests - */ - get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository => - val page = IssueSearchCondition.page(request) - // TODO: more api spec condition - val condition = IssueSearchCondition(request) - val baseOwner = getAccountByUserName(repository.owner).get - val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name) - JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) => - ApiPullRequest( - issue, - pullRequest, - ApiRepository(headRepo, ApiUser(headOwner)), - ApiRepository(repository, ApiUser(baseOwner)), - ApiUser(issueUser)) }) - }) - get("/:owner/:repository/pull/:id")(referrersOnly { repository => params("id").toIntOpt.flatMap{ issueId => val owner = repository.owner @@ -126,47 +107,6 @@ trait PullRequestsControllerBase extends ControllerBase { } getOrElse NotFound }) - /** - * https://developer.github.com/v3/pulls/#get-a-single-pull-request - */ - get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository => - (for{ - issueId <- params("id").toIntOpt - (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId) - users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set()) - 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))) - }).getOrElse(NotFound) - }) - - /** - * https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request - */ - get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository => - val owner = repository.owner - val name = repository.name - params("id").toIntOpt.flatMap{ issueId => - getPullRequest(owner, name, issueId) map { case(issue, pullreq) => - using(Git.open(getRepositoryDir(owner, name))){ git => - 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 - JsonFormat(commits) - } - } - } getOrElse NotFound - }) - ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository => params("id").toIntOpt.flatMap{ issueId => val owner = repository.owner @@ -523,7 +463,7 @@ trait PullRequestsControllerBase extends ControllerBase { getIssue(owner, name, issueId.toString) foreach { issue => // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) // notifications Notifier().toNotify(repository, issue, form.content.getOrElse("")){ @@ -535,19 +475,6 @@ trait PullRequestsControllerBase extends ControllerBase { } }) - // 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. * @@ -611,14 +538,4 @@ trait PullRequestsControllerBase extends ControllerBase { hasWritePermission(owner, repoName, context.loginAccount)) } - // TODO: same as gitbucket.core.servlet.CommitLogHook ... - private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = { - StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => - if(getIssue(owner, repository, issueId).isDefined){ - getAccountByMailAddress(commit.committerEmailAddress).foreach { account => - createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") - } - } - } - } } diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 3166f9c9a..87f63c13d 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -49,11 +49,12 @@ trait RepositorySettingsControllerBase extends ControllerBase { )(CollaboratorForm.apply) // for web hook url addition - case class WebHookForm(url: String, events: Set[WebHook.Event]) + case class WebHookForm(url: String, events: Set[WebHook.Event], token: Option[String]) def webHookForm(update:Boolean) = mapping( "url" -> trim(label("url", text(required, webHook(update)))), - "events" -> webhookEvents + "events" -> webhookEvents, + "token" -> optional(trim(label("token", text(maxlength(100))))) )(WebHookForm.apply) // for transfer ownership @@ -141,22 +142,6 @@ trait RepositorySettingsControllerBase extends ControllerBase { } }) - /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ - 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 - protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) - } yield { - if(protection.enabled){ - enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts) - } else { - disableBranchProtection(repository.owner, repository.name, branch) - } - JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) - }) getOrElse NotFound - }) - /** * Display the Collaborators page. */ @@ -198,7 +183,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { * Display the web hook edit page. */ get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => - val webhook = WebHook(repository.owner, repository.name, "") + val webhook = WebHook(repository.owner, repository.name, "", None) html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true) }) @@ -206,7 +191,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { * Add the web hook URL. */ post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) => - addWebHook(repository.owner, repository.name, form.url, form.events) + addWebHook(repository.owner, repository.name, form.url, form.events, form.token) flash += "info" -> s"Webhook ${form.url} created" redirect(s"/${repository.owner}/${repository.name}/settings/hooks") }) @@ -235,7 +220,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { import scala.concurrent.ExecutionContext.Implicits.global val url = params("url") - val dummyWebHookInfo = WebHook(repository.owner, repository.name, url) + val token = Some(params("token")) + val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, token) val dummyPayload = { val ownerAccount = getAccountByUserName(repository.owner).get val commits = if(repository.commitCount == 0) List.empty else git.log @@ -294,7 +280,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { * Update web hook settings. */ post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) => - updateWebHook(repository.owner, repository.name, form.url, form.events) + updateWebHook(repository.owner, repository.name, form.url, form.events, form.token) flash += "info" -> s"webhook ${form.url} updated" redirect(s"/${repository.owner}/${repository.name}/settings/hooks") }) diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 1007cf9d7..e5680d3be 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -2,7 +2,6 @@ package gitbucket.core.controller import javax.servlet.http.{HttpServletResponse, HttpServletRequest} -import gitbucket.core.api._ import gitbucket.core.plugin.PluginRegistry import gitbucket.core.repo.html import gitbucket.core.helper @@ -13,7 +12,7 @@ import gitbucket.core.util.StringUtil._ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Implicits._ import gitbucket.core.util.Directory._ -import gitbucket.core.model.{Account, CommitState, WebHook} +import gitbucket.core.model.{Account, WebHook} import gitbucket.core.service.WebHookService._ import gitbucket.core.view import gitbucket.core.view.helpers @@ -122,13 +121,6 @@ trait RepositoryViewerControllerBase extends ControllerBase { fileList(_) }) - /** - * https://developer.github.com/v3/repos/#get - */ - get("/api/v3/repos/:owner/:repository")(referrersOnly { repository => - JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get))) - }) - /** * Displays the file list of the specified path and branch. */ @@ -160,65 +152,6 @@ trait RepositoryViewerControllerBase extends ControllerBase { } }) - /** - * https://developer.github.com/v3/repos/statuses/#create-a-status - */ - post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { 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) - } yield { - JsonFormat(ApiCommitStatus(status, ApiUser(creator))) - }) getOrElse NotFound - }) - - /** - * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref - * - * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name. - */ - val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository => - (for{ - ref <- params.get("ref") - sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) - } yield { - JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) => - ApiCommitStatus(status, ApiUser(creator)) - }) - }) getOrElse NotFound - }) - - /** - * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref - * - * legacy route - */ - get("/api/v3/repos/:owner/:repo/statuses/:ref"){ - listStatusesRoute.action() - } - - /** - * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref - * - * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name. - */ - get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository => - (for{ - ref <- params.get("ref") - owner <- getAccountByUserName(repository.owner) - sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) - } yield { - val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha) - JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner))) - }) getOrElse NotFound - }) - get("/:owner/:repository/new/*")(collaboratorsOnly { repository => val (branch, path) = splitPath(repository, multiParams("splat").head) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index b921895f5..ecdf3378a 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -1,18 +1,24 @@ package gitbucket.core.controller import gitbucket.core.admin.html -import gitbucket.core.service.{AccountService, SystemSettingsService} +import gitbucket.core.service.{AccountService, SystemSettingsService, RepositoryService} import gitbucket.core.util.AdminAuthenticator import gitbucket.core.ssh.SshServer import gitbucket.core.plugin.PluginRegistry import SystemSettingsService._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.StringUtil._ import io.github.gitbucket.scalatra.forms._ +import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages class SystemSettingsController extends SystemSettingsControllerBase - with AccountService with AdminAuthenticator + with AccountService with RepositoryService with AdminAuthenticator -trait SystemSettingsControllerBase extends ControllerBase { - self: AccountService with AdminAuthenticator => +trait SystemSettingsControllerBase extends AccountManagementControllerBase { + self: AccountService with RepositoryService with AdminAuthenticator => private val form = mapping( "baseUrl" -> trim(label("Base URL", optional(text()))), @@ -68,6 +74,61 @@ trait SystemSettingsControllerBase extends ControllerBase { case class PluginForm(pluginIds: List[String]) + + case class NewUserForm(userName: String, password: String, fullName: String, + mailAddress: String, isAdmin: Boolean, + url: Option[String], fileId: Option[String]) + + case class EditUserForm(userName: String, password: Option[String], fullName: String, + mailAddress: String, isAdmin: Boolean, url: Option[String], + fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) + + case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], + members: String) + + case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], + members: String, clearImage: Boolean, isRemoved: Boolean) + + + val newUserForm = mapping( + "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), + "password" -> trim(label("Password" ,text(required, maxlength(20)))), + "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), + "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))), + "isAdmin" -> trim(label("User Type" ,boolean())), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))) + )(NewUserForm.apply) + + val editUserForm = mapping( + "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))), + "password" -> trim(label("Password" ,optional(text(maxlength(20))))), + "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), + "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))), + "isAdmin" -> trim(label("User Type" ,boolean())), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName")))) + )(EditUserForm.apply) + + val newGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) + )(NewGroupForm.apply) + + val editGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean())) + )(EditGroupForm.apply) + + get("/admin/system")(adminOnly { html.system(flash.get("info")) }) @@ -92,4 +153,138 @@ trait SystemSettingsControllerBase extends ControllerBase { html.plugins(PluginRegistry().getPlugins()) }) + + get("/admin/users")(adminOnly { + val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) + val users = getAllUsers(includeRemoved) + val members = users.collect { case account if(account.groupAccount) => + account.userName -> getGroupMembers(account.userName).map(_.userName) + }.toMap + + html.userlist(users, members, includeRemoved) + }) + + get("/admin/users/_newuser")(adminOnly { + html.user(None) + }) + + post("/admin/users/_newuser", newUserForm)(adminOnly { form => + createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url) + updateImage(form.userName, form.fileId, false) + redirect("/admin/users") + }) + + get("/admin/users/:userName/_edituser")(adminOnly { + val userName = params("userName") + html.user(getAccountByUserName(userName, true)) + }) + + post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => + val userName = params("userName") + getAccountByUserName(userName, true).map { account => + + if(form.isRemoved){ + // Remove repositories + // getRepositoryNamesOfUser(userName).foreach { repositoryName => + // deleteRepository(userName, repositoryName) + // FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) + // FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) + // FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) + // } + // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY + removeUserRelatedData(userName) + } + + updateAccount(account.copy( + password = form.password.map(sha1).getOrElse(account.password), + fullName = form.fullName, + mailAddress = form.mailAddress, + administrator = form.isAdmin, + url = form.url, + removed = form.isRemoved)) + + updateImage(userName, form.fileId, form.clearImage) + redirect("/admin/users") + + } getOrElse NotFound + }) + + get("/admin/users/_newgroup")(adminOnly { + html.usergroup(None, Nil) + }) + + post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => + createGroup(form.groupName, form.url) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) + updateImage(form.groupName, form.fileId, false) + redirect("/admin/users") + }) + + get("/admin/users/:groupName/_editgroup")(adminOnly { + defining(params("groupName")){ groupName => + html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName)) + } + }) + + post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => + getAccountByUserName(groupName, true).map { account => + updateGroup(groupName, form.url, form.isRemoved) + + if(form.isRemoved){ + // Remove from GROUP_MEMBER + updateGroupMembers(form.groupName, Nil) + // Remove repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + deleteRepository(groupName, repositoryName) + FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) + } + } else { + // Update GROUP_MEMBER + updateGroupMembers(form.groupName, members) + // Update COLLABORATOR for group repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + removeCollaborators(form.groupName, repositoryName) + members.foreach { case (userName, isManager) => + addCollaborator(form.groupName, repositoryName, userName) + } + } + } + + updateImage(form.groupName, form.fileId, form.clearImage) + redirect("/admin/users") + + } getOrElse NotFound + } + }) + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } + + protected def disableByNotYourself(paramName: String): Constraint = new Constraint() { + override def validate(name: String, value: String, messages: Messages): Option[String] = { + params.get(paramName).flatMap { userName => + if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true")) + Some("You can't disable your account yourself") + else + None + } + } + } + } diff --git a/src/main/scala/gitbucket/core/controller/UserManagementController.scala b/src/main/scala/gitbucket/core/controller/UserManagementController.scala deleted file mode 100644 index 6ff9dffe6..000000000 --- a/src/main/scala/gitbucket/core/controller/UserManagementController.scala +++ /dev/null @@ -1,204 +0,0 @@ -package gitbucket.core.controller - -import gitbucket.core.service.{RepositoryService, AccountService} -import gitbucket.core.admin.users.html -import gitbucket.core.util._ -import gitbucket.core.util.ControlUtil._ -import gitbucket.core.util.StringUtil._ -import gitbucket.core.util.Implicits._ -import gitbucket.core.util.Directory._ -import io.github.gitbucket.scalatra.forms._ -import org.scalatra.i18n.Messages -import org.apache.commons.io.FileUtils - -class UserManagementController extends UserManagementControllerBase - with AccountService with RepositoryService with AdminAuthenticator - -trait UserManagementControllerBase extends AccountManagementControllerBase { - self: AccountService with RepositoryService with AdminAuthenticator => - - case class NewUserForm(userName: String, password: String, fullName: String, - mailAddress: String, isAdmin: Boolean, - url: Option[String], fileId: Option[String]) - - case class EditUserForm(userName: String, password: Option[String], fullName: String, - mailAddress: String, isAdmin: Boolean, url: Option[String], - fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) - - case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], - members: String) - - case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], - members: String, clearImage: Boolean, isRemoved: Boolean) - - val newUserForm = mapping( - "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), - "password" -> trim(label("Password" ,text(required, maxlength(20)))), - "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), - "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))), - "isAdmin" -> trim(label("User Type" ,boolean())), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))) - )(NewUserForm.apply) - - val editUserForm = mapping( - "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))), - "password" -> trim(label("Password" ,optional(text(maxlength(20))))), - "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), - "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))), - "isAdmin" -> trim(label("User Type" ,boolean())), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName")))) - )(EditUserForm.apply) - - val newGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))) - )(NewGroupForm.apply) - - val editGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean())) - )(EditGroupForm.apply) - - get("/admin/users")(adminOnly { - val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) - val users = getAllUsers(includeRemoved) - val members = users.collect { case account if(account.groupAccount) => - account.userName -> getGroupMembers(account.userName).map(_.userName) - }.toMap - - html.list(users, members, includeRemoved) - }) - - get("/admin/users/_newuser")(adminOnly { - html.user(None) - }) - - post("/admin/users/_newuser", newUserForm)(adminOnly { form => - createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url) - updateImage(form.userName, form.fileId, false) - redirect("/admin/users") - }) - - get("/admin/users/:userName/_edituser")(adminOnly { - val userName = params("userName") - html.user(getAccountByUserName(userName, true)) - }) - - post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => - val userName = params("userName") - getAccountByUserName(userName, true).map { account => - - if(form.isRemoved){ - // Remove repositories -// getRepositoryNamesOfUser(userName).foreach { repositoryName => -// deleteRepository(userName, repositoryName) -// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) -// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) -// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) -// } - // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY - removeUserRelatedData(userName) - } - - updateAccount(account.copy( - password = form.password.map(sha1).getOrElse(account.password), - fullName = form.fullName, - mailAddress = form.mailAddress, - administrator = form.isAdmin, - url = form.url, - removed = form.isRemoved)) - - updateImage(userName, form.fileId, form.clearImage) - redirect("/admin/users") - - } getOrElse NotFound - }) - - get("/admin/users/_newgroup")(adminOnly { - html.group(None, Nil) - }) - - post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => - createGroup(form.groupName, form.url) - updateGroupMembers(form.groupName, form.members.split(",").map { - _.split(":") match { - case Array(userName, isManager) => (userName, isManager.toBoolean) - } - }.toList) - updateImage(form.groupName, form.fileId, false) - redirect("/admin/users") - }) - - get("/admin/users/:groupName/_editgroup")(adminOnly { - defining(params("groupName")){ groupName => - html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName)) - } - }) - - post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => - defining(params("groupName"), form.members.split(",").map { - _.split(":") match { - case Array(userName, isManager) => (userName, isManager.toBoolean) - } - }.toList){ case (groupName, members) => - getAccountByUserName(groupName, true).map { account => - updateGroup(groupName, form.url, form.isRemoved) - - if(form.isRemoved){ - // Remove from GROUP_MEMBER - updateGroupMembers(form.groupName, Nil) - // Remove repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - deleteRepository(groupName, repositoryName) - FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) - FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) - FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) - } - } else { - // Update GROUP_MEMBER - updateGroupMembers(form.groupName, members) - // Update COLLABORATOR for group repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - removeCollaborators(form.groupName, repositoryName) - members.foreach { case (userName, isManager) => - addCollaborator(form.groupName, repositoryName, userName) - } - } - } - - updateImage(form.groupName, form.fileId, form.clearImage) - redirect("/admin/users") - - } getOrElse NotFound - } - }) - - private def members: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = { - if(value.split(",").exists { - _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } - }) None else Some("Must select one manager at least.") - } - } - - protected def disableByNotYourself(paramName: String): Constraint = new Constraint() { - override def validate(name: String, value: String, messages: Messages): Option[String] = { - params.get(paramName).flatMap { userName => - if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true")) - Some("You can't disable your account yourself") - else - None - } - } - } -} diff --git a/src/main/scala/gitbucket/core/model/WebHook.scala b/src/main/scala/gitbucket/core/model/WebHook.scala index 3889c000c..9be55a831 100644 --- a/src/main/scala/gitbucket/core/model/WebHook.scala +++ b/src/main/scala/gitbucket/core/model/WebHook.scala @@ -7,7 +7,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile => class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { val url = column[String]("URL") - def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply) + val token = column[Option[String]]("TOKEN", O.Nullable) + def * = (userName, repositoryName, url, token) <> ((WebHook.apply _).tupled, WebHook.unapply) def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) } @@ -16,7 +17,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile => case class WebHook( userName: String, repositoryName: String, - url: String + url: String, + token: Option[String] ) object WebHook { diff --git a/src/main/scala/gitbucket/core/service/HandleCommentService.scala b/src/main/scala/gitbucket/core/service/HandleCommentService.scala new file mode 100644 index 000000000..6a1b06869 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/HandleCommentService.scala @@ -0,0 +1,91 @@ +package gitbucket.core.service + +import gitbucket.core.controller.Context +import gitbucket.core.model.Issue +import gitbucket.core.model.Profile._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.Notifier +import profile.simple._ + +trait HandleCommentService { + self: RepositoryService with IssuesService with ActivityService + with WebHookService with WebHookIssueCommentService with WebHookPullRequestService => + + /** + * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] + */ + def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String]) + (implicit context: Context, s: Session) = { + + defining(repository.owner, repository.name){ case (owner, name) => + val userName = context.loginAccount.get.userName + + val (action, recordActivity) = actionOpt + .collect { + case "close" if(!issue.closed) => true -> + (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) + case "reopen" if(issue.closed) => false -> + (Some("reopen") -> Some(recordReopenIssueActivity _)) + } + .map { case (closed, t) => + updateClosed(owner, name, issue.issueId, closed) + t + } + .getOrElse(None -> None) + + val commentId = (content, action) match { + case (None, None) => None + case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action)) + case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment"))) + } + + // record comment activity if comment is entered + content foreach { + (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) + (owner, name, userName, issue.issueId, _) + } + recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) ) + + // extract references and create refer comment + content.map { content => + createReferComment(owner, name, issue, content, context.loginAccount.get) + } + + // call web hooks + action match { + case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) } + case Some(act) => val webHookAction = act match { + case "open" => "opened" + case "reopen" => "reopened" + case "close" => "closed" + case _ => act + } + if(issue.isPullRequest){ + callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get) + } else { + callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get) + } + } + + // notifications + Notifier() match { + case f => + content foreach { + f.toNotify(repository, issue, _){ + Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}") + } + } + action foreach { + f.toNotify(repository, issue, _){ + Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}") + } + } + } + + commentId.map( issue -> _ ) + } + } + +} diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 82c8c588e..86430a97e 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -1,6 +1,8 @@ package gitbucket.core.service import gitbucket.core.model.Profile._ +import gitbucket.core.util.JGitUtil.CommitInfo +import gitbucket.core.util.StringUtil import profile.simple._ import gitbucket.core.util.StringUtil._ @@ -12,6 +14,7 @@ import Q.interpolation trait IssuesService { + self: AccountService => import IssuesService._ def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = @@ -394,6 +397,29 @@ trait IssuesService { } } } + + def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String, loginAccount: Account)(implicit s: Session) = { + 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, loginAccount.userName, issueId.toInt, content, "refer") + } + } + } + } + + def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session) = { + StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => + if(getIssue(owner, repository, issueId).isDefined){ + getAccountByMailAddress(commit.committerEmailAddress).foreach { account => + createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") + } + } + } + } + } object IssuesService { diff --git a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala new file mode 100644 index 000000000..12afb35e9 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala @@ -0,0 +1,79 @@ +package gitbucket.core.service + +import gitbucket.core.model.Profile._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.JGitUtil +import gitbucket.core.model.Account +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.dircache.DirCache +import org.eclipse.jgit.lib.{FileMode, Constants} +import profile.simple._ + +trait RepositoryCreationService { + self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService => + + def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) + (implicit s: Session) { + val ownerAccount = getAccountByUserName(owner).get + val loginUserName = loginAccount.userName + + // Insert to the database at first + insertRepository(name, owner, description, isPrivate) + + // Add collaborators for group repository + if(ownerAccount.groupAccount){ + 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) + } + + def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = { + createLabel(userName, repositoryName, "bug", "fc2929") + createLabel(userName, repositoryName, "duplicate", "cccccc") + createLabel(userName, repositoryName, "enhancement", "84b6eb") + createLabel(userName, repositoryName, "invalid", "e6e6e6") + createLabel(userName, repositoryName, "question", "cc317c") + createLabel(userName, repositoryName, "wontfix", "ffffff") + } + + +} diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index d1b226156..70a877cf3 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -19,7 +19,7 @@ trait RepositoryService { self: AccountService => * @param originRepositoryName specify for the forked repository. (default is None) * @param originUserName specify for the forked repository. (default is None) */ - def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, + def insertRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, originRepositoryName: Option[String] = None, originUserName: Option[String] = None, parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None) (implicit s: Session): Unit = { diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index d04d90bb9..4af5ec3cc 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -1,8 +1,13 @@ package gitbucket.core.service +import java.io.ByteArrayInputStream + +import fr.brouillard.oss.security.xhub.XHub +import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter} import gitbucket.core.api._ import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment} import gitbucket.core.model.Profile._ +import org.apache.http.client.utils.URLEncodedUtils import profile.simple._ import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.RepositoryName @@ -33,8 +38,11 @@ trait WebHookService { /** get All WebHook informations of repository event */ def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = - WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind) - .list.map(t => WebHook(t.userName, t.repositoryName, t.url)) + WebHooks.filter(_.byRepository(owner, repository)) + .innerJoin(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) } + .filter{ case (wh, whe) => whe.event === event.bind} + .map{ case (wh, whe) => wh } + .list.distinct /** get All WebHook information from repository to url */ def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] = @@ -44,14 +52,15 @@ trait WebHookService { .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])(implicit s: Session): Unit = { - WebHooks insert WebHook(owner, repository, url) + def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = { + WebHooks insert WebHook(owner, repository, url, token) events.toSet.map{ event: WebHook.Event => WebHookEvents insert WebHookEvent(owner, repository, url, event) } } - def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { + def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = { + WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => w.token).update(token) WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete events.toSet.map{ event: WebHook.Event => WebHookEvents insert WebHookEvent(owner, repository, url, event) @@ -69,17 +78,17 @@ trait WebHookService { } } - def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload) + 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 org.apache.http.protocol.HttpContext import org.apache.http.client.methods.HttpPost - if(webHookURLs.nonEmpty){ + if(webHooks.nonEmpty){ val json = JsonFormat(payload) - webHookURLs.map { webHookUrl => + webHooks.map { webHook => val reqPromise = Promise[HttpRequest] val f = Future { val itcp = new org.apache.http.HttpRequestInterceptor{ @@ -89,19 +98,26 @@ trait WebHookService { } try{ val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build - logger.debug(s"start web hook invocation for ${webHookUrl.url}") - val httpPost = new HttpPost(webHookUrl.url) + logger.debug(s"start web hook invocation for ${webHook.url}") + val httpPost = new HttpPost(webHook.url) httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded") httpPost.addHeader("X-Github-Event", event.name) httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString) val params: java.util.List[NameValuePair] = new java.util.ArrayList() params.add(new BasicNameValuePair("payload", json)) - httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")) + def postContent = new UrlEncodedFormEntity(params, "UTF-8") + httpPost.setEntity(postContent) + + if (!webHook.token.isEmpty) { + // TODO find a better way and see how to extract content from postContent + val contentAsBytes = URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8") + httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, contentAsBytes)) + } val res = httpClient.execute(httpPost) httpPost.releaseConnection() - logger.debug(s"end web hook invocation for ${webHookUrl}") + logger.debug(s"end web hook invocation for ${webHook}") res }catch{ case e:Throwable => { @@ -113,12 +129,12 @@ trait WebHookService { } } f.onSuccess { - case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}") + case s => logger.debug(s"Success: web hook request to ${webHook.url}") } f.onFailure { - case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t) + case t => logger.error(s"Failed: web hook request to ${webHook.url}", t) } - (webHookUrl, json, reqPromise.future, f) + (webHook, json, reqPromise.future, f) } } else { Nil diff --git a/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala b/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala index 8cef2e836..ff45a32b2 100644 --- a/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala +++ b/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala @@ -21,6 +21,7 @@ object AutoUpdate { * The history of versions. A head of this sequence is the current GitBucket version. */ val versions = Seq( + new Version(3, 13), new Version(3, 12), new Version(3, 11), new Version(3, 10), diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index 0583f61eb..047e5ed18 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -10,7 +10,6 @@ import gitbucket.core.service.WebHookService._ import gitbucket.core.service._ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Implicits._ -import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util._ import org.eclipse.jgit.api.Git @@ -168,7 +167,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) { if (issueCount > 0) { pushedIds.add(commit.id) - createIssueComment(commit) + createIssueComment(owner, repository, commit) // close issues if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){ closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository) @@ -230,13 +229,4 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: } } - private def createIssueComment(commit: CommitInfo) = { - StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => - if(getIssue(owner, repository, issueId).isDefined){ - getAccountByMailAddress(commit.committerEmailAddress).foreach { account => - createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") - } - } - } - } } diff --git a/src/main/twirl/gitbucket/core/admin/users/user.scala.html b/src/main/twirl/gitbucket/core/admin/user.scala.html similarity index 100% rename from src/main/twirl/gitbucket/core/admin/users/user.scala.html rename to src/main/twirl/gitbucket/core/admin/user.scala.html diff --git a/src/main/twirl/gitbucket/core/admin/users/group.scala.html b/src/main/twirl/gitbucket/core/admin/usergroup.scala.html similarity index 100% rename from src/main/twirl/gitbucket/core/admin/users/group.scala.html rename to src/main/twirl/gitbucket/core/admin/usergroup.scala.html diff --git a/src/main/twirl/gitbucket/core/admin/users/list.scala.html b/src/main/twirl/gitbucket/core/admin/userlist.scala.html similarity index 100% rename from src/main/twirl/gitbucket/core/admin/users/list.scala.html rename to src/main/twirl/gitbucket/core/admin/userlist.scala.html diff --git a/src/main/twirl/gitbucket/core/helper/attached.scala.html b/src/main/twirl/gitbucket/core/helper/attached.scala.html index f8af46bab..789d3d10d 100644 --- a/src/main/twirl/gitbucket/core/helper/attached.scala.html +++ b/src/main/twirl/gitbucket/core/helper/attached.scala.html @@ -27,12 +27,6 @@ $(function(){ throw e; } } - - // Adjust clickable area width - $('#@textareaId').next('div.clickable').css({ - 'width': ($('#@textareaId').width() + 18) + 'px', - 'font-size': '13px' - }); }); } diff --git a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html index 8c6369231..8b791472d 100644 --- a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html +++ b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html @@ -138,7 +138,7 @@ @avatar(comment.commentedUserName, 16) @user(comment.commentedUserName, styleClass="username strong") - close @issueOrPullRequest() + closed this @issueOrPullRequest() @helper.html.datetimeago(comment.registeredDate) @@ -210,7 +210,7 @@ $(function(){ $(document).on('click', '.commit-comment-box i.octicon-pencil', function(){ var id = $(this).closest('a').data('comment-id'); var url = '@url(repository)/commit_comments/_data/' + id; - var $content = $('.commit-commentContent-' + id, $(this).closest('.box')); + var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box')); $.get(url, { diff --git a/src/main/twirl/gitbucket/core/issues/create.scala.html b/src/main/twirl/gitbucket/core/issues/create.scala.html index 39665d1ef..97ab7ac5d 100644 --- a/src/main/twirl/gitbucket/core/issues/create.scala.html +++ b/src/main/twirl/gitbucket/core/issues/create.scala.html @@ -14,7 +14,7 @@