diff --git a/README.md b/README.md index a902ad6..e229137 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://travis-ci.org/takezoe/gitbucket.svg?branch=master)](https://travis-ci.org/takezoe/gitbucket) ========= -GitBucket is the easily installable Github clone written with Scala. +GitBucket is the easily installable GitHub clone powered by Scala. Features @@ -79,6 +79,13 @@ Release Notes -------- +### 2.8 - 1 Feb 2015 +- New logo and icons +- New system setting options to control visibility +- Comment on side-by-side diff +- Information message on sign-in page +- Fork repository by group account + ### 2.7 - 29 Dec 2014 - Comment for commit and diff - Fix security issue in markdown rendering diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index 90fe415..bdbd898 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -14,6 +14,7 @@ context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") // Register controllers + context.mount(new AnonymousAccessController, "/*") context.mount(new IndexController, "/") context.mount(new SearchController, "/") context.mount(new FileUploadController, "/upload") diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index 732e18d..8311654 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -135,8 +135,7 @@ get("/:userName/_avatar"){ val userName = params("userName") getAccountByUserName(userName).flatMap(_.image).map { image => - contentType = FileUtil.getMimeType(image) - new java.io.File(getUserUploadDir(userName), image) + RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image)) } getOrElse { contentType = "image/png" Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png") diff --git a/src/main/scala/app/AnonymousAccessController.scala b/src/main/scala/app/AnonymousAccessController.scala new file mode 100644 index 0000000..35481ab --- /dev/null +++ b/src/main/scala/app/AnonymousAccessController.scala @@ -0,0 +1,14 @@ +package app + +class AnonymousAccessController extends AnonymousAccessControllerBase + +trait AnonymousAccessControllerBase extends ControllerBase { + get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) { + if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") && + !context.currentPath.startsWith("/register")) { + Unauthorized() + } else { + pass() + } + } +} diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala index c31b90f..8a184f0 100644 --- a/src/main/scala/app/ControllerBase.scala +++ b/src/main/scala/app/ControllerBase.scala @@ -134,6 +134,18 @@ if (path.startsWith("http")) path else baseUrl + super.url(path, params, false, false, false) + /** + * Use this method to response the raw data against XSS. + */ + protected def RawData[T](contentType: String, rawData: T): T = { + if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){ + this.contentType = "text/plain" + } else { + this.contentType = contentType + } + response.addHeader("X-Content-Type-Options", "nosniff") + rawData + } } /** diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index 9f6efcf..063b8b2 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -292,8 +292,7 @@ (Directory.getAttachedDir(repository.owner, repository.name) match { case dir if(dir.exists && dir.isDirectory) => dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => - contentType = FileUtil.getMimeType(file.getName) - file + RawData(FileUtil.getMimeType(file.getName), file) } case _ => None }) getOrElse NotFound diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 00f6755..57b1027 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -21,7 +21,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService /** @@ -29,7 +29,7 @@ */ trait RepositoryViewerControllerBase extends ControllerBase { self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService => ArchiveCommand.registerFormat("zip", new ZipFormat) ArchiveCommand.registerFormat("tar.gz", new TgzFormat) @@ -214,8 +214,7 @@ if(raw){ // Download defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => - contentType = FileUtil.getContentType(path, bytes) - bytes + RawData(FileUtil.getContentType(path, bytes), bytes) } } else { repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), @@ -496,6 +495,9 @@ //refUpdate.setRefLogMessage("merged", true) refUpdate.update() + // update pull request + updatePullRequests(repository.owner, repository.name, branch) + // record activity recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)))) diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala index 5c11959..e278433 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -16,6 +16,7 @@ "baseUrl" -> trim(label("Base URL", optional(text()))), "information" -> trim(label("Information", optional(text()))), "allowAccountRegistration" -> trim(label("Account registration", boolean())), + "allowAnonymousAccess" -> trim(label("Anonymous access", boolean())), "isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())), "gravatar" -> trim(label("Gravatar", boolean())), "notification" -> trim(label("Notification", boolean())), diff --git a/src/main/scala/app/WikiController.scala b/src/main/scala/app/WikiController.scala index 5270d03..c273018 100644 --- a/src/main/scala/app/WikiController.scala +++ b/src/main/scala/app/WikiController.scala @@ -164,8 +164,7 @@ val path = multiParams("splat").head getFileContent(repository.owner, repository.name, path).map { bytes => - contentType = FileUtil.getContentType(path, bytes) - bytes + RawData(FileUtil.getContentType(path, bytes), bytes) } getOrElse NotFound }) diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala index 9a3239b..68121f9 100644 --- a/src/main/scala/service/PullRequestService.scala +++ b/src/main/scala/service/PullRequestService.scala @@ -3,6 +3,7 @@ import model.Profile._ import profile.simple._ import model.{PullRequest, Issue} +import util.JGitUtil trait PullRequestService { self: IssuesService => import PullRequestService._ @@ -81,6 +82,18 @@ .map { case (t1, t2) => t1 } .list + /** + * Fetch pull request contents into refs/pull/${issueId}/head and update pull request table. + */ + def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit = + getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => + if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){ + val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest( + pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.issueId, + pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch) + updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom) + } + } } object PullRequestService { diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala index 6894125..f54291b 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -2,7 +2,7 @@ import model.Profile._ import profile.simple._ -import model.{Repository, Account, Collaborator} +import model.{Repository, Account, Collaborator, Label} import util.JGitUtil trait RepositoryService { self: AccountService => @@ -94,9 +94,17 @@ PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + // Convert labelId + val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap + val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap + IssueLabels.insertAll(issueLabels.map(x => x.copy( + labelId = newLabelMap(oldLabelMap(x.labelId)), + userName = newUserName, + repositoryName = newRepositoryName + )) :_*) + if(account.isGroupAccount){ Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*) } else { diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala index 7575094..156a23b 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -14,6 +14,7 @@ settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) settings.information.foreach(x => props.setProperty(Information, x)) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) + props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString) props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString) props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(Notification, settings.notification.toString) @@ -65,6 +66,7 @@ getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue[String](props, Information, None), getValue(props, AllowAccountRegistration, false), + getValue(props, AllowAnonymousAccess, true), getValue(props, IsCreateRepoOptionPublic, true), getValue(props, Gravatar, true), getValue(props, Notification, false), @@ -113,6 +115,7 @@ baseUrl: Option[String], information: Option[String], allowAccountRegistration: Boolean, + allowAnonymousAccess: Boolean, isCreateRepoOptionPublic: Boolean, gravatar: Boolean, notification: Boolean, @@ -158,6 +161,7 @@ private val BaseURL = "base_url" private val Information = "information" private val AllowAccountRegistration = "allow_account_registration" + private val AllowAnonymousAccess = "allow_anonymous_access" private val IsCreateRepoOptionPublic = "is_create_repository_option_public" private val Gravatar = "gravatar" private val Notification = "notification" diff --git a/src/main/scala/servlet/BasicAuthenticationFilter.scala b/src/main/scala/servlet/BasicAuthenticationFilter.scala index 8272c7a..cbecfc1 100644 --- a/src/main/scala/servlet/BasicAuthenticationFilter.scala +++ b/src/main/scala/servlet/BasicAuthenticationFilter.scala @@ -28,33 +28,45 @@ override def setCharacterEncoding(encoding: String) = {} } + val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString) + + val settings = loadSystemSettings() + try { - defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) => - getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { - case Some(repository) => { - if(!request.getRequestURI.endsWith("/git-receive-pack") && - !"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){ - chain.doFilter(req, wrappedResponse) - } else { - request.getHeader("Authorization") match { - case null => requireAuth(response) - case auth => decodeAuthHeader(auth).split(":") match { - case Array(username, password) => getWritableUser(username, password, repository) match { - case Some(account) => { - request.setAttribute(Keys.Request.UserName, account.userName) - chain.doFilter(req, wrappedResponse) + defining(request.paths){ + case Array(_, repositoryOwner, repositoryName, _*) => + getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { + case Some(repository) => { + if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){ + chain.doFilter(req, wrappedResponse) + } else { + request.getHeader("Authorization") match { + case null => requireAuth(response) + case auth => decodeAuthHeader(auth).split(":") match { + case Array(username, password) => { + authenticate(settings, username, password) match { + case Some(account) => { + if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){ + request.setAttribute(Keys.Request.UserName, account.userName) + } + chain.doFilter(req, wrappedResponse) + } + case None => requireAuth(response) + } } - case None => requireAuth(response) + case _ => requireAuth(response) } - case _ => requireAuth(response) } } } + case None => { + logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.") + response.sendError(HttpServletResponse.SC_NOT_FOUND) + } } - case None => { - logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.") - response.sendError(HttpServletResponse.SC_NOT_FOUND) - } + case _ => { + logger.debug(s"Not enough path arguments: ${request.paths}") + response.sendError(HttpServletResponse.SC_NOT_FOUND) } } } catch { @@ -65,13 +77,6 @@ } } - private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo) - (implicit session: Session): Option[Account] = - authenticate(loadSystemSettings(), username, password) match { - case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x - case _ => None - } - private def requireAuth(response: HttpServletResponse): Unit = { response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"") response.sendError(HttpServletResponse.SC_UNAUTHORIZED) diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala index df55d46..7fde407 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -174,7 +174,7 @@ case ReceiveCommand.Type.CREATE | ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD => - updatePullRequests(branchName) + updatePullRequests(owner, repository, branchName) case _ => } } @@ -211,26 +211,4 @@ } } } - - /** - * Fetch pull request contents into refs/pull/${issueId}/head and update pull request table. - */ - private def updatePullRequests(branch: String) = - getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => - if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){ - using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)), - Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) => - oldGit.fetch - .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString) - .setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true)) - .call - - val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName - val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit, - pullreq.userName, pullreq.repositoryName, pullreq.branch, - pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch) - updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom) - } - } - } } diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index b5e2e3b..0f76f1e 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -13,6 +13,7 @@ import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.diff.DiffEntry.ChangeType import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} +import org.eclipse.jgit.transport.RefSpec import java.util.Date import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException} import service.RepositoryService @@ -675,6 +676,25 @@ } /** + * Fetch pull request contents into refs/pull/${issueId}/head and return (commitIdTo, commitIdFrom) + */ + def updatePullRequest(userName: String, repositoryName:String, branch: String, issueId: Int, + requestUserName: String, requestRepositoryName: String, requestBranch: String):(String, String) = + using(Git.open(Directory.getRepositoryDir(userName, repositoryName)), + Git.open(Directory.getRepositoryDir(requestUserName, requestRepositoryName))){ (oldGit, newGit) => + oldGit.fetch + .setRemote(Directory.getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString) + .setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head").setForceUpdate(true)) + .call + + val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${issueId}/head").getName + val commitIdFrom = getForkedCommitId(oldGit, newGit, + userName, repositoryName, branch, + requestUserName, requestRepositoryName, requestBranch) + (commitIdTo, commitIdFrom) + } + + /** * Returns the last modified commit of specified path * @param git the Git object * @param startCommit the search base commit id diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index 87719b4..836e5c9 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -195,6 +195,32 @@ printTag(node, "li") } } + + override def visit(node: ExpLinkNode) { + printLink(linkRenderer.render(node, printLinkChildrenToString(node))) + } + + def printLinkChildrenToString(node: SuperNode) = { + val priorPrinter = printer + printer = new Printer() + visitLinkChildren(node) + val result = printer.getString() + printer = priorPrinter + result + } + + def visitLinkChildren(node: SuperNode) { + import scala.collection.JavaConversions._ + node.getChildren.foreach(child => child match { + case node: ExpImageNode => visitLinkChild(node) + case node: SuperNode => visitLinkChildren(node) + case _ => child.accept(this) + }) + } + + def visitLinkChild(node: ExpImageNode) { + printer.print("\"").printEncoded(printChildrenToString(node)).print("\"/") + } } object GitBucketHtmlSerializer { diff --git a/src/main/twirl/admin/system.scala.html b/src/main/twirl/admin/system.scala.html index 09c8ac8..194a164 100644 --- a/src/main/twirl/admin/system.scala.html +++ b/src/main/twirl/admin/system.scala.html @@ -66,6 +66,21 @@ + + +
+ +
+ + +
+
diff --git a/src/main/twirl/main.scala.html b/src/main/twirl/main.scala.html index 154431d..9b8ae02 100644 --- a/src/main/twirl/main.scala.html +++ b/src/main/twirl/main.scala.html @@ -6,7 +6,7 @@ @title - + diff --git a/src/main/twirl/search/code.scala.html b/src/main/twirl/search/code.scala.html index 04d0a19..cc45665 100644 --- a/src/main/twirl/search/code.scala.html +++ b/src/main/twirl/search/code.scala.html @@ -16,7 +16,7 @@ @files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
@file.path
-
Last commited @helper.html.datetimeago(file.lastModified)
+
Last committed @helper.html.datetimeago(file.lastModified)
@Html(file.highlightText)
} diff --git a/src/main/webapp/assets/common/images/favicon.png b/src/main/webapp/assets/common/images/favicon.png deleted file mode 100644 index 332b6ce..0000000 --- a/src/main/webapp/assets/common/images/favicon.png +++ /dev/null Binary files differ diff --git a/src/main/webapp/assets/common/images/gitbucket.png b/src/main/webapp/assets/common/images/gitbucket.png index fad9a32..945d93a 100644 --- a/src/main/webapp/assets/common/images/gitbucket.png +++ b/src/main/webapp/assets/common/images/gitbucket.png Binary files differ diff --git a/src/test/scala/view/AvatarImageProviderSpec.scala b/src/test/scala/view/AvatarImageProviderSpec.scala index 7e9ecc2..78c32ab 100644 --- a/src/test/scala/view/AvatarImageProviderSpec.scala +++ b/src/test/scala/view/AvatarImageProviderSpec.scala @@ -95,6 +95,7 @@ baseUrl = None, information = None, allowAccountRegistration = false, + allowAnonymousAccess = true, isCreateRepoOptionPublic = true, gravatar = useGravatar, notification = false,