Merge remote-tracking branch 'upstream/master' into pr-add-release

# Conflicts:
#	build.sbt
#	src/main/scala/ScalatraBootstrap.scala
#	src/main/scala/gitbucket/core/GitBucketCoreModule.scala
#	src/main/scala/gitbucket/core/controller/FileUploadController.scala
#	src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala
#	src/main/scala/gitbucket/core/service/RepositoryService.scala
#	src/main/twirl/gitbucket/core/menu.scala.html
This commit is contained in:
KOUNOIKE Yuusuke
2018-01-07 13:22:56 +09:00
384 changed files with 28722 additions and 9485 deletions

View File

@@ -1,4 +1,9 @@
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
@@ -8,6 +13,8 @@ import java.security.ProtectionDomain;
public class JettyLauncher {
public static void main(String[] args) throws Exception {
System.setProperty("java.awt.headless", "true");
String host = null;
int port = 8080;
InetSocketAddress address = null;
@@ -32,12 +39,21 @@ public class JettyLauncher {
contextPath = "/" + contextPath;
}
break;
case "--max_file_size":
System.setProperty("gitbucket.maxFileSize", dim[1]);
break;
case "--gitbucket.home":
System.setProperty("gitbucket.home", dim[1]);
break;
case "--temp_dir":
tmpDirPath = dim[1];
break;
case "--plugin_dir":
System.setProperty("gitbucket.pluginDir", dim[1]);
break;
case "--validate_password":
System.setProperty("gitbucket.validate.password", dim[1]);
break;
}
}
}
@@ -60,6 +76,15 @@ public class JettyLauncher {
// connector.setPort(port);
// server.addConnector(connector);
// Disabling Server header
for (Connector connector : server.getConnectors()) {
for (ConnectionFactory factory : connector.getConnectionFactories()) {
if (factory instanceof HttpConnectionFactory) {
((HttpConnectionFactory) factory).getHttpConfiguration().setSendServerVersion(false);
}
}
}
WebAppContext context = new WebAppContext();
File tmpDir;
@@ -80,6 +105,9 @@ public class JettyLauncher {
}
context.setTempDirectory(tmpDir);
// Disabling the directory listing feature.
context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation();
@@ -91,7 +119,9 @@ public class JettyLauncher {
context.setInitParameter("org.scalatra.ForceHttps", "true");
}
server.setHandler(context);
Handler handler = addStatisticsHandler(context);
server.setHandler(handler);
server.setStopAtShutdown(true);
server.setStopTimeout(7_000);
server.start();
@@ -110,14 +140,11 @@ public class JettyLauncher {
return new File(System.getProperty("user.home"), ".gitbucket");
}
private static void deleteDirectory(File dir){
for(File file: dir.listFiles()){
if(file.isFile()){
file.delete();
} else if(file.isDirectory()){
deleteDirectory(file);
}
}
dir.delete();
private static Handler addStatisticsHandler(Handler handler) {
// The graceful shutdown is implemented via the statistics handler.
// See the following: https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142
final StatisticsHandler statisticsHandler = new StatisticsHandler();
statisticsHandler.setHandler(handler);
return statisticsHandler;
}
}

View File

@@ -0,0 +1,26 @@
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT,
COALESCE(D.ORDERING, 9999) AS PRIORITY
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) C
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID)
LEFT OUTER JOIN PRIORITY D
ON (A.PRIORITY_ID = D.PRIORITY_ID);

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<createTable tableName="PRIORITY">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="PRIORITY_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="PRIORITY_NAME" type="varchar(100)" nullable="false"/>
<column name="DESCRIPTION" type="varchar(255)" nullable="true"/>
<column name="ORDERING" type="int" nullable="false"/>
<column name="IS_DEFAULT" type="boolean" nullable="false"/>
<column name="COLOR" type="char(6)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_PRIORITY_PK" tableName="PRIORITY" columnNames="USER_NAME, REPOSITORY_NAME, PRIORITY_ID"/>
<addForeignKeyConstraint constraintName="IDX_PRIORITY_FK0" baseTableName="PRIORITY" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<addColumn tableName="ISSUE">
<column name="PRIORITY_ID" type="int" nullable="true" />
</addColumn>
<addForeignKeyConstraint constraintName="IDX_ISSUE_FK3" baseTableName="ISSUE" baseColumnNames="PRIORITY_ID" referencedTableName="PRIORITY" referencedColumnNames="PRIORITY_ID"/>
<createTable tableName="ACCOUNT_WEB_HOOK">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="URL" type="varchar(200)" nullable="false"/>
<column name="TOKEN" type="varchar(100)" nullable="true"/>
<column name="CTYPE" type="varchar(10)" nullable="true"/>
</createTable>
<addPrimaryKey constraintName="IDX_ACCOUNT_WEB_HOOK_PK" tableName="ACCOUNT_WEB_HOOK" columnNames="USER_NAME, URL"/>
<addForeignKeyConstraint constraintName="IDX_ACCOUNT_WEB_HOOK_FK0" baseTableName="ACCOUNT_WEB_HOOK" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
<createTable tableName="ACCOUNT_WEB_HOOK_EVENT">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="URL" type="varchar(200)" nullable="false"/>
<column name="EVENT" type="varchar(30)" nullable="false"/>
</createTable>
</changeSet>

View File

@@ -2,7 +2,7 @@
import java.util.EnumSet
import javax.servlet._
import gitbucket.core.controller._
import gitbucket.core.controller.{ReleaseController, _}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.servlet._
@@ -25,30 +25,33 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter)
context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
context.addFilter("ghCompatRepositoryAccessFilter", new GHCompatRepositoryAccessFilter)
context.getFilterRegistration("ghCompatRepositoryAccessFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
// Register controllers
context.mount(new AnonymousAccessController, "/*")
context.mount(new PreProcessController, "/*")
PluginRegistry().getControllers.foreach { case (controller, path) =>
context.mount(controller, path)
}
context.addFilter("pluginControllerFilter", new PluginControllerFilter)
context.getFilterRegistration("pluginControllerFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
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 AccountController, "/*")
context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*")
context.mount(new LabelsController, "/*")
context.mount(new MilestonesController, "/*")
context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*")
context.mount(new ReleaseController, "/*")
context.mount(new RepositorySettingsController, "/*")
val filter = new CompositeScalatraFilter()
filter.mount(new IndexController, "/")
filter.mount(new ApiController, "/api/v3")
filter.mount(new SystemSettingsController, "/admin")
filter.mount(new DashboardController, "/*")
filter.mount(new AccountController, "/*")
filter.mount(new RepositoryViewerController, "/*")
filter.mount(new WikiController, "/*")
filter.mount(new LabelsController, "/*")
filter.mount(new PrioritiesController, "/*")
filter.mount(new MilestonesController, "/*")
filter.mount(new IssuesController, "/*")
filter.mount(new PullRequestsController, "/*")
filter.mount(new ReleaseController, "/*")
filter.mount(new RepositorySettingsController, "/*")
context.addFilter("compositeScalatraFilter", filter)
context.getFilterRegistration("compositeScalatraFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
// Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(Directory.GitBucketHome)

View File

@@ -32,7 +32,24 @@ object GitBucketCoreModule extends Module("gitbucket-core",
new Version("4.11.0",
new LiquibaseMigration("update/gitbucket-core_4.11.xml")
),
new Version("4.12.0",
new Version("4.12.0"),
new Version("4.12.1"),
new Version("4.13.0"),
new Version("4.14.0",
new LiquibaseMigration("update/gitbucket-core_4.14.xml"),
new SqlMigration("update/gitbucket-core_4.14.sql")
),
new Version("4.14.1"),
new Version("4.15.0"),
new Version("4.16.0"),
new Version("4.17.0"),
new Version("4.18.0"),
new Version("4.19.0"),
new Version("4.19.1"),
new Version("4.19.2"),
new Version("4.19.3"),
new Version("4.20.0"),
new Version("4.21.0",
new LiquibaseMigration("update/gitbucket-core_4.12.xml")
)
)

View File

@@ -35,23 +35,23 @@ case class ApiCommit(
object ApiCommit{
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo, urlIsHtmlUrl: Boolean = false): ApiCommit = {
val diffs = JGitUtil.getDiffs(git, commit.id, false)
val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false)
ApiCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.commitTime,
added = diffs._1.collect {
added = diffs.collect {
case x if x.changeType == DiffEntry.ChangeType.ADD => x.newPath
},
removed = diffs._1.collect {
removed = diffs.collect {
case x if x.changeType == DiffEntry.ChangeType.DELETE => x.oldPath
},
modified = diffs._1.collect {
modified = diffs.collect {
case x if x.changeType != DiffEntry.ChangeType.ADD && x.changeType != DiffEntry.ChangeType.DELETE => x.newPath
},
author = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit)
)(repositoryName, urlIsHtmlUrl)
}
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
def forWebhookPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
}

View File

@@ -0,0 +1,124 @@
package gitbucket.core.api
import gitbucket.core.model.Account
import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo}
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import ApiCommits._
case class ApiCommits(
url: ApiPath,
sha: String,
html_url: ApiPath,
comment_url: ApiPath,
commit: Commit,
author: ApiUser,
committer: ApiUser,
parents: Seq[Tree],
stats: Stats,
files: Seq[File]
)
object ApiCommits {
case class Commit(
url: ApiPath,
author: ApiPersonIdent,
committer: ApiPersonIdent,
message: String,
comment_count: Int,
tree: Tree
)
case class Tree(
url: ApiPath,
sha: String
)
case class Stats(
additions: Int,
deletions: Int,
total: Int
)
case class File(
filename: String,
additions: Int,
deletions: Int,
changes: Int,
status: String,
raw_url: ApiPath,
blob_url: ApiPath,
patch: String
)
def apply(repositoryName: RepositoryName, commitInfo: CommitInfo, diffs: Seq[DiffInfo], author: Account, committer: Account,
commentCount: Int): ApiCommits = {
val files = diffs.map { diff =>
var additions = 0
var deletions = 0
diff.patch.getOrElse("").split("\n").foreach { line =>
if(line.startsWith("+")) additions = additions + 1
if(line.startsWith("-")) deletions = deletions + 1
}
File(
filename = if(diff.changeType == ChangeType.DELETE){ diff.oldPath } else { diff.newPath },
additions = additions,
deletions = deletions,
changes = additions + deletions,
status = diff.changeType match {
case ChangeType.ADD => "added"
case ChangeType.MODIFY => "modified"
case ChangeType.DELETE => "deleted"
case ChangeType.RENAME => "renamed"
case ChangeType.COPY => "copied"
},
raw_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.parents.head}/${diff.oldPath}")
} else {
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.id}/${diff.newPath}")
},
blob_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.parents.head}/${diff.oldPath}")
} else {
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.id}/${diff.newPath}")
},
patch = diff.patch.getOrElse("")
)
}
ApiCommits(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
sha = commitInfo.id,
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"),
comment_url = ApiPath(""),
commit = Commit(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
author = ApiPersonIdent.author(commitInfo),
committer = ApiPersonIdent.committer(commitInfo),
message = commitInfo.shortMessage,
comment_count = commentCount,
tree = Tree(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${commitInfo.id}"), // TODO This endpoint has not been implemented yet.
sha = commitInfo.id
)
),
author = ApiUser(author),
committer = ApiUser(committer),
parents = commitInfo.parents.map { parent =>
Tree(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${parent}"), // TODO This endpoint has not been implemented yet.
sha = parent
)
},
stats = Stats(
additions = files.map(_.additions).sum,
deletions = files.map(_.deletions).sum,
total = files.map(_.additions).sum + files.map(_.deletions).sum
),
files = files
)
}
}

View File

@@ -1,6 +1,13 @@
package gitbucket.core.api
/**
* path for api url. if set path '/repos/aa/bb' then, expand 'http://server:post/repos/aa/bb' when converted to json.
* Path for API url.
* If set path '/repos/aa/bb' then, expand 'http://server:port/repos/aa/bb' when converted to json.
*/
case class ApiPath(path: String)
/**
* Path for git repository via SSH.
* If set path '/aa/bb.git' then, expand 'git@server:port/aa/bb.git' when converted to json.
*/
case class SshPath(path: String)

View File

@@ -3,7 +3,6 @@ package gitbucket.core.api
import gitbucket.core.model.{Account, Issue, IssueComment, PullRequest}
import java.util.Date
/**
* https://developer.github.com/v3/pulls/
*/
@@ -19,7 +18,8 @@ case class ApiPullRequest(
merged_by: Option[ApiUser],
title: String,
body: String,
user: ApiUser) {
user: ApiUser,
assignee: Option[ApiUser]){
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
@@ -39,6 +39,7 @@ object ApiPullRequest{
headRepo: ApiRepository,
baseRepo: ApiRepository,
user: ApiUser,
assignee: Option[ApiUser],
mergedComment: Option[(IssueComment, Account)]
): ApiPullRequest =
ApiPullRequest(
@@ -59,14 +60,16 @@ object ApiPullRequest{
merged_by = mergedComment.map { case (_, account) => ApiUser(account) },
title = issue.title,
body = issue.content.getOrElse(""),
user = user
user = user,
assignee = assignee
)
case class Commit(
sha: String,
ref: String,
repo: ApiRepository)(baseOwner:String){
val label = if( baseOwner == repo.owner.login ){ ref }else{ s"${repo.owner.login}:${ref}" }
val label = if( baseOwner == repo.owner.login ){ ref } else { s"${repo.owner.login}:${ref}" }
val user = repo.owner
}
}

View File

@@ -24,6 +24,7 @@ case class ApiRepository(
val http_url = ApiPath(s"/git/${full_name}.git")
val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(s":${full_name}.git"))
}
object ApiRepository{
@@ -37,7 +38,7 @@ object ApiRepository{
name = repository.repositoryName,
full_name = s"${repository.userName}/${repository.repositoryName}",
description = repository.description.getOrElse(""),
watchers = 0,
watchers = watchers,
forks = forkedCount,
`private` = repository.isPrivate,
default_branch = repository.defaultBranch,
@@ -50,7 +51,18 @@ object ApiRepository{
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
this(repositoryInfo.repository, ApiUser(owner))
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
def forWebhookPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
def forDummyPayload(owner: ApiUser): ApiRepository =
ApiRepository(
name = "dummy",
full_name = s"${owner.login}/dummy",
description = "",
watchers = 0,
forks = 0,
`private` = false,
default_branch = "master",
owner = owner
)(true)
}

View File

@@ -1,23 +1,24 @@
package gitbucket.core.api
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.format._
import java.time._
import java.time.format.DateTimeFormatter
import java.util.Date
import scala.util.Try
import org.json4s._
import org.json4s.jackson.Serialization
import java.util.Date
import scala.util.Try
object JsonFormat {
case class Context(baseUrl: String)
case class Context(baseUrl: String, sshUrl: Option[String])
val parserISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val parserISO = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format =>
(
{ case JString(s) => Try(parserISO.parseDateTime(s)).toOption.map(_.toDate).getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
{ case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) }
{ case JString(s) => Try(Date.from(Instant.parse(s))).getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
{ case x: Date => JString(OffsetDateTime.ofInstant(x.toInstant, ZoneId.of("UTC")).format(parserISO)) }
)
) + FieldSerializer[ApiUser]() +
FieldSerializer[ApiPullRequest]() +
@@ -33,23 +34,31 @@ object JsonFormat {
FieldSerializer[ApiComment]() +
FieldSerializer[ApiContents]() +
FieldSerializer[ApiLabel]() +
FieldSerializer[ApiCommits]() +
FieldSerializer[ApiCommits.Commit]() +
FieldSerializer[ApiCommits.Tree]() +
FieldSerializer[ApiCommits.Stats]() +
FieldSerializer[ApiCommits.File]() +
ApiBranchProtection.enforcementLevelSerializer
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
(
{
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
},
{
case ApiPath(path) => JString(c.baseUrl + path)
}
)
)
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](_ => ({
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
}, {
case ApiPath(path) => JString(c.baseUrl + path)
}))
def sshPathSerializer(c: Context) = new CustomSerializer[SshPath](_ => ({
case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) => SshPath(s.substring(c.sshUrl.get.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
}, {
case SshPath(path) => c.sshUrl.map { sshUrl => JString(sshUrl + path) } getOrElse JNothing
}))
/**
* convert object to json string
*/
def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
def apply(obj: AnyRef)(implicit c: Context): String =
Serialization.write(obj)(jsonFormats + apiPathSerializer(c) + sshPathSerializer(c))
}

View File

@@ -2,31 +2,30 @@ package gitbucket.core.controller
import gitbucket.core.account.html
import gitbucket.core.helper
import gitbucket.core.model.{GroupMember, Role}
import gitbucket.core.model.{AccountWebHook, GroupMember, RepositoryWebHook, Role, WebHook, WebHookContentType}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service._
import gitbucket.core.service.WebHookService._
import gitbucket.core.ssh.SshUtil
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.scalatra.BadRequest
import org.scalatra.forms._
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 RepositoryCreationService
with AccessTokenService with WebHookService with PrioritiesService 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 RepositoryCreationService =>
with AccessTokenService with WebHookService with PrioritiesService with RepositoryCreationService =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
description: Option[String], url: Option[String], fileId: Option[String])
@@ -40,7 +39,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"password" -> trim(label("Password" , text(required, maxlength(20), password))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"description" -> trim(label("bio" , optional(text()))),
@@ -49,7 +48,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
)(AccountNewForm.apply)
val editForm = mapping(
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"password" -> trim(label("Password" , optional(text(maxlength(20), password)))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"description" -> trim(label("bio" , optional(text()))),
@@ -87,15 +86,16 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"clearImage" -> trim(label("Clear image" ,boolean()))
)(EditGroupForm.apply)
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, initOption: String, sourceUrl: Option[String])
case class ForkRepositoryForm(owner: String, name: String)
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(100), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
"owner" -> trim(label("Owner", text(required, maxlength(100), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))),
"description" -> trim(label("Description", optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"initOption" -> trim(label("Initialize option", text(required))),
"sourceUrl" -> trim(label("Source URL", optionalRequired(_.value("initOption") == "COPY", text())))
)(RepositoryCreationForm.apply)
val forkRepositoryForm = mapping(
@@ -109,6 +109,48 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"account" -> trim(label("Group/User name", text(required, validAccountName)))
)(AccountForm.apply)
// for account web hook url addition.
case class AccountWebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])
def accountWebHookForm(update:Boolean) = mapping(
"url" -> trim(label("url", text(required, accountWebHook(update)))),
"events" -> accountWebhookEvents,
"ctype" -> label("ctype", text()),
"token" -> optional(trim(label("token", text(maxlength(100)))))
)(
(url, events, ctype, token) => AccountWebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
)
/**
* Provides duplication check for web hook url. duplicated from RepositorySettingsController.scala
*/
private def accountWebHook(needExists: Boolean): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountWebHook(params("userName"), value).isDefined != needExists){
Some(if(needExists){
"URL had not been registered yet."
} else {
"URL had been registered already."
})
} else {
None
}
}
private def accountWebhookEvents = new ValueType[Set[WebHook.Event]]{
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t =>
params.optionValue(name + "." + t.name).map(_ => t)
}.toSet
}
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
if(convert(name, params, messages).isEmpty){
Seq(name -> messages("error.required").format(name))
} else {
Nil
}
}
/**
* Displays user information.
*/
@@ -191,6 +233,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} getOrElse NotFound()
})
get("/captures/(.*)".r) {
multiParams("captures").head
}
get("/:userName/_delete")(oneselfOnly {
val userName = params("userName")
@@ -206,9 +252,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
// Remove from GROUP_MEMBER and COLLABORATOR
removeUserRelatedData(userName)
updateAccount(account.copy(isRemoved = true))
// call hooks
PluginRegistry().getAccountHooks.foreach(_.deleted(userName))
session.invalidate
redirect("/")
}
@@ -269,6 +319,113 @@ trait AccountControllerBase extends AccountManagementControllerBase {
redirect(s"/${userName}/_application")
})
get("/:userName/_hooks")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { account =>
gitbucket.core.account.html.hooks(account, getAccountWebHooks(account.userName), flash.get("info"))
} getOrElse NotFound()
})
/**
* Display the account web hook edit page.
*/
get("/:userName/_hooks/new")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { account =>
val webhook = AccountWebHook(userName, "", WebHookContentType.FORM, None)
html.edithook(webhook, Set(WebHook.Push), account, true)
} getOrElse NotFound()
})
/**
* Add the account web hook URL.
*/
post("/:userName/_hooks/new", accountWebHookForm(false))(oneselfOnly { form =>
val userName = params("userName")
addAccountWebHook(userName, form.url, form.events, form.ctype, form.token)
flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${userName}/_hooks")
})
/**
* Delete the account web hook URL.
*/
get("/:userName/_hooks/delete")(oneselfOnly {
val userName = params("userName")
deleteAccountWebHook(userName, params("url"))
flash += "info" -> s"Webhook ${params("url")} deleted"
redirect(s"/${userName}/_hooks")
})
/**
* Display the account web hook edit page.
*/
get("/:userName/_hooks/edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).flatMap { account =>
getAccountWebHook(userName, params("url")).map { case (webhook, events) =>
html.edithook(webhook, events, account, false)
}
} getOrElse NotFound()
})
/**
* Update account web hook settings.
*/
post("/:userName/_hooks/edit", accountWebHookForm(true))(oneselfOnly { form =>
val userName = params("userName")
updateAccountWebHook(userName, form.url, form.events, form.ctype, form.token)
flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${userName}/_hooks")
})
/**
* Send the test request to registered account web hook URLs.
*/
ajaxPost("/:userName/_hooks/test")(oneselfOnly {
// TODO Is it possible to merge with [[RepositorySettingsController.ajaxPost]]?
import scala.concurrent.duration._
import scala.concurrent._
import scala.util.control.NonFatal
import org.apache.http.util.EntityUtils
import scala.concurrent.ExecutionContext.Implicits.global
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => Array(h.getName, h.getValue) }
val userName = params("userName")
val url = params("url")
val token = Some(params("token"))
val ctype = WebHookContentType.valueOf(params("ctype"))
val dummyWebHookInfo = RepositoryWebHook(userName, "dummy", url, ctype, token)
val dummyPayload = {
val ownerAccount = getAccountByUserName(userName).get
WebHookPushPayload.createDummyPayload(ownerAccount)
}
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
val toErrorMap: PartialFunction[Throwable, Map[String,String]] = {
case e: java.net.UnknownHostException => Map("error"-> ("Unknown host " + e.getMessage))
case e: java.lang.IllegalArgumentException => Map("error"-> ("invalid url"))
case e: org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url"))
case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage))
}
contentType = formats("json")
org.json4s.jackson.Serialization.write(Map(
"url" -> url,
"request" -> Await.result(reqFuture.map(req => Map(
"headers" -> _headers(req.getAllHeaders),
"payload" -> json
)).recover(toErrorMap), 20 seconds),
"response" -> Await.result(resFuture.map(res => Map(
"status" -> res.getStatusLine(),
"body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders())
)).recover(toErrorMap), 20 seconds)
))
})
get("/register"){
if(context.settings.allowAccountRegistration){
if(context.loginAccount.isDefined){
@@ -288,7 +445,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
get("/groups/new")(usersOnly {
html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
html.creategroup(List(GroupMember("", context.loginAccount.get.userName, true)))
})
post("/groups/new", newGroupForm)(usersOnly { form =>
@@ -304,7 +461,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:groupName/_editgroup")(managersOnly {
defining(params("groupName")){ groupName =>
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
getAccountByUserName(groupName, true).map { account =>
html.editgroup(account, getGroupMembers(groupName), flash.get("info"))
} getOrElse NotFound()
}
})
@@ -312,13 +471,17 @@ trait AccountControllerBase extends AccountManagementControllerBase {
defining(params("groupName")){ groupName =>
// Remove from GROUP_MEMBER
updateGroupMembers(groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
// Disable group
getAccountByUserName(groupName, false).foreach { account =>
updateGroup(groupName, account.description, account.url, true)
}
// // Remove repositories
// getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
// deleteRepository(groupName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
// }
}
redirect("/")
})
@@ -343,7 +506,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// }
updateImage(form.groupName, form.fileId, form.clearImage)
redirect(s"/${form.groupName}")
flash += "info" -> "Account information has been updated."
redirect(s"/${groupName}/_editgroup")
} getOrElse NotFound()
}
@@ -362,11 +527,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name).isEmpty){
// Create the repository
createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme)
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.created(form.owner, form.name))
createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.initOption, form.sourceUrl)
}
}
@@ -400,59 +561,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val loginUserName = loginAccount.userName
val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists
redirect(s"/${accountName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
insertRepository(
repositoryName = repository.name,
userName = accountName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Set default collaborators for the private fork
if(repository.repository.isPrivate){
// Copy collaborators from the source repository
getCollaborators(repository.owner, repository.name).foreach { case (collaborator, _) =>
addCollaborator(accountName, repository.name, collaborator.collaboratorName, collaborator.role)
}
// Register an owner of the source repository as a collaborator
addCollaborator(accountName, repository.name, repository.owner, Role.ADMIN.name)
}
// Insert default labels
insertDefaultLabels(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(accountName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(accountName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name))
// redirect to the repository
redirect(s"/${accountName}/${repository.name}")
}
if (getRepository(accountName, repository.name).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))) {
// redirect to the repository if repository already exists
redirect(s"/${accountName}/${repository.name}")
} else {
// fork repository asynchronously
forkRepository(accountName, repository, loginUserName)
// redirect to the repository
redirect(s"/${accountName}/${repository.name}")
}
} else BadRequest()
})
@@ -463,10 +580,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
private def uniqueRepository: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
for {
userName <- params.optionValue("owner")
_ <- getRepositoryNamesOfUser(userName).find(_ == value)
} yield {
"Repository already exists."
}
}
}
private def members: Constraint = new Constraint(){

View File

@@ -1,14 +0,0 @@
package gitbucket.core.controller
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()
}
}
}

View File

@@ -5,15 +5,19 @@ import gitbucket.core.model._
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.PullRequestService._
import gitbucket.core.service._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.view.helpers.{renderMarkup, isRenderable}
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util._
import gitbucket.core.view.helpers.{isRenderable, renderMarkup}
import org.eclipse.jgit.api.Git
import org.scalatra.{NoContent, UnprocessableEntity, Created}
import org.eclipse.jgit.revwalk.RevWalk
import org.scalatra.{Created, NoContent, UnprocessableEntity}
import scala.collection.JavaConverters._
import scala.concurrent.Await
import scala.concurrent.duration.Duration
class ApiController extends ApiControllerBase
with RepositoryService
@@ -33,6 +37,7 @@ class ApiController extends ApiControllerBase
with WebHookIssueCommentService
with WikiService
with ActivityService
with PrioritiesService
with OwnerAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
@@ -48,10 +53,12 @@ trait ApiControllerBase extends ControllerBase {
with LabelsService
with MilestonesService
with PullRequestService
with CommitsService
with CommitStatusService
with RepositoryCreationService
with IssueCreationService
with HandleCommentService
with PrioritiesService
with OwnerAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
@@ -122,10 +129,10 @@ trait ApiControllerBase extends ControllerBase {
/**
* https://developer.github.com/v3/repos/branches/#get-branch
*/
get ("/api/v3/repos/:owner/:repo/branches/:branch")(referrersOnly { repository =>
get ("/api/v3/repos/:owner/:repo/branches/*")(referrersOnly { repository =>
//import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.contains(branch)
branch <- params.get("splat") if repository.branchList.contains(branch)
br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
@@ -244,7 +251,8 @@ trait ApiControllerBase extends ControllerBase {
} 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 f = createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init)
Await.result(f, Duration.Inf)
val repository = getRepository(owner, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else {
@@ -268,7 +276,8 @@ trait ApiControllerBase extends ControllerBase {
} 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 f = createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init)
Await.result(f, Duration.Inf)
val repository = getRepository(groupName, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
} else {
@@ -284,10 +293,10 @@ trait ApiControllerBase extends ControllerBase {
/**
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
patch("/api/v3/repos/:owner/:repo/branches/*")(ownerOnly { repository =>
import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.contains(branch)
branch <- params.get("splat") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield {
@@ -365,6 +374,7 @@ trait ApiControllerBase extends ControllerBase {
data.body,
data.assignees.headOption,
milestone.map(_.milestoneId),
None,
data.labels,
loginAccount)
JsonFormat(ApiIssue(issue, RepositoryName(repository), ApiUser(loginAccount)))
@@ -378,7 +388,7 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
comments = getCommentsForApi(repository.owner, repository.name, issueId)
} yield {
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
}) getOrElse NotFound()
@@ -495,7 +505,7 @@ trait ApiControllerBase extends ControllerBase {
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account)] =
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
searchPullRequestByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
@@ -503,13 +513,14 @@ trait ApiControllerBase extends ControllerBase {
repos = repository.owner -> repository.name
)
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
})
@@ -526,6 +537,7 @@ trait ApiControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
JsonFormat(ApiPullRequest(
@@ -534,6 +546,7 @@ trait ApiControllerBase extends ControllerBase {
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
))
}) getOrElse NotFound()
@@ -624,6 +637,52 @@ trait ApiControllerBase extends ControllerBase {
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/repos/commits/#get-a-single-commit
*/
get("/api/v3/repos/:owner/:repo/commits/:sha")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
val sha = params("sha")
using(Git.open(getRepositoryDir(owner, name))){ git =>
val repo = git.getRepository
val objectId = repo.resolve(sha)
val commitInfo = using(new RevWalk(repo)){ revWalk =>
new CommitInfo(revWalk.parseCommit(objectId))
}
JsonFormat(ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, Some(commitInfo.parents.head), commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, sha).size
))
}
})
private def getAccount(userName: String, email: String): Account = {
getAccountByMailAddress(email).getOrElse {
Account(
userName = userName,
fullName = userName,
mailAddress = email,
password = "xxx",
isAdmin = false,
url = None,
registeredDate = new java.util.Date(),
updatedDate = new java.util.Date(),
lastLoginDate = None,
image = None,
isGroupAccount = false,
isRemoved = true,
description = None
)
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName

View File

@@ -9,12 +9,11 @@ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import io.github.gitbucket.scalatra.forms._
import org.json4s._
import org.scalatra._
import org.scalatra.i18n._
import org.scalatra.json._
import org.scalatra.forms._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
@@ -26,14 +25,17 @@ import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk._
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
/**
* Provides generic features for controller implementations.
*/
abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with ValidationSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
private val logger = LoggerFactory.getLogger(getClass)
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
before("/api/v3/*") {
@@ -41,25 +43,11 @@ abstract class ControllerBase extends ScalatraFilter
}
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
val httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse]
val context = request.getServletContext.getContextPath
val path = httpRequest.getRequestURI.substring(context.length)
val httpRequest = request.asInstanceOf[HttpServletRequest]
val context = request.getServletContext.getContextPath
val path = httpRequest.getRequestURI.substring(context.length)
if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val baseUrl = this.baseUrl(httpRequest)
if(account == null){
// Redirect to login form
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
} else if(account.isAdmin){
// H2 Console (administrators only)
chain.doFilter(request, response)
} else {
// Redirect to dashboard
httpResponse.sendRedirect(baseUrl + "/")
}
} else if(path.startsWith("/git/") || path.startsWith("/git-lfs/")){
if(path.startsWith("/git/") || path.startsWith("/git-lfs/")){
// Git repository
chain.doFilter(request, response)
} else {
@@ -147,16 +135,37 @@ abstract class ControllerBase extends ScalatraFilter
}
}
error{
case e => {
logger.error(s"Catch unhandled error in request: ${request}", e)
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.InternalServerError()
} else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json")
org.scalatra.InternalServerError(ApiError("Internal Server Error"))
} else {
org.scalatra.InternalServerError(gitbucket.core.html.error("Internal Server Error", Some(e)))
}
}
}
override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
includeContextPath: Boolean = true, includeServletPath: Boolean = true,
absolutize: Boolean = true, withSessionId: Boolean = true)
(implicit request: HttpServletRequest, response: HttpServletResponse): String =
if (path.startsWith("http")) path
else baseUrl + super.url(path, params, false, false, false)
/**
* Extends scalatra-form's trim rule to eliminate CR and LF.
*/
protected def trim2[T](valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T](){
def convert(value: String, messages: Messages): T = valueType.convert(trim(value), messages)
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Seq[(String, String)] =
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
valueType.validate(name, trim(value), params, messages)
private def trim(value: String): String = if(value == null) null else value.replaceAll("\r\n", "").trim
private def trim(value: String): String = if(value == null) null else value.replace("\r\n", "").trim
}
/**
@@ -291,13 +300,14 @@ trait AccountManagementControllerBase extends ControllerBase {
}
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
getAccountByMailAddress(value, true)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.optionValue(paramName) }
.map { _ => "Mail address is already registered." }
}
}
val allReservedNames = Set("git", "admin", "upload", "api")
val allReservedNames = Set("git", "admin", "upload", "api", "assets", "plugin-assets", "signin", "signout", "register", "activities.atom", "sidebar-collapse", "groups", "new")
protected def reservedNames(): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){
Some(s"${value} is reserved")

View File

@@ -1,8 +1,8 @@
package gitbucket.core.controller
import gitbucket.core.model.Account
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.{AccountService, RepositoryService, ReleaseService}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.servlet.Database
import gitbucket.core.util._
import gitbucket.core.util.SyntaxSugars._
@@ -26,7 +26,7 @@ class FileUploadController extends ScalatraServlet
with AccountService
with ReleaseService{
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
configureMultipartHandling(MultipartConfig(maxFileSize = Some(FileUtil.MaxFileSize)))
post("/image"){
execute({ (file, fileId) =>
@@ -35,12 +35,19 @@ class FileUploadController extends ScalatraServlet
}, FileUtil.isImage)
}
post("/tmp"){
execute({ (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
session += Keys.Session.Upload(fileId) -> file.name
}, _ => true)
}
post("/file/:owner/:repository"){
execute({ (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
}, FileUtil.isUploadableType)
}, _ => true)
}
post("/wiki/:owner/:repository"){
@@ -72,12 +79,12 @@ class FileUploadController extends ScalatraServlet
builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, loginAccount.userName, loginAccount.mailAddress, s"Uploaded ${fileName}")
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, s"Uploaded ${fileName}")
fileName
}
}
}, FileUtil.isUploadableType)
}, _ => true)
}
} getOrElse BadRequest()
}
@@ -97,7 +104,7 @@ class FileUploadController extends ScalatraServlet
fileId), file.get)
fileName
}
}, (_ => true))
}, _ => true)
}.getOrElse(BadRequest())
}
@@ -124,10 +131,11 @@ class FileUploadController extends ScalatraServlet
}
}
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
private def execute(f: (FileItem, String) => Unit , mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)
contentType = "text/plain"
Ok(fileId)
}
case _ => BadRequest()

View File

@@ -6,7 +6,7 @@ import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator}
import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
import org.scalatra.Ok
@@ -19,11 +19,12 @@ trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with UsersAuthenticator with ReferrerAuthenticator =>
case class SignInForm(userName: String, password: String)
case class SignInForm(userName: String, password: String, hash: Option[String])
val signinForm = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
"password" -> trim(label("Password", text(required))),
"hash" -> trim(optional(text()))
)(SignInForm.apply)
// val searchForm = mapping(
@@ -54,7 +55,7 @@ trait IndexControllerBase extends ControllerBase {
post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account)
case Some(account) => signin(account, form.hash)
case None => {
flash += "userName" -> form.userName
flash += "password" -> form.password
@@ -74,7 +75,7 @@ trait IndexControllerBase extends ControllerBase {
xml.feed(getRecentActivities())
}
get("/sidebar-collapse"){
post("/sidebar-collapse"){
if(params("collapse") == "true"){
session.setAttribute("sidebar-collapse", "true")
} else {
@@ -86,7 +87,7 @@ trait IndexControllerBase extends ControllerBase {
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: Account) = {
private def signin(account: Account, hash: Option[String]) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
@@ -98,7 +99,7 @@ trait IndexControllerBase extends ControllerBase {
if(redirectUrl.stripSuffix("/") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
redirect(redirectUrl + hash.getOrElse(""))
}
}.getOrElse {
redirect("/")
@@ -120,7 +121,12 @@ trait IndexControllerBase extends ControllerBase {
case (true, false) => !t.isGroupAccount
case (false, true) => t.isGroupAccount
case (false, false) => false
}}.map { t => t.userName }
}}.map { t =>
Map(
"label" -> s"<b>@${t.userName}</b> ${t.fullName}",
"value" -> t.userName
)
}
))
)
})

View File

@@ -8,7 +8,7 @@ import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
import org.scalatra.{BadRequest, Ok}
@@ -27,6 +27,7 @@ class IssuesController extends IssuesControllerBase
with PullRequestService
with WebHookIssueCommentService
with CommitsService
with PrioritiesService
trait IssuesControllerBase extends ControllerBase {
self: IssuesService
@@ -41,10 +42,11 @@ trait IssuesControllerBase extends ControllerBase {
with ReferrerAuthenticator
with WritableUsersAuthenticator
with PullRequestService
with WebHookIssueCommentService =>
with WebHookIssueCommentService
with PrioritiesService =>
case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
assignedUserName: Option[String], milestoneId: Option[Int], priorityId: Option[Int], labelNames: Option[String])
case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
@@ -53,6 +55,7 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())),
"labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply)
@@ -76,7 +79,7 @@ trait IssuesControllerBase extends ControllerBase {
get("/:owner/:repository/issues")(referrersOnly { repository =>
val q = request.getParameter("q")
if(Option(q).exists(_.contains("is:pr"))){
redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
redirect(s"/${repository.owner}/${repository.name}/pulls?q=${StringUtil.urlEncode(q)}")
} else {
searchIssues(repository)
}
@@ -84,17 +87,22 @@ trait IssuesControllerBase extends ControllerBase {
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
getIssue(owner, name, issueId) map {
html.issue(
_,
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
getAssignableUserNames(owner, name),
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
isIssueEditable(repository),
isIssueManageable(repository),
repository)
getIssue(owner, name, issueId) map { issue =>
if(issue.isPullRequest){
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else {
html.issue(
issue,
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
getAssignableUserNames(owner, name),
getMilestonesWithIssueCount(owner, name),
getPriorities(owner, name),
getLabels(owner, name),
isIssueEditable(repository),
isIssueManageable(repository),
repository)
}
} getOrElse NotFound()
}
})
@@ -105,6 +113,8 @@ trait IssuesControllerBase extends ControllerBase {
html.create(
getAssignableUserNames(owner, name),
getMilestones(owner, name),
getPriorities(owner, name),
getDefaultPriority(owner, name),
getLabels(owner, name),
isIssueManageable(repository),
getContentTemplate(repository, "ISSUE_TEMPLATE"),
@@ -121,6 +131,7 @@ trait IssuesControllerBase extends ControllerBase {
form.content,
form.assignedUserName,
form.milestoneId,
form.priorityId,
form.labelNames.toArray.flatMap(_.split(",")),
context.loginAccount.get)
@@ -182,7 +193,7 @@ trait IssuesControllerBase extends ControllerBase {
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditableContent(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content)
updateComment(comment.issueId, comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized()
} getOrElse NotFound()
@@ -193,7 +204,7 @@ trait IssuesControllerBase extends ControllerBase {
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditableContent(owner, name, comment.commentedUserName)){
Ok(deleteComment(comment.commentId))
Ok(deleteComment(comment.issueId, comment.commentId))
} else Unauthorized()
} getOrElse NotFound()
}
@@ -287,6 +298,11 @@ trait IssuesControllerBase extends ControllerBase {
} getOrElse Ok()
})
ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository =>
updatePriorityId(repository.owner, repository.name, params("id").toInt, priorityId("priorityId"))
Ok("updated")
})
post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
defining(params.get("value")){ action =>
action match {
@@ -331,6 +347,14 @@ trait IssuesControllerBase extends ControllerBase {
}
})
post("/:owner/:repository/issues/batchedit/priority")(writableUsersOnly { repository =>
defining(priorityId("value")){ value =>
executeBatch(repository) {
updatePriorityId(repository.owner, repository.name, _, value)
}
}
})
get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
(Directory.getAttachedDir(repository.owner, repository.name) match {
case dir if(dir.exists && dir.isDirectory) =>
@@ -344,6 +368,7 @@ trait IssuesControllerBase extends ControllerBase {
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute
@@ -366,6 +391,7 @@ trait IssuesControllerBase extends ControllerBase {
page,
getAssignableUserNames(owner, repoName),
getMilestones(owner, repoName),
getPriorities(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), false, owner -> repoName),

View File

@@ -4,7 +4,8 @@ import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._
import gitbucket.core.util.SyntaxSugars._
import org.scalatra.forms._
import org.scalatra.i18n.Messages
import org.scalatra.Ok
@@ -82,10 +83,10 @@ trait LabelsControllerBase extends ControllerBase {
}
private def uniqueLabelName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
val owner = params("owner")
val repository = params("repository")
params.get("labelId").map { labelId =>
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
val owner = params.value("owner")
val repository = params.value("repository")
params.optionValue("labelId").map { labelId =>
getLabel(owner, repository, value).filter(_.labelId != labelId.toInt).map(_ => "Name has already been taken.")
}.getOrElse {
getLabel(owner, repository, value).map(_ => "Name has already been taken.")

View File

@@ -4,7 +4,7 @@ import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService

View File

@@ -0,0 +1,40 @@
package gitbucket.core.controller
import org.scalatra.MovedPermanently
class PreProcessController extends PreProcessControllerBase
trait PreProcessControllerBase extends ControllerBase {
/**
* Provides GitHub compatible URLs for Git client.
*
* <ul>
* <li>git clone http://localhost:8080/owner/repo</li>
* <li>git clone http://localhost:8080/owner/repo.git</li>
* </ul>
*
* @see https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
*/
get("/*/*/info/refs") {
val query = Option(request.getQueryString).map("?" + _).getOrElse("")
halt(MovedPermanently(baseUrl + "/git" + request.getRequestURI + query))
}
/**
* Filter requests from anonymous users.
*
* If anonymous access is allowed, pass all requests.
* But if it's not allowed, demands authentication except some paths.
*/
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs")) {
Unauthorized()
} else {
pass()
}
}
}

View File

@@ -0,0 +1,112 @@
package gitbucket.core.controller
import gitbucket.core.issues.priorities.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, PrioritiesService}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
import org.scalatra.forms._
import org.scalatra.i18n.Messages
import org.scalatra.Ok
class PrioritiesController extends PrioritiesControllerBase
with PrioritiesService with IssuesService with RepositoryService with AccountService
with ReferrerAuthenticator with WritableUsersAuthenticator
trait PrioritiesControllerBase extends ControllerBase {
self: PrioritiesService with IssuesService with RepositoryService
with ReferrerAuthenticator with WritableUsersAuthenticator =>
case class PriorityForm(priorityName: String, description: Option[String], color: String)
val priorityForm = mapping(
"priorityName" -> trim(label("Priority name", text(required, priorityName, uniquePriorityName, maxlength(100)))),
"description" -> trim(label("Description", optional(text(maxlength(255))))),
"priorityColor" -> trim(label("Color", text(required, color)))
)(PriorityForm.apply)
get("/:owner/:repository/issues/priorities")(referrersOnly { repository =>
html.list(
getPriorities(repository.owner, repository.name),
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/priorities/new")(writableUsersOnly { repository =>
html.edit(None, repository)
})
ajaxPost("/:owner/:repository/issues/priorities/new", priorityForm)(writableUsersOnly { (form, repository) =>
val priorityId = createPriority(repository.owner, repository.name, form.priorityName, form.description, form.color.substring(1))
html.priority(
getPriority(repository.owner, repository.name, priorityId).get,
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/priorities/:priorityId/edit")(writableUsersOnly { repository =>
getPriority(repository.owner, repository.name, params("priorityId").toInt).map { priority =>
html.edit(Some(priority), repository)
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/priorities/:priorityId/edit", priorityForm)(writableUsersOnly { (form, repository) =>
updatePriority(repository.owner, repository.name, params("priorityId").toInt, form.priorityName, form.description, form.color.substring(1))
html.priority(
getPriority(repository.owner, repository.name, params("priorityId").toInt).get,
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
})
ajaxPost("/:owner/:repository/issues/priorities/reorder")(writableUsersOnly { (repository) =>
reorderPriorities(repository.owner, repository.name, params("order")
.split(",")
.map(id => id.toInt)
.zipWithIndex
.toMap)
Ok()
})
ajaxPost("/:owner/:repository/issues/priorities/default")(writableUsersOnly { (repository) =>
setDefaultPriority(repository.owner, repository.name, priorityId("priorityId"))
Ok()
})
ajaxPost("/:owner/:repository/issues/priorities/:priorityId/delete")(writableUsersOnly { repository =>
deletePriority(repository.owner, repository.name, params("priorityId").toInt)
Ok()
})
val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
/**
* Constraint for the identifier such as user name, repository name or page name.
*/
private def priorityName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(value.contains(',')){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
private def uniquePriorityName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
val owner = params.value("owner")
val repository = params.value("repository")
params.optionValue("priorityId").map { priorityId =>
getPriority(owner, repository, value).filter(_.priorityId != priorityId.toInt).map(_ => "Name has already been taken.")
}.getOrElse {
getPriority(owner, repository, value).map(_ => "Name has already been taken.")
}
}
}
}

View File

@@ -1,6 +1,7 @@
package gitbucket.core.controller
import gitbucket.core.model.WebHook
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService
@@ -12,9 +13,10 @@ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.revwalk.RevWalk
import scala.collection.JavaConverters._
@@ -23,14 +25,14 @@ class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with CommitsService with ActivityService with WebHookPullRequestService
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator
with CommitStatusService with MergeService with ProtectedBranchService
with CommitStatusService with MergeService with ProtectedBranchService with PrioritiesService
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator
with CommitStatusService with MergeService with ProtectedBranchService =>
with CommitStatusService with MergeService with ProtectedBranchService with PrioritiesService =>
val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))),
@@ -44,11 +46,13 @@ trait PullRequestsControllerBase extends ControllerBase {
"commitIdTo" -> trim(text(required, maxlength(40))),
"assignedUserName" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())),
"labelNames" -> trim(optional(text()))
)(PullRequestForm.apply)
val mergeForm = mapping(
"message" -> trim(label("Message", text(required)))
"message" -> trim(label("Message", text(required))),
"strategy" -> trim(label("Strategy", text(required)))
)(MergeForm.apply)
case class PullRequestForm(
@@ -63,10 +67,11 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdTo: String,
assignedUserName: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Option[String]
)
case class MergeForm(message: String)
case class MergeForm(message: String, strategy: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
val q = request.getParameter("q")
@@ -92,12 +97,15 @@ trait PullRequestsControllerBase extends ControllerBase {
getIssueLabels(owner, name, issueId),
getAssignableUserNames(owner, name),
getMilestonesWithIssueCount(owner, name),
getPriorities(owner, name),
getLabels(owner, name),
commits,
diffs,
isEditable(repository),
isManageable(repository),
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
flash.toMap.map(f => f._1 -> f._2.toString))
}
}
@@ -109,13 +117,13 @@ trait PullRequestsControllerBase extends ControllerBase {
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
val hasConflict = LockUtil.lock(s"${owner}/${name}"){
val conflictMessage = LockUtil.lock(s"${owner}/${name}"){
checkConflict(owner, name, pullreq.branch, issueId)
}
val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus(
hasConflict = hasConflict,
conflictMessage = conflictMessage,
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
branchProtection = branchProtection,
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
@@ -138,22 +146,36 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound()
})
get("/:owner/:repository/pull/:id/delete/*")(writableUsersOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
get("/:owner/:repository/pull/:id/delete_branch")(readableUsersOnly { baseRepository =>
(for {
issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount)
} yield {
val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if(branchProtection.enabled){
flash += "error" -> s"branch ${pullreq.requestBranch} is protected."
} else {
if(repository.repository.defaultBranch != pullreq.requestBranch){
val userName = context.loginAccount.get.userName
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, pullreq.requestBranch)
}
createComment(baseRepository.owner, baseRepository.name, userName, issueId, pullreq.requestBranch, "delete_branch")
} else {
flash += "error" -> s"""Can't delete the default branch "${pullreq.requestBranch}"."""
}
}
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} getOrElse NotFound()
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
}) getOrElse NotFound()
})
post("/:owner/:repository/pull/:id/update_branch")(writableUsersOnly { baseRepository =>
post("/:owner/:repository/pull/:id/update_branch")(readableUsersOnly { baseRepository =>
(for {
issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount
@@ -217,7 +239,7 @@ trait PullRequestsControllerBase extends ControllerBase {
}
}
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
}) getOrElse NotFound()
})
@@ -231,21 +253,37 @@ trait PullRequestsControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
val commentId = createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
// record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// merge git repository
mergePullRequest(git, pullreq.branch, issueId,
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
val revCommits = using(new RevWalk( git.getRepository )){ revWalk =>
commits.flatten.map { commit =>
revWalk.parseCommit(git.getRepository.resolve(commit.id))
}
}.reverse
// merge git repository
form.strategy match {
case "merge-commit" =>
mergePullRequest(git, pullreq.branch, issueId,
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
case "rebase" =>
rebasePullRequest(git, pullreq.branch, issueId, revCommits,
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
case "squash" =>
squashPullRequest(git, pullreq.branch, issueId,
s"${issue.title} (#${issueId})\n\n" + form.message,
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
}
// close issue by content of pull request
val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
@@ -261,9 +299,10 @@ trait PullRequestsControllerBase extends ControllerBase {
// call web hook
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issue, "merge"){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
// call hooks
PluginRegistry().getPullRequestHooks.foreach{ h =>
h.addedComment(commentId, form.message, issue, repository)
h.merged(issue, repository)
}
redirect(s"/${owner}/${name}/pull/${issueId}")
@@ -303,8 +342,8 @@ trait PullRequestsControllerBase extends ControllerBase {
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner) {
@@ -312,7 +351,7 @@ trait PullRequestsControllerBase extends ControllerBase {
Some(forkedRepository.name)
} else if(forkedRepository.repository.originUserName.isEmpty){
// when ForkedRepository is the original repository
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_.userName == originOwner).map(_.repositoryName)
} else if(Some(originOwner) == forkedRepository.repository.originUserName){
// Original repository
forkedRepository.repository.originRepositoryName
@@ -359,10 +398,13 @@ trait PullRequestsControllerBase extends ControllerBase {
title,
commits,
diffs,
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
((forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(userName), Some(repositoryName)) => getRepository(userName, repositoryName) match {
case Some(x) => x.repository :: getForkedRepositories(userName, repositoryName)
case None => getForkedRepositories(userName, repositoryName)
}
case _ => forkedRepository.repository :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
}).map { repository => (repository.userName, repository.repositoryName) },
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
originId,
forkedId,
@@ -375,6 +417,7 @@ trait PullRequestsControllerBase extends ControllerBase {
hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount),
getAssignableUserNames(originRepository.owner, originRepository.name),
getMilestones(originRepository.owner, originRepository.name),
getPriorities(originRepository.owner, originRepository.name),
getLabels(originRepository.owner, originRepository.name)
)
}
@@ -389,15 +432,15 @@ trait PullRequestsControllerBase extends ControllerBase {
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
Some(forkedRepository.name)
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_.userName == originOwner).map(_.repositoryName)
}
};
originRepository <- getRepository(originOwner, originRepositoryName)
@@ -412,7 +455,7 @@ trait PullRequestsControllerBase extends ControllerBase {
checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)
}
html.mergecheck(conflict)
html.mergecheck(conflict.isDefined)
}
}) getOrElse NotFound()
})
@@ -430,6 +473,7 @@ trait PullRequestsControllerBase extends ControllerBase {
content = form.content,
assignedUserName = if (manageable) form.assignedUserName else None,
milestoneId = if (manageable) form.milestoneId else None,
priorityId = if (manageable) form.priorityId else None,
isPullRequest = true)
createPullRequest(
@@ -468,23 +512,50 @@ trait PullRequestsControllerBase extends ControllerBase {
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")) {
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
}
// call hooks
PluginRegistry().getPullRequestHooks.foreach(_.created(issue, repository))
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
})
ajaxGet("/:owner/:repository/pulls/proposals")(readableUsersOnly { repository =>
val branches = JGitUtil.getBranches(
owner = repository.owner,
name = repository.name,
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
)
.filter(x => x.mergeInfo.map(_.ahead).getOrElse(0) > 0 && x.mergeInfo.map(_.behind).getOrElse(0) == 0)
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
.map(_.name)
.reverse
val targetRepository = (for {
parentUserName <- repository.repository.parentUserName
parentRepoName <- repository.repository.parentRepositoryName
parentRepository <- getRepository(parentUserName, parentRepoName)
} yield {
parentRepository
}).getOrElse {
repository
}
val proposedBranches = branches.filter { branch =>
getPullRequestsByRequest(repository.owner, repository.name, branch, None).isEmpty
}
html.proposals(proposedBranches, targetRepository, repository)
})
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
private def parseCompareIdentifier(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))
@@ -505,6 +576,7 @@ trait PullRequestsControllerBase extends ControllerBase {
page,
getAssignableUserNames(owner, repoName),
getMilestones(owner, repoName),
getPriorities(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), true, owner -> repoName),

View File

@@ -1,7 +1,10 @@
package gitbucket.core.controller
import java.time.{LocalDateTime, ZoneId, ZoneOffset}
import java.util.Date
import gitbucket.core.settings.html
import gitbucket.core.model.WebHook
import gitbucket.core.model.{RepositoryWebHook, WebHook}
import gitbucket.core.service._
import gitbucket.core.service.WebHookService._
import gitbucket.core.util._
@@ -9,7 +12,7 @@ import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
@@ -40,7 +43,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)
val optionsForm = mapping(
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), identifier, renameRepositoryName))),
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), repository, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type" , boolean())),
"issuesOption" -> trim(label("Issues Option" , text(required, featureOption))),
@@ -133,20 +136,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
}
}
// Move lfs directory
defining(getLfsDir(repository.owner, repository.name)){ dir =>
// Move files directory
defining(getRepositoryFilesDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getLfsDir(repository.owner, form.repositoryName))
}
}
// Move release directory
defining(getReleaseFilesDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getReleaseFilesDir(repository.owner, form.repositoryName))
// Move attached directory
defining(getAttachedDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getAttachedDir(repository.owner, form.repositoryName))
FileUtils.moveDirectory(dir, getRepositoryFilesDir(repository.owner, form.repositoryName))
}
}
// Delete parent directory
@@ -163,7 +156,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
get("/:owner/:repository/settings/branches")(ownerOnly { repository =>
val protecteions = getProtectedBranchList(repository.owner, repository.name)
html.branches(repository, protecteions, flash.get("info"))
});
})
/** Update default branch */
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
@@ -188,7 +181,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name, org.joda.time.LocalDateTime.now.minusWeeks(1).toDate).toSet
val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name,
Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.UTC))).toSet
val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
html.branchprotection(repository, branch, protection, knownContexts, flash.get("info"))
}
@@ -225,8 +219,8 @@ 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, "", WebHookContentType.FORM, None)
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
val webhook = RepositoryWebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None)
html.edithook(webhook, Set(WebHook.Push), repository, true)
})
/**
@@ -264,7 +258,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
val url = params("url")
val token = Some(params("token"))
val ctype = WebHookContentType.valueOf(params("ctype"))
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, ctype, token)
val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token)
val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get
val commits = if(JGitUtil.isEmpty(git)) List.empty else git.log
@@ -301,7 +295,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"headers" -> _headers(req.getAllHeaders),
"payload" -> json
)).recover(toErrorMap), 20 seconds),
"responce" -> Await.result(resFuture.map(res => Map(
"response" -> Await.result(resFuture.map(res => Map(
"status" -> res.getStatusLine(),
"body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders())
@@ -315,7 +309,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
*/
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
html.edithooks(webhook, events, repository, flash.get("info"), false)
html.edithook(webhook, events, repository, false)
} getOrElse NotFound()
})
@@ -356,24 +350,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
}
}
// Move lfs directory
defining(getLfsDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory()) {
FileUtils.moveDirectory(dir, getLfsDir(form.newOwner, repository.name))
// Move files directory
defining(getRepositoryFilesDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getRepositoryFilesDir(form.newOwner, repository.name))
}
}
// Move release directory
defining(getReleaseFilesDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getReleaseFilesDir(form.newOwner, repository.name))
// Move attached directory
defining(getAttachedDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getAttachedDir(form.newOwner, repository.name))
}
}
// Delere parent directory
FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name))
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.transferred(repository.owner, form.newOwner, repository.name))
@@ -393,10 +375,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getReleaseFilesDir(repository.owner, repository.name))
val lfsDir = getLfsDir(repository.owner, repository.name)
FileUtils.deleteDirectory(lfsDir)
FileUtil.deleteDirectoryIfEmpty(lfsDir.getParentFile())
FileUtils.deleteDirectory(getRepositoryFilesDir(repository.owner, repository.name))
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.deleted(repository.owner, repository.name))
@@ -411,7 +390,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/gc")(ownerOnly { repository =>
LockUtil.lock(s"${repository.owner}/${repository.name}") {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.gc();
git.gc().call()
}
}
flash += "info" -> "Garbage collection has been executed."
@@ -453,12 +432,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
private def webhookEvents = new ValueType[Set[WebHook.Event]]{
def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = {
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t)
}.toSet
}
def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
Seq(name -> messages("error.required").format(name))
} else {
Nil
@@ -484,19 +463,22 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Duplicate check for the rename repository name.
*/
private def renameRepositoryName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("repository").filter(_ != value).flatMap { _ =>
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
for {
repoName <- params.optionValue("repository") if repoName != value
userName <- params.optionValue("owner")
_ <- getRepositoryNamesOfUser(userName).find(_ == value)
} yield {
"Repository already exists."
}
}
}
/**
*
*/
private def featureOption: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] =
if(Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
}

View File

@@ -1,6 +1,6 @@
package gitbucket.core.controller
import java.io.FileInputStream
import java.io.File
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.plugin.PluginRegistry
@@ -13,20 +13,21 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.model.{Account, CommitState, CommitStatus, WebHook}
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
import gitbucket.core.view.helpers
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.IOUtils
import org.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.errors.MissingObjectException
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import org.json4s.jackson.Serialization
import org.scalatra._
import org.scalatra.i18n.Messages
class RepositoryViewerController extends RepositoryViewerControllerBase
@@ -45,6 +46,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
case class UploadForm(
branch: String,
path: String,
uploadFiles: String,
message: Option[String]
)
case class EditorForm(
branch: String,
path: String,
@@ -53,14 +61,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
charset: String,
lineSeparator: String,
newFileName: String,
oldFileName: Option[String]
oldFileName: Option[String],
commit: String
)
case class DeleteForm(
branch: String,
path: String,
message: Option[String],
fileName: String
fileName: String,
commit: String
)
case class CommentForm(
@@ -71,6 +81,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
issueId: Option[Int]
)
val uploadForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"uploadFiles" -> trim(label("Upload files", text(required))),
"message" -> trim(label("Message", optional(text()))),
)(UploadForm.apply)
val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
@@ -79,14 +96,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"charset" -> trim(label("Charset", text(required))),
"lineSeparator" -> trim(label("Line Separator", text(required))),
"newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
"oldFileName" -> trim(label("Old filename", optional(text()))),
"commit" -> trim(label("Commit", text(required, conflict)))
)(EditorForm.apply)
val deleteForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"message" -> trim(label("Message", optional(text()))),
"fileName" -> trim(label("Filename", text(required)))
"fileName" -> trim(label("Filename", text(required))),
"commit" -> trim(label("Commit", text(required, conflict)))
)(DeleteForm.apply)
val commentForm = mapping(
@@ -130,14 +149,31 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays the file list of the repository root and the default branch.
*/
get("/:owner/:repository") {
params.get("go-get") match {
case Some("1") => defining(request.paths){ paths =>
getRepository(paths(0), paths(1)).map(gitbucket.core.html.goget(_))getOrElse NotFound()
val owner = params("owner")
val repository = params("repository")
if (RepositoryCreationService.isCreating(owner, repository)) {
gitbucket.core.repo.html.creating(owner, repository)
} else {
params.get("go-get") match {
case Some("1") => defining(request.paths) { paths =>
getRepository(owner, repository).map(gitbucket.core.html.goget(_)) getOrElse NotFound()
}
case _ => referrersOnly(fileList(_))
}
case _ => referrersOnly(fileList(_))
}
}
ajaxGet("/:owner/:repository/creating") {
val owner = params("owner")
val repository = params("repository")
contentType = formats("json")
Serialization.write(Map(
"creating" -> RepositoryCreationService.isCreating(owner, repository),
"error" -> RepositoryCreationService.getCreationError(owner, repository)
))
}
/**
* Displays the file list of the specified path and branch.
*/
@@ -157,13 +193,24 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (branchName, path) = repository.splitPath(multiParams("splat").head)
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
def getStatuses(sha: String): List[CommitStatus] = {
getCommitStatues(repository.owner, repository.name, sha)
}
def getSummary(statuses: List[CommitStatus]): (CommitState, String) = {
val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet)
val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")
state -> summary
}
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
}, page, hasNext, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), getStatuses, getSummary)
case Left(_) => NotFound()
}
}
@@ -172,11 +219,50 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/new/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, None, Some("UTF-8")),
protectedBranch)
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
html.editor(
branch = branch,
repository = repository,
pathList = if (path.length == 0) Nil else path.split("/").toList,
fileName = None,
content = JGitUtil.ContentInfo("text", None, None, Some("UTF-8")),
protectedBranch = protectedBranch,
commit = revCommit.getName
)
}
})
get("/:owner/:repository/upload/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
html.upload(branch, repository, if(path.length == 0) Nil else path.split("/").toList, protectedBranch)
})
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
val files = form.uploadFiles.split("\n").map { line =>
val i = line.indexOf(':')
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
}
commitFiles(
repository = repository,
branch = form.branch,
path = form.path,
files = files,
message = form.message.getOrElse("Add files via upload")
)
if(form.path.length == 0){
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}")
} else {
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}/${form.path}")
}
})
get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
@@ -186,9 +272,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId),
protectedBranch)
html.editor(
branch = branch,
repository = repository,
pathList = paths.take(paths.size - 1).toList,
fileName = Some(paths.last),
content = JGitUtil.getContentInfo(git, path, objectId),
protectedBranch = protectedBranch,
commit = revCommit.getName
)
} getOrElse NotFound()
}
})
@@ -200,8 +292,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId))
html.delete(
branch = branch,
repository = repository,
pathList = paths.take(paths.size - 1).toList,
fileName = paths.last,
content = JGitUtil.getContentInfo(git, path, objectId),
commit = revCommit.getName
)
} getOrElse NotFound()
}
})
@@ -215,7 +313,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
oldFileName = None,
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
charset = form.charset,
message = form.message.getOrElse(s"Create ${form.newFileName}")
message = form.message.getOrElse(s"Create ${form.newFileName}"),
commit = form.commit
)
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
@@ -232,21 +331,31 @@ trait RepositoryViewerControllerBase extends ControllerBase {
oldFileName = form.oldFileName,
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
charset = form.charset,
message = if(form.oldFileName.contains(form.newFileName)){
message = if (form.oldFileName.contains(form.newFileName)) {
form.message.getOrElse(s"Update ${form.newFileName}")
} else {
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
}
},
commit = form.commit
)
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
redirect(s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${
if (form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
}")
})
post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
form.message.getOrElse(s"Delete ${form.fileName}"))
commitFile(
repository = repository,
branch = form.branch,
path = form.path,
newFileName = None,
oldFileName = Some(form.fileName),
content = "",
charset = "",
message = form.message.getOrElse(s"Delete ${form.fileName}"),
commit = form.commit
)
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
})
@@ -275,25 +384,23 @@ trait RepositoryViewerControllerBase extends ControllerBase {
// Download (This route is left for backword compatibility)
responseRawFile(git, objectId, path, repository)
} else {
html.blob(id, repository, path.split("/").toList,
JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
request.paths(2) == "blame",
isLfsFile(git, objectId))
html.blob(
branch = id,
repository = repository,
pathList = path.split("/").toList,
content = JGitUtil.getContentInfo(git, path, objectId),
latestCommit = new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
isBlame = request.paths(2) == "blame",
isLfsFile = isLfsFile(git, objectId)
)
}
} getOrElse NotFound()
}
})
private def isLfsFile(git: Git, objectId: ObjectId): Boolean = {
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
if(loader.isLarge){
false
} else {
new String(loader.getCachedBytes, "UTF-8").startsWith("version https://git-lfs.github.com/spec/v1")
}
}.getOrElse(false)
JGitUtil.getObjectLoaderFromId(git, objectId)(JGitUtil.isLfsPointer).getOrElse(false)
}
get("/:owner/:repository/blame/*"){
@@ -308,7 +415,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = formats("json")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
Map(
Serialization.write(Map(
"root" -> s"${context.baseUrl}/${repository.owner}/${repository.name}",
"id" -> id,
"path" -> path,
@@ -323,8 +430,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"prevPath" -> blame.prevPath,
"commited" -> blame.commitTime.getTime,
"message" -> blame.message,
"lines" -> blame.lines)
})
"lines" -> blame.lines
)
}))
}
})
@@ -337,14 +445,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
try {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit =>
JGitUtil.getDiffs(git, id) match {
case (diffs, oldCommitId) =>
html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
getCommitComments(repository.owner, repository.name, id, true),
repository, diffs, oldCommitId, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
}
val diffs = JGitUtil.getDiffs(git, None, id, true, false)
val oldCommitId = JGitUtil.getParentCommitId(git, id)
html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
getCommitComments(repository.owner, repository.name, id, true),
repository, diffs, oldCommitId, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
}
}
} catch {
@@ -352,6 +460,31 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
})
get("/:owner/:repository/patch/:id")(referrersOnly { repository =>
try {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val diff = JGitUtil.getPatch(git, None, params("id"))
contentType = formats("txt")
diff
}
} catch {
case e:MissingObjectException => NotFound()
}
})
get("/:owner/:repository/patch/*...*")(referrersOnly { repository =>
try {
val Seq(fromId, toId) = multiParams("splat")
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val diff = JGitUtil.getPatch(git, Some(fromId), toId)
contentType = formats("txt")
diff
}
} catch {
case e: MissingObjectException => NotFound()
}
})
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
@@ -384,9 +517,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get
form.issueId match {
case Some(issueId) =>
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
getPullRequest(repository.owner, repository.name, issueId).foreach { case (issue, pullRequest) =>
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId, form.content, issue, repository))
callPullRequestReviewCommentWebHook("create", comment, repository, issue, pullRequest, context.baseUrl, context.loginAccount.get)
}
case None =>
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(comment, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
})
@@ -515,7 +652,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repository.repository.originRepositoryName.getOrElse(repository.name)),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
repository.repository.originRepositoryName.getOrElse(repository.name)
).map { repository => (repository.userName, repository.repositoryName) },
context.loginAccount match {
case None => List()
case account: Option[Account] => getGroupsByUserName(account.get.userName)
@@ -547,6 +685,140 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
})
case class UploadFiles(branch: String, path: String, fileIds: Map[String,String], message: String) {
lazy val isValid: Boolean = fileIds.nonEmpty
}
case class CommitFile(id: String, name: String)
private def commitFiles(repository: RepositoryService.RepositoryInfo,
files: Seq[CommitFile],
branch: String, path: String, message: String) = {
// prepend path to the filename
val newFiles = files.map { file =>
file.copy(name = if(path.length == 0) file.name else s"${path}/${file.name}")
}
_commitFile(repository, branch, message) { case (git, headTip, builder, inserter) =>
JGitUtil.processTree(git, headTip) { (path, tree) =>
if(!newFiles.exists(_.name.contains(path))) {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
newFiles.foreach { file =>
val bytes = FileUtils.readFileToByteArray(new File(getTemporaryDir(session.getId), file.id))
builder.add(JGitUtil.createDirCacheEntry(file.name,
FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes)))
builder.finish()
}
}
}
private def commitFile(repository: RepositoryService.RepositoryInfo,
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
content: String, charset: String, message: String, commit: String) = {
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
_commitFile(repository, branch, message){ case (git, headTip, builder, inserter) =>
if(headTip.getName == commit){
val permission = JGitUtil.processTree(git, headTip) { (path, tree) =>
// Add all entries except the editing file
if (!newPath.contains(path) && !oldPath.contains(path)) {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
// Retrieve permission if file exists to keep it
oldPath.collect { case x if x == path => tree.getEntryFileMode.getBits }
}.flatten.headOption
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath,
permission.map { bits => FileMode.fromBits(bits) } getOrElse FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
}
builder.finish()
}
}
}
private def _commitFile(repository: RepositoryService.RepositoryInfo,
branch: String, message: String)(f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit) = {
LockUtil.lock(s"${repository.owner}/${repository.name}") {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val loginAccount = context.loginAccount.get
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName)
f(git, headTip, builder, inserter)
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
headName, loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.close()
val receivePack = new ReceivePack(git.getRepository)
val receiveCommand = new ReceiveCommand(headTip, commitId, headName)
// call post commit hook
val error = PluginRegistry().getReceiveHooks.flatMap { hook =>
hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, loginAccount.userName)
}.headOption
error match {
case Some(error) =>
// commit is rejected
// TODO Notify commit failure to edited user
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(headTip)
refUpdate.setForceUpdate(true)
refUpdate.update()
case None =>
// update refs
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
refUpdate.update()
// update pull request
updatePullRequests(repository.owner, repository.name, branch)
// record activity
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo))
// create issue comment by commit message
createIssueComment(repository.owner, repository.name, commitInfo)
// close issue by commit message
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call post commit hook
PluginRegistry().getReceiveHooks.foreach { hook =>
hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, loginAccount.userName)
}
//call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
}
}
}
}
}
}
private val readmeFiles = PluginRegistry().renderableExtensions.map { extension =>
s"readme.${extension}"
} ++ Seq("readme.txt", "readme")
@@ -569,7 +841,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
// get files
val files = JGitUtil.getFileList(git, revision, path)
val files = JGitUtil.getFileList(git, revision, path, context.settings.baseUrl)
val parentPath = if (path == ".") Nil else path.split("/").toList
// process README.md or README.markdown
val readme = files.find { file =>
@@ -597,84 +869,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
private def commitFile(repository: RepositoryService.RepositoryInfo,
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
content: String, charset: String, message: String) = {
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
LockUtil.lock(s"${repository.owner}/${repository.name}"){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val loginAccount = context.loginAccount.get
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName)
val permission = JGitUtil.processTree(git, headTip){ (path, tree) =>
// Add all entries except the editing file
if(!newPath.contains(path) && !oldPath.contains(path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
// Retrieve permission if file exists to keep it
oldPath.collect { case x if x == path => tree.getEntryFileMode.getBits }
}.flatten.headOption
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath,
permission.map { bits => FileMode.fromBits(bits) } getOrElse FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
}
builder.finish()
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
headName, loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.close()
// update refs
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// update pull request
updatePullRequests(repository.owner, repository.name, branch)
// record activity
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo))
// create issue comment by commit message
createIssueComment(repository.owner, repository.name, commitInfo)
// close issue by commit message
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
}
}
}
}
}
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
val revision = name.stripSuffix(suffix)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val oid = git.getRepository.resolve(revision)
val revCommit = JGitUtil.getRevCommitFromId(git, oid)
val sha1 = oid.getName()
val sha1 = oid.getName()
val repositorySuffix = (if(sha1.startsWith(revision)) sha1 else revision).replace('/','-')
val filename = repository.name + "-" + repositorySuffix + suffix
@@ -694,6 +895,26 @@ trait RepositoryViewerControllerBase extends ControllerBase {
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def conflict: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
val owner = params("owner")
val repository = params("repository")
val branch = params("branch")
LockUtil.lock(s"${owner}/${repository}") {
using(Git.open(getRepositoryDir(owner, repository))) { git =>
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName)
if(headTip.getName != value){
Some("Someone pushed new commits before you. Please reload this page and re-apply your changes.")
} else {
None
}
}
}
}
}
override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = {
e.printStackTrace()
}

View File

@@ -6,19 +6,29 @@ import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
import gitbucket.core.util.{AdminAuthenticator, Mailer}
import gitbucket.core.ssh.SshServer
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository}
import SystemSettingsService._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.StringUtil._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.{FileUtils, IOUtils}
import org.scalatra.forms._
import org.apache.commons.io.IOUtils
import org.scalatra.i18n.Messages
import com.github.zafarkhaja.semver.{Version => Semver}
import gitbucket.core.GitBucketCoreModule
import org.scalatra._
import org.json4s.jackson.Serialization
import scala.collection.JavaConverters._
import scala.collection.mutable.ListBuffer
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with RepositoryService with AdminAuthenticator
case class Table(name: String, columns: Seq[Column])
case class Column(name: String, primaryKey: Boolean)
trait SystemSettingsControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with AdminAuthenticator =>
@@ -59,7 +69,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(Ldap.apply)),
"skinName" -> trim(label("AdminLTE skin name", text(required)))
)(SystemSettings.apply).verifying { settings =>
Vector(
if(settings.ssh && settings.baseUrl.isEmpty){
@@ -106,7 +117,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
"password" -> trim(label("Password" ,text(required, maxlength(20), password))),
"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())),
@@ -117,7 +128,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val editUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
"password" -> trim(label("Password" ,optional(text(maxlength(20), password)))),
"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())),
@@ -147,6 +158,71 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
)(EditGroupForm.apply)
get("/admin/dbviewer")(adminOnly {
val conn = request2Session(request).conn
val meta = conn.getMetaData
val tables = ListBuffer[Table]()
using(meta.getTables(null, "%", "%", Array("TABLE", "VIEW"))){ rs =>
while(rs.next()){
val tableName = rs.getString("TABLE_NAME")
val pkColumns = ListBuffer[String]()
using(meta.getPrimaryKeys(null, null, tableName)){ rs =>
while(rs.next()){
pkColumns += rs.getString("COLUMN_NAME").toUpperCase
}
}
val columns = ListBuffer[Column]()
using(meta.getColumns(null, "%", tableName, "%")){ rs =>
while(rs.next()){
val columnName = rs.getString("COLUMN_NAME").toUpperCase
columns += Column(columnName, pkColumns.contains(columnName))
}
}
tables += Table(tableName.toUpperCase, columns)
}
}
html.dbviewer(tables)
})
post("/admin/dbviewer/_query")(adminOnly {
contentType = formats("json")
params.get("query").collectFirst { case query if query.trim.nonEmpty =>
val trimmedQuery = query.trim
if(trimmedQuery.nonEmpty){
try {
val conn = request2Session(request).conn
using(conn.prepareStatement(query)){ stmt =>
if(trimmedQuery.toUpperCase.startsWith("SELECT")){
using(stmt.executeQuery()){ rs =>
val meta = rs.getMetaData
val columns = for(i <- 1 to meta.getColumnCount) yield {
meta.getColumnName(i)
}
val result = ListBuffer[Map[String, String]]()
while(rs.next()){
val row = columns.map { columnName =>
columnName -> Option(rs.getObject(columnName)).map(_.toString).getOrElse("<NULL>")
}.toMap
result += row
}
Ok(Serialization.write(Map("type" -> "query", "columns" -> columns, "rows" -> result)))
}
} else {
val rows = stmt.executeUpdate()
Ok(Serialization.write(Map("type" -> "update", "rows" -> rows)))
}
}
} catch {
case e: Exception =>
Ok(Serialization.write(Map("type" -> "error", "message" -> e.toString)))
}
}
} getOrElse Ok(Serialization.write(Map("type" -> "error", "message" -> "query is empty")))
})
get("/admin/system")(adminOnly {
html.system(flash.get("info"))
})
@@ -169,9 +245,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
try {
new Mailer(form.smtp).send(form.testAddress,
"Test message from GitBucket", "This is a test message from GitBucket.",
context.loginAccount.get)
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send(
to = form.testAddress,
subject = "Test message from GitBucket",
textMsg = "This is a test message from GitBucket.",
htmlMsg = None,
loginAccount = context.loginAccount
)
"Test mail has been sent to: " + form.testAddress
@@ -181,7 +261,71 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
})
get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins())
// Installed plugins
val enabledPlugins = PluginRegistry().getPlugins()
val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion)
// Plugins in the local repository
val repositoryPlugins = PluginRepository.getPlugins()
.filterNot { meta =>
enabledPlugins.exists { plugin => plugin.pluginId == meta.id &&
Semver.valueOf(plugin.pluginVersion).greaterThanOrEqualTo(Semver.valueOf(meta.latestVersion.version))
}
}.map { meta =>
(meta, meta.versions.reverse.find { version => gitbucketVersion.satisfies(version.range) })
}.collect { case (meta, Some(version)) =>
new PluginInfoBase(
pluginId = meta.id,
pluginName = meta.name,
pluginVersion = version.version,
description = meta.description
)
}
// Merge
val plugins = enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false))
html.plugins(plugins, flash.get("info"))
})
post("/admin/plugins/_reload")(adminOnly {
PluginRegistry.reload(request.getServletContext(), loadSystemSettings(), request2Session(request).conn)
flash += "info" -> "All plugins were reloaded."
redirect("/admin/plugins")
})
post("/admin/plugins/:pluginId/:version/_uninstall")(adminOnly {
val pluginId = params("pluginId")
val version = params("version")
PluginRegistry().getPlugins()
.collect { case plugin if (plugin.pluginId == pluginId && plugin.pluginVersion == version) => plugin }
.foreach { _ =>
PluginRegistry.uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn)
flash += "info" -> s"${pluginId} was uninstalled."
}
redirect("/admin/plugins")
})
post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
val pluginId = params("pluginId")
val version = params("version")
/// TODO!!!!
PluginRepository.getPlugins()
.collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version) )}
.foreach { case (meta, version) =>
version.foreach { version =>
// TODO Install version!
PluginRegistry.install(
new java.io.File(PluginHome, s".repository/${version.file}"),
request.getServletContext,
loadSystemSettings(),
request2Session(request).conn
)
flash += "info" -> s"${pluginId} was installed."
}
}
redirect("/admin/plugins")
})
@@ -225,7 +369,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
// Remove from GROUP_MEMBER and COLLABORATOR
removeUserRelatedData(userName)
}
@@ -239,6 +383,10 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
isRemoved = form.isRemoved))
updateImage(userName, form.fileId, form.clearImage)
// call hooks
if(form.isRemoved) PluginRegistry().getAccountHooks.foreach(_.deleted(userName))
redirect("/admin/users")
}
} getOrElse NotFound()
@@ -277,13 +425,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
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))
}
// // 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)

View File

@@ -0,0 +1,91 @@
package gitbucket.core.controller
import org.json4s.{JField, JObject, JString}
import org.scalatra._
import org.scalatra.json._
import org.scalatra.forms._
import org.scalatra.i18n.I18nSupport
import org.scalatra.servlet.ServletBase
/**
* Extends scalatra-forms to support the client-side validation and Ajax requests as well.
*/
trait ValidationSupport extends FormSupport { self: ServletBase with JacksonJsonSupport with I18nSupport =>
def get[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form)
get(path){
validate(form)(errors => BadRequest(), form => action(form))
}
}
def post[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form)
post(path){
validate(form)(errors => BadRequest(), form => action(form))
}
}
def put[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form)
put(path){
validate(form)(errors => BadRequest(), form => action(form))
}
}
def delete[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form)
delete(path){
validate(form)(errors => BadRequest(), form => action(form))
}
}
def ajaxGet[T](path: String, form: ValueType[T])(action: T => Any): Route = {
get(path){
validate(form)(errors => ajaxError(errors), form => action(form))
}
}
def ajaxPost[T](path: String, form: ValueType[T])(action: T => Any): Route = {
post(path){
validate(form)(errors => ajaxError(errors), form => action(form))
}
}
def ajaxDelete[T](path: String, form: ValueType[T])(action: T => Any): Route = {
delete(path){
validate(form)(errors => ajaxError(errors), form => action(form))
}
}
def ajaxPut[T](path: String, form: ValueType[T])(action: T => Any): Route = {
put(path){
validate(form)(errors => ajaxError(errors), form => action(form))
}
}
private def registerValidate[T](path: String, form: ValueType[T]) = {
post(path.replaceFirst("/$", "") + "/validate"){
contentType = "application/json"
toJson(form.validate("", multiParams, messages))
}
}
/**
* Responds errors for ajax requests.
*/
private def ajaxError(errors: Seq[(String, String)]): JObject = {
status = 400
contentType = "application/json"
toJson(errors)
}
/**
* Converts errors to JSON.
*/
private def toJson(errors: Seq[(String, String)]): JObject =
JObject(errors.map { case (key, value) =>
JField(key, JString(value))
}.toList)
}

View File

@@ -1,23 +1,26 @@
package gitbucket.core.controller
import gitbucket.core.model.WebHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.WebHookService.WebHookGollumPayload
import gitbucket.core.wiki.html
import gitbucket.core.service.{AccountService, ActivityService, RepositoryService, WikiService}
import gitbucket.core.service._
import gitbucket.core.util._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService
with WikiService with RepositoryService with AccountService with ActivityService with WebHookService
with ReadableUsersAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with ActivityService with ReadableUsersAuthenticator with ReferrerAuthenticator =>
self: WikiService with RepositoryService with AccountService with ActivityService with WebHookService
with ReadableUsersAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
@@ -73,7 +76,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, Some(from), to, true, false).filter(_.newPath == pageName + ".md"), repository,
isEditable(repository), flash.get("info"))
}
})
@@ -82,7 +85,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
html.compare(None, from, to, JGitUtil.getDiffs(git, Some(from), to, true, false), repository,
isEditable(repository), flash.get("info"))
}
})
@@ -136,6 +139,11 @@ trait WikiControllerBase extends ControllerBase {
).map { commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
callWebHookOf(repository.owner, repository.name, WebHook.Gollum){
getAccountByUserName(repository.owner).map { repositoryUser =>
WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount)
}
}
}
if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
@@ -155,11 +163,24 @@ trait WikiControllerBase extends ControllerBase {
post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) =>
if(isEditable(repository)){
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse(""), None)
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
saveWikiPage(
repository.owner,
repository.name,
form.currentPageName,
form.pageName,
form.content,
loginAccount,
form.message.getOrElse(""),
None
).map { commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
callWebHookOf(repository.owner, repository.name, WebHook.Gollum){
getAccountByUserName(repository.owner).map { repositoryUser =>
WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount)
}
}
}
if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
@@ -198,15 +219,18 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
val path = multiParams("splat").head
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve("master"))
getFileContent(repository.owner, repository.name, path).map { bytes =>
RawData(FileUtil.getContentType(path, bytes), bytes)
} getOrElse NotFound()
getPathObjectId(git, path, revCommit).map { objectId =>
responseRawFile(git, objectId, path, repository)
} getOrElse NotFound()
}
})
private def unique: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] =
getWikiPageList(params.value("owner"), params.value("repository")).find(_ == value).map(_ => "Page already exists.")
}
private def pagename: Constraint = new Constraint(){

View File

@@ -0,0 +1,25 @@
package gitbucket.core.model
trait AccountWebHookComponent extends TemplateComponent { self: Profile =>
import profile.api._
private implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
lazy val AccountWebHooks = TableQuery[AccountWebHooks]
class AccountWebHooks(tag: Tag) extends Table[AccountWebHook](tag, "ACCOUNT_WEB_HOOK") with BasicTemplate {
val url = column[String]("URL")
val token = column[Option[String]]("TOKEN")
val ctype = column[WebHookContentType]("CTYPE")
def * = (userName, url, ctype, token) <> ((AccountWebHook.apply _).tupled, AccountWebHook.unapply)
def byPrimaryKey(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind)
}
}
case class AccountWebHook(
userName: String,
url: String,
ctype: WebHookContentType,
token: Option[String]
) extends WebHook

View File

@@ -0,0 +1,34 @@
package gitbucket.core.model
trait AccountWebHookEventComponent extends TemplateComponent {
self: Profile =>
import profile.api._
import gitbucket.core.model.Profile.AccountWebHooks
lazy val AccountWebHookEvents = TableQuery[AccountWebHookEvents]
class AccountWebHookEvents(tag: Tag) extends Table[AccountWebHookEvent](tag, "ACCOUNT_WEB_HOOK_EVENT") with BasicTemplate {
val url = column[String]("URL")
val event = column[WebHook.Event]("EVENT")
def * = (userName, url, event) <> ((AccountWebHookEvent.apply _).tupled, AccountWebHookEvent.unapply)
def byAccountWebHook(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind)
def byAccountWebHook(owner: Rep[String], url: Rep[String]) =
(this.userName === userName) && (this.url === url)
def byAccountWebHook(webhook: AccountWebHooks) =
(this.userName === webhook.userName) && (this.url === webhook.url)
def byPrimaryKey(userName: String, url: String, event: WebHook.Event) =
(this.userName === userName.bind) && (this.url === url.bind) && (this.event === event.bind)
}
}
case class AccountWebHookEvent(
userName: String,
url: String,
event: WebHook.Event
)

View File

@@ -7,6 +7,10 @@ protected[model] trait TemplateComponent { self: Profile =>
val userName = column[String]("USER_NAME")
val repositoryName = column[String]("REPOSITORY_NAME")
def byAccount(userName: String) = (this.userName === userName.bind)
def byAccount(userName: Rep[String]) = (this.userName === userName)
def byRepository(owner: String, repository: String) =
(userName === owner.bind) && (repositoryName === repository.bind)
@@ -38,6 +42,20 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(owner, repository) && (this.labelName === labelName.bind)
}
trait PriorityTemplate extends BasicTemplate { self: Table[_] =>
val priorityId = column[Int]("PRIORITY_ID")
val priorityName = column[String]("PRIORITY_NAME")
def byPriority(owner: String, repository: String, priorityId: Int) =
byRepository(owner, repository) && (this.priorityId === priorityId.bind)
def byPriority(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) =
byRepository(userName, repositoryName) && (this.priorityId === priorityId)
def byPriority(owner: String, repository: String, priorityName: String) =
byRepository(owner, repository) && (this.priorityName === priorityName.bind)
}
trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
val milestoneId = column[Int]("MILESTONE_ID")

View File

@@ -13,12 +13,13 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate {
class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate {
val commentCount = column[Int]("COMMENT_COUNT")
def * = (userName, repositoryName, issueId, commentCount)
val priority = column[Int]("PRIORITY")
def * = (userName, repositoryName, issueId, commentCount, priority)
}
class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate {
class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate with PriorityTemplate {
val openedUserName = column[String]("OPENED_USER_NAME")
val assignedUserName = column[String]("ASSIGNED_USER_NAME")
val title = column[String]("TITLE")
@@ -27,7 +28,7 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
val pullRequest = column[Boolean]("PULL_REQUEST")
def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply)
def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, priorityId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
}
@@ -39,6 +40,7 @@ case class Issue(
issueId: Int,
openedUserName: String,
milestoneId: Option[Int],
priorityId: Option[Int],
assignedUserName: Option[String],
title: String,
content: Option[String],

View File

@@ -0,0 +1,43 @@
package gitbucket.core.model
trait PriorityComponent extends TemplateComponent { self: Profile =>
import profile.api._
lazy val Priorities = TableQuery[Priorities]
class Priorities(tag: Tag) extends Table[Priority](tag, "PRIORITY") with PriorityTemplate {
override val priorityId = column[Int]("PRIORITY_ID", O AutoInc)
override val priorityName = column[String]("PRIORITY_NAME")
val description = column[String]("DESCRIPTION")
val ordering = column[Int]("ORDERING")
val isDefault = column[Boolean]("IS_DEFAULT")
val color = column[String]("COLOR")
def * = (userName, repositoryName, priorityId, priorityName, description.?, isDefault, ordering, color) <> (Priority.tupled, Priority.unapply)
def byPrimaryKey(owner: String, repository: String, priorityId: Int) = byPriority(owner, repository, priorityId)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) = byPriority(userName, repositoryName, priorityId)
}
}
case class Priority (
userName: String,
repositoryName: String,
priorityId: Int = 0,
priorityName: String,
description: Option[String],
isDefault: Boolean,
ordering: Int = 0,
color: String){
val fontColor = {
val r = color.substring(0, 2)
val g = color.substring(2, 4)
val b = color.substring(4, 6)
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000"
} else {
"ffffff"
}
}
}

View File

@@ -15,6 +15,11 @@ trait Profile {
t => new java.util.Date(t.getTime)
)
/**
* WebHookBase.Event Column Types
*/
implicit val eventColumnType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_))
/**
* Extends Column to add conditional condition
*/
@@ -47,12 +52,15 @@ trait CoreProfile extends ProfileProvider with Profile
with IssueCommentComponent
with IssueLabelComponent
with LabelComponent
with PriorityComponent
with MilestoneComponent
with PullRequestComponent
with RepositoryComponent
with SshKeyComponent
with WebHookComponent
with WebHookEventComponent
with RepositoryWebHookComponent
with RepositoryWebHookEventComponent
with AccountWebHookComponent
with AccountWebHookEventComponent
with ProtectedBranchComponent
with DeployKeyComponent
with ReleaseComponent

View File

@@ -0,0 +1,27 @@
package gitbucket.core.model
trait RepositoryWebHookComponent extends TemplateComponent { self: Profile =>
import profile.api._
implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks]
class RepositoryWebHooks(tag: Tag) extends Table[RepositoryWebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL")
val token = column[Option[String]]("TOKEN")
val ctype = column[WebHookContentType]("CTYPE")
def * = (userName, repositoryName, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
}
}
case class RepositoryWebHook(
userName: String,
repositoryName: String,
url: String,
ctype: WebHookContentType,
token: Option[String]
) extends WebHook

View File

@@ -0,0 +1,28 @@
package gitbucket.core.model
trait RepositoryWebHookEventComponent extends TemplateComponent { self: Profile =>
import profile.api._
import gitbucket.core.model.Profile.RepositoryWebHooks
lazy val RepositoryWebHookEvents = TableQuery[RepositoryWebHookEvents]
class RepositoryWebHookEvents(tag: Tag) extends Table[RepositoryWebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate {
val url = column[String]("URL")
val event = column[WebHook.Event]("EVENT")
def * = (userName, repositoryName, url, event) <> ((RepositoryWebHookEvent.apply _).tupled, RepositoryWebHookEvent.unapply)
def byRepositoryWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
def byRepositoryWebHook(owner: Rep[String], repository: Rep[String], url: Rep[String]) =
byRepository(userName, repositoryName) && (this.url === url)
def byRepositoryWebHook(webhook: RepositoryWebHooks) =
byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url)
def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byRepositoryWebHook(owner, repository, url) && (this.event === event.bind)
}
}
case class RepositoryWebHookEvent(
userName: String,
repositoryName: String,
url: String,
event: WebHook.Event
)

View File

@@ -1,22 +1,5 @@
package gitbucket.core.model
trait WebHookComponent extends TemplateComponent { self: Profile =>
import profile.api._
implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
lazy val WebHooks = TableQuery[WebHooks]
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL")
val token = column[Option[String]]("TOKEN")
val ctype = column[WebHookContentType]("CTYPE")
def * = (userName, repositoryName, url, ctype, token) <> ((WebHook.apply _).tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
}
}
abstract sealed case class WebHookContentType(code: String, ctype: String)
object WebHookContentType {
@@ -33,13 +16,11 @@ object WebHookContentType {
def valueOpt(code: String): Option[WebHookContentType] = map.get(code)
}
case class WebHook(
userName: String,
repositoryName: String,
url: String,
ctype: WebHookContentType,
token: Option[String]
)
trait WebHook{
val url: String
val ctype: WebHookContentType
val token: Option[String]
}
object WebHook {
abstract sealed class Event(val name: String)
@@ -86,6 +67,7 @@ object WebHook {
TeamAdd,
Watch
)
private val map: Map[String,Event] = values.map(e => e.name -> e).toMap
def valueOf(name: String): Event = map(name)
def valueOpt(name: String): Option[Event] = map.get(name)

View File

@@ -1,30 +0,0 @@
package gitbucket.core.model
trait WebHookEventComponent extends TemplateComponent { self: Profile =>
import profile.api._
import gitbucket.core.model.Profile.WebHooks
lazy val WebHookEvents = TableQuery[WebHookEvents]
implicit val typedType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_))
class WebHookEvents(tag: Tag) extends Table[WebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate {
val url = column[String]("URL")
val event = column[WebHook.Event]("EVENT")
def * = (userName, repositoryName, url, event) <> ((WebHookEvent.apply _).tupled, WebHookEvent.unapply)
def byWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
def byWebHook(owner: Rep[String], repository: Rep[String], url: Rep[String]) =
byRepository(userName, repositoryName) && (this.url === url)
def byWebHook(webhook: WebHooks) =
byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url)
def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byWebHook(owner, repository, url) && (this.event === event.bind)
}
}
case class WebHookEvent(
userName: String,
repositoryName: String,
url: String,
event: WebHook.Event
)

View File

@@ -0,0 +1,10 @@
package gitbucket.core.plugin
import gitbucket.core.model.Profile._
import profile.api._
trait AccountHook {
def deleted(userName: String)(implicit session: Session): Unit = ()
}

View File

@@ -0,0 +1,22 @@
package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.model.Issue
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.model.Profile._
import profile.api._
trait IssueHook {
def created(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
}
trait PullRequestHook extends IssueHook {
def merged(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
}

View File

@@ -1,12 +1,15 @@
package gitbucket.core.plugin
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.Account
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.SyntaxSugars._
import io.github.gitbucket.solidbase.model.Version
import org.apache.sshd.server.Command
import play.twirl.api.Html
/**
* Trait for define plugin interface.
@@ -69,6 +72,16 @@ abstract class Plugin {
*/
def repositoryRoutings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[GitRepositoryRouting] = Nil
/**
* Override to add account hooks.
*/
val accountHooks: Seq[AccountHook] = Nil
/**
* Override to add account hooks.
*/
def accountHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[AccountHook] = Nil
/**
* Override to add receive hooks.
*/
@@ -89,6 +102,36 @@ abstract class Plugin {
*/
def repositoryHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[RepositoryHook] = Nil
/**
* Override to add issue hooks.
*/
val issueHooks: Seq[IssueHook] = Nil
/**
* Override to add issue hooks.
*/
def issueHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[IssueHook] = Nil
/**
* Override to add pull request hooks.
*/
val pullRequestHooks: Seq[PullRequestHook] = Nil
/**
* Override to add pull request hooks.
*/
def pullRequestHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[PullRequestHook] = Nil
/**
* Override to add repository headers.
*/
val repositoryHeaders: Seq[(RepositoryInfo, Context) => Option[Html]] = Nil
/**
* Override to add repository headers.
*/
def repositoryHeaders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Html]] = Nil
/**
* Override to add global menus.
*/
@@ -159,6 +202,16 @@ abstract class Plugin {
*/
def dashboardTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
/**
* Override to add issue sidebars.
*/
val issueSidebars: Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = Nil
/**
* Override to add issue sidebars.
*/
def issueSidebars(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = Nil
/**
* Override to add assets mappings.
*/
@@ -189,6 +242,17 @@ abstract class Plugin {
*/
def suggestionProviders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[SuggestionProvider] = Nil
/**
* Override to add ssh command providers.
*/
val sshCommandProviders: Seq[PartialFunction[String, Command]] = Nil
/**
* Override to add ssh command providers.
*/
def sshCommandProviders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[PartialFunction[String, Command]] = Nil
/**
* This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry.
@@ -209,12 +273,24 @@ abstract class Plugin {
(repositoryRoutings ++ repositoryRoutings(registry, context, settings)).foreach { routing =>
registry.addRepositoryRouting(routing)
}
(accountHooks ++ accountHooks(registry, context, settings)).foreach { accountHook =>
registry.addAccountHook(accountHook)
}
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
registry.addReceiveHook(receiveHook)
}
(repositoryHooks ++ repositoryHooks(registry, context, settings)).foreach { repositoryHook =>
registry.addRepositoryHook(repositoryHook)
}
(issueHooks ++ issueHooks(registry, context, settings)).foreach { issueHook =>
registry.addIssueHook(issueHook)
}
(pullRequestHooks ++ pullRequestHooks(registry, context, settings)).foreach { pullRequestHook =>
registry.addPullRequestHook(pullRequestHook)
}
(repositoryHeaders ++ repositoryHeaders(registry, context, settings)).foreach { repositoryHeader =>
registry.addRepositoryHeader(repositoryHeader)
}
(globalMenus ++ globalMenus(registry, context, settings)).foreach { globalMenu =>
registry.addGlobalMenu(globalMenu)
}
@@ -236,6 +312,9 @@ abstract class Plugin {
(dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab =>
registry.addDashboardTab(dashboardTab)
}
(issueSidebars ++ issueSidebars(registry, context, settings)).foreach { issueSidebarComponent =>
registry.addIssueSidebar(issueSidebarComponent)
}
(assetsMappings ++ assetsMappings(registry, context, settings)).foreach { assetMapping =>
registry.addAssetsMapping((assetMapping._1, assetMapping._2, getClass.getClassLoader))
}
@@ -245,14 +324,23 @@ abstract class Plugin {
(suggestionProviders ++ suggestionProviders(registry, context, settings)).foreach { suggestionProvider =>
registry.addSuggestionProvider(suggestionProvider)
}
(sshCommandProviders ++ sshCommandProviders(registry, context, settings)).foreach { sshCommandProvider =>
registry.addSshCommandProvider(sshCommandProvider)
}
}
/**
* This method is invoked in shutdown of plugin system.
* This method is invoked when the plugin system is shutting down.
* If the plugin has any resources, release them in this method.
*/
def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
// /**
// * This method is invoked when this plugin is uninstalled.
// * Cleanup database or any other resources in this method if necessary.
// */
// def uninstall(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
/**
* Helper method to get a resource from classpath.
*/

View File

@@ -1,233 +0,0 @@
package gitbucket.core.plugin
import java.io.{File, FilenameFilter, InputStream}
import java.net.URLClassLoader
import java.util.Base64
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.Account
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module
import org.slf4j.LoggerFactory
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
class PluginRegistry {
private val plugins = new ListBuffer[PluginInfo]
private val javaScripts = new ListBuffer[(String, String)]
private val controllers = new ListBuffer[(ControllerBase, String)]
private val images = mutable.Map[String, String]()
private val renderers = mutable.Map[String, Renderer]()
renderers ++= Seq(
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
)
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
private val receiveHooks = new ListBuffer[ReceiveHook]
receiveHooks += new ProtectedBranchReceiveHook()
private val repositoryHooks = new ListBuffer[RepositoryHook]
private val globalMenus = new ListBuffer[(Context) => Option[Link]]
private val repositoryMenus = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
private val repositorySettingTabs = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
private val profileTabs = new ListBuffer[(Account, Context) => Option[Link]]
private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
private val assetsMappings = new ListBuffer[(String, String, ClassLoader)]
private val textDecorators = new ListBuffer[TextDecorator]
private val suggestionProviders = new ListBuffer[SuggestionProvider]
suggestionProviders += new UserNameSuggestionProvider()
def addPlugin(pluginInfo: PluginInfo): Unit = plugins += pluginInfo
def getPlugins(): List[PluginInfo] = plugins.toList
def addImage(id: String, bytes: Array[Byte]): Unit = {
val encoded = Base64.getEncoder.encodeToString(bytes)
images += ((id, encoded))
}
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
def addImage(id: String, in: InputStream): Unit = {
val bytes = using(in){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
addImage(id, bytes)
}
def getImage(id: String): String = images(id)
def addController(path: String, controller: ControllerBase): Unit = controllers += ((controller, path))
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = addController(path, controller)
def getControllers(): Seq[(ControllerBase, String)] = controllers.toSeq
def addJavaScript(path: String, script: String): Unit = javaScripts += ((path, script))
def getJavaScript(currentPath: String): List[String] = javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
def addRenderer(extension: String, renderer: Renderer): Unit = renderers += ((extension, renderer))
def getRenderer(extension: String): Renderer = renderers.getOrElse(extension, DefaultRenderer)
def renderableExtensions: Seq[String] = renderers.keys.toSeq
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings += routing
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.toSeq
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
PluginRegistry().getRepositoryRoutings().find {
case GitRepositoryRouting(urlPath, _, _) => {
repositoryPath.matches("/" + urlPath + "(/.*)?")
}
}
}
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks += commitHook
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
def addRepositoryHook(repositoryHook: RepositoryHook): Unit = repositoryHooks += repositoryHook
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.toSeq
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus += globalMenu
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus += repositoryMenu
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs += repositorySettingTab
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs += profileTab
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus += systemSettingMenu
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus += accountSettingMenu
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = dashboardTabs += dashboardTab
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings += assetsMapping
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.toSeq
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators += textDecorator
def getTextDecorators: Seq[TextDecorator] = textDecorators.toSeq
def addSuggestionProvider(suggestionProvider: SuggestionProvider): Unit = suggestionProviders += suggestionProvider
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.toSeq
}
/**
* Provides entry point to PluginRegistry.
*/
object PluginRegistry {
private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
private val instance = new PluginRegistry()
/**
* Returns the PluginRegistry singleton instance.
*/
def apply(): PluginRegistry = instance
/**
* Initializes all installed plugins.
*/
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
val pluginDir = new File(PluginHome)
val manager = new JDBCVersionManager(conn)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
}).foreach { pluginJar =>
val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
try {
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
// Migration
val solidbase = new Solidbase()
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
// Check version
val databaseVersion = manager.getCurrentVersion(plugin.pluginId)
val pluginVersion = plugin.versions.last.getVersion
if(databaseVersion != pluginVersion){
throw new IllegalStateException(s"Plugin version is ${pluginVersion}, but database version is ${databaseVersion}")
}
// Initialize
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
pluginVersion = plugin.versions.last.getVersion,
description = plugin.description,
pluginClass = plugin
))
} catch {
case e: Throwable => {
logger.error(s"Error during plugin initialization: ${pluginJar.getAbsolutePath}", e)
}
}
}
}
}
def shutdown(context: ServletContext, settings: SystemSettings): Unit = {
instance.getPlugins().foreach { pluginInfo =>
try {
pluginInfo.pluginClass.shutdown(instance, context, settings)
} catch {
case e: Exception => {
logger.error(s"Error during plugin shutdown", e)
}
}
}
}
}
case class Link(id: String, label: String, path: String, icon: Option[String] = None)
case class PluginInfo(
pluginId: String,
pluginName: String,
pluginVersion: String,
description: String,
pluginClass: Plugin
)

View File

@@ -0,0 +1,425 @@
package gitbucket.core.plugin
import java.io.{File, FilenameFilter, InputStream}
import java.net.URLClassLoader
import java.nio.file.{Files, Paths, StandardWatchEventKinds}
import java.util.Base64
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentHashMap
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module
import org.apache.commons.io.FileUtils
import org.apache.sshd.server.Command
import org.slf4j.LoggerFactory
import play.twirl.api.Html
import scala.collection.JavaConverters._
class PluginRegistry {
private val plugins = new ConcurrentLinkedQueue[PluginInfo]
private val javaScripts = new ConcurrentLinkedQueue[(String, String)]
private val controllers = new ConcurrentLinkedQueue[(ControllerBase, String)]
private val images = new ConcurrentHashMap[String, String]
private val renderers = new ConcurrentHashMap[String, Renderer]
renderers.put("md", MarkdownRenderer)
renderers.put("markdown", MarkdownRenderer)
private val repositoryRoutings = new ConcurrentLinkedQueue[GitRepositoryRouting]
private val accountHooks = new ConcurrentLinkedQueue[AccountHook]
private val receiveHooks = new ConcurrentLinkedQueue[ReceiveHook]
receiveHooks.add(new ProtectedBranchReceiveHook())
private val repositoryHooks = new ConcurrentLinkedQueue[RepositoryHook]
private val issueHooks = new ConcurrentLinkedQueue[IssueHook]
private val pullRequestHooks = new ConcurrentLinkedQueue[PullRequestHook]
private val repositoryHeaders = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Html]]
private val globalMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val repositoryMenus = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Link]]
private val repositorySettingTabs = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Link]]
private val profileTabs = new ConcurrentLinkedQueue[(Account, Context) => Option[Link]]
private val systemSettingMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val accountSettingMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val dashboardTabs = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val issueSidebars = new ConcurrentLinkedQueue[(Issue, RepositoryInfo, Context) => Option[Html]]
private val assetsMappings = new ConcurrentLinkedQueue[(String, String, ClassLoader)]
private val textDecorators = new ConcurrentLinkedQueue[TextDecorator]
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
suggestionProviders.add(new UserNameSuggestionProvider())
private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]()
def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo)
def getPlugins(): List[PluginInfo] = plugins.asScala.toList
def addImage(id: String, bytes: Array[Byte]): Unit = {
val encoded = Base64.getEncoder.encodeToString(bytes)
images.put(id, encoded)
}
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
def addImage(id: String, in: InputStream): Unit = {
val bytes = using(in){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
addImage(id, bytes)
}
def getImage(id: String): String = images.get(id)
def addController(path: String, controller: ControllerBase): Unit = controllers.add((controller, path))
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = addController(path, controller)
def getControllers(): Seq[(ControllerBase, String)] = controllers.asScala.toSeq
def addJavaScript(path: String, script: String): Unit = javaScripts.add((path, script)) //javaScripts += ((path, script))
def getJavaScript(currentPath: String): List[String] = javaScripts.asScala.filter(x => currentPath.matches(x._1)).toList.map(_._2)
def addRenderer(extension: String, renderer: Renderer): Unit = renderers.put(extension, renderer)
def getRenderer(extension: String): Renderer = renderers.asScala.getOrElse(extension, DefaultRenderer)
def renderableExtensions: Seq[String] = renderers.keys.asScala.toSeq
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings.add(routing)
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.asScala.toSeq
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
PluginRegistry().getRepositoryRoutings().find {
case GitRepositoryRouting(urlPath, _, _) => {
repositoryPath.matches("/" + urlPath + "(/.*)?")
}
}
}
def addAccountHook(accountHook: AccountHook): Unit = accountHooks.add(accountHook)
def getAccountHooks: Seq[AccountHook] = accountHooks.asScala.toSeq
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks.add(commitHook)
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.asScala.toSeq
def addRepositoryHook(repositoryHook: RepositoryHook): Unit = repositoryHooks.add(repositoryHook)
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.asScala.toSeq
def addIssueHook(issueHook: IssueHook): Unit = issueHooks.add(issueHook)
def getIssueHooks: Seq[IssueHook] = issueHooks.asScala.toSeq
def addPullRequestHook(pullRequestHook: PullRequestHook): Unit = pullRequestHooks.add(pullRequestHook)
def getPullRequestHooks: Seq[PullRequestHook] = pullRequestHooks.asScala.toSeq
def addRepositoryHeader(repositoryHeader: (RepositoryInfo, Context) => Option[Html]): Unit = repositoryHeaders.add(repositoryHeader)
def getRepositoryHeaders: Seq[(RepositoryInfo, Context) => Option[Html]] = repositoryHeaders.asScala.toSeq
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus.add(globalMenu)
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.asScala.toSeq
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus.add(repositoryMenu)
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.asScala.toSeq
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs.add(repositorySettingTab)
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.asScala.toSeq
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs.add(profileTab)
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.asScala.toSeq
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus.add(systemSettingMenu)
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.asScala.toSeq
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus.add(accountSettingMenu)
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.asScala.toSeq
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = dashboardTabs.add(dashboardTab)
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.asScala.toSeq
def addIssueSidebar(issueSidebar: (Issue, RepositoryInfo, Context) => Option[Html]): Unit = issueSidebars.add(issueSidebar)
def getIssueSidebars: Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = issueSidebars.asScala.toSeq
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings.add(assetsMapping)
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.asScala.toSeq
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators.add(textDecorator)
def getTextDecorators: Seq[TextDecorator] = textDecorators.asScala.toSeq
def addSuggestionProvider(suggestionProvider: SuggestionProvider): Unit = suggestionProviders.add(suggestionProvider)
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.asScala.toSeq
def addSshCommandProvider(sshCommandProvider: PartialFunction[String, Command]): Unit = sshCommandProviders.add(sshCommandProvider)
def getSshCommandProviders: Seq[PartialFunction[String, Command]] = sshCommandProviders.asScala.toSeq
}
/**
* Provides entry point to PluginRegistry.
*/
object PluginRegistry {
private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
private var instance = new PluginRegistry()
private var watcher: PluginWatchThread = null
private var extraWatcher: PluginWatchThread = null
private val initializing = new AtomicBoolean(false)
/**
* Returns the PluginRegistry singleton instance.
*/
def apply(): PluginRegistry = instance
/**
* Reload all plugins.
*/
def reload(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
shutdown(context, settings)
instance = new PluginRegistry()
initialize(context, settings, conn)
}
/**
* Uninstall a specified plugin.
*/
def uninstall(pluginId: String, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
instance.getPlugins()
.collect { case plugin if plugin.pluginId == pluginId => plugin }
.foreach { plugin =>
// try {
// plugin.pluginClass.uninstall(instance, context, settings)
// } catch {
// case e: Exception =>
// logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e)
// }
shutdown(context, settings)
plugin.pluginJar.delete()
instance = new PluginRegistry()
initialize(context, settings, conn)
}
}
/**
* Install a plugin from a specified jar file.
*/
def install(file: File, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
shutdown(context, settings)
FileUtils.copyFile(file, new File(PluginHome, file.getName))
instance = new PluginRegistry()
initialize(context, settings, conn)
}
private def listPluginJars(dir: File): Seq[File] = {
dir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
}).toSeq.sortBy(_.getName).reverse
}
lazy val extraPluginDir: Option[String] = Option(System.getProperty("gitbucket.pluginDir"))
/**
* Initializes all installed plugins.
*/
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
val pluginDir = new File(PluginHome)
val manager = new JDBCVersionManager(conn)
// Clean installed directory
val installedDir = new File(PluginHome, ".installed")
if(installedDir.exists){
FileUtils.deleteDirectory(installedDir)
}
installedDir.mkdirs()
val pluginJars = listPluginJars(pluginDir)
val extraJars = extraPluginDir.map { extraDir => listPluginJars(new File(extraDir)) }.getOrElse(Nil)
(extraJars ++ pluginJars).foreach { pluginJar =>
val installedJar = new File(installedDir, pluginJar.getName)
FileUtils.copyFile(pluginJar, installedJar)
logger.info(s"Initialize ${pluginJar.getName}")
val classLoader = new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
try {
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
val pluginId = plugin.pluginId
// Check duplication
instance.getPlugins().find(_.pluginId == pluginId) match {
case Some(x) => {
logger.warn(s"Plugin ${pluginId} is duplicated. ${x.pluginJar.getName} is available.")
}
case None => {
// Migration
val solidbase = new Solidbase()
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
// Check database version
val databaseVersion = manager.getCurrentVersion(plugin.pluginId)
val pluginVersion = plugin.versions.last.getVersion
if (databaseVersion != pluginVersion) {
throw new IllegalStateException(s"Plugin version is ${pluginVersion}, but database version is ${databaseVersion}")
}
// Initialize
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
pluginVersion = plugin.versions.last.getVersion,
description = plugin.description,
pluginClass = plugin,
pluginJar = pluginJar,
classLoader = classLoader
))
}
}
} catch {
case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
}
}
if(watcher == null){
watcher = new PluginWatchThread(context, PluginHome)
watcher.start()
}
extraPluginDir.foreach { extraDir =>
if(extraWatcher == null){
extraWatcher = new PluginWatchThread(context, extraDir)
extraWatcher.start()
}
}
}
def shutdown(context: ServletContext, settings: SystemSettings): Unit = synchronized {
instance.getPlugins().foreach { plugin =>
try {
plugin.pluginClass.shutdown(instance, context, settings)
if(watcher != null){
watcher.interrupt()
watcher = null
}
if(extraWatcher != null){
extraWatcher.interrupt()
extraWatcher = null
}
} catch {
case e: Exception => {
logger.error(s"Error during plugin shutdown: ${plugin.pluginJar.getName}", e)
}
} finally {
plugin.classLoader.close()
}
}
}
}
case class Link(
id: String,
label: String,
path: String,
icon: Option[String] = None
)
class PluginInfoBase(
val pluginId: String,
val pluginName: String,
val pluginVersion: String,
val description: String
)
case class PluginInfo(
override val pluginId: String,
override val pluginName: String,
override val pluginVersion: String,
override val description: String,
pluginClass: Plugin,
pluginJar: File,
classLoader: URLClassLoader
) extends PluginInfoBase(pluginId, pluginName, pluginVersion, description)
class PluginWatchThread(context: ServletContext, dir: String) extends Thread with SystemSettingsService {
import gitbucket.core.model.Profile.profile.blockingApi._
import scala.collection.JavaConverters._
private val logger = LoggerFactory.getLogger(classOf[PluginWatchThread])
override def run(): Unit = {
val path = Paths.get(dir)
if(!Files.exists(path)){
Files.createDirectories(path)
}
val fs = path.getFileSystem
val watcher = fs.newWatchService
val watchKey = path.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.OVERFLOW)
logger.info("Start PluginWatchThread: " + path)
try {
while (watchKey.isValid()) {
val detectedWatchKey = watcher.take()
val events = detectedWatchKey.pollEvents.asScala.filter { e =>
e.context.toString != ".installed" && !e.context.toString.endsWith(".bak")
}
if(events.nonEmpty){
events.foreach { event =>
logger.info(event.kind + ": " + event.context)
}
new Thread {
override def run(): Unit = {
gitbucket.core.servlet.Database() withTransaction { session =>
logger.info("Reloading plugins...")
PluginRegistry.reload(context, loadSystemSettings(), session.conn)
logger.info("Reloading finished.")
}
}
}.start()
}
detectedWatchKey.reset()
}
} catch {
case _: InterruptedException => watchKey.cancel()
}
logger.info("Shutdown PluginWatchThread")
}
}

View File

@@ -0,0 +1,43 @@
package gitbucket.core.plugin
import org.json4s._
import gitbucket.core.util.Directory._
import org.apache.commons.io.FileUtils
object PluginRepository {
implicit val formats = DefaultFormats
def parsePluginJson(json: String): Seq[PluginMetadata] = {
org.json4s.jackson.JsonMethods.parse(json).extract[Seq[PluginMetadata]]
}
lazy val LocalRepositoryDir = new java.io.File(PluginHome, ".repository")
lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json")
def getPlugins(): Seq[PluginMetadata] = {
if(LocalRepositoryIndexFile.exists){
parsePluginJson(FileUtils.readFileToString(LocalRepositoryIndexFile, "UTF-8"))
} else Nil
}
}
// Mapped from plugins.json
case class PluginMetadata(
id: String,
name: String,
description: String,
versions: Seq[VersionDef],
default: Boolean = false
){
lazy val latestVersion: VersionDef = versions.last
}
case class VersionDef(
version: String,
url: String,
range: String
){
lazy val file = url.substring(url.lastIndexOf("/") + 1)
}

View File

@@ -3,6 +3,7 @@ package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService
import gitbucket.core.view.Markdown
import gitbucket.core.view.helpers.urlLink
import play.twirl.api.Html
/**
@@ -33,12 +34,7 @@ object MarkdownRenderer extends Renderer {
object DefaultRenderer extends Renderer {
override def render(request: RenderRequest): Html = {
import request._
Html(
s"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
Html(s"""<tt><pre class="plain">${urlLink(request.fileContent)}</pre></tt>""")
}
}
@@ -51,4 +47,4 @@ case class RenderRequest(
enableRefsLink: Boolean,
enableAnchor: Boolean,
context: Context
)
)

View File

@@ -3,15 +3,92 @@ package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService.RepositoryInfo
/**
* The base trait of suggestion providers which supplies completion proposals in some text areas.
*/
trait SuggestionProvider {
/**
* The identifier of this suggestion provider.
* You must specify the unique identifier in the all suggestion providers.
*/
val id: String
/**
* The trigger of this suggestion provider. When user types this character, the proposal list would be displayed.
* Also this is used as the prefix of the replaced string.
*/
val prefix: String
/**
* The suffix of the replaced string. The default is `" "`.
*/
val suffix: String = " "
/**
* Which contexts is this suggestion provider enabled. Currently, available contexts are `"issues"` and `"wiki"`.
*/
val context: Seq[String]
def values(repository: RepositoryInfo): Seq[String]
def template(implicit context: Context): String = "value"
/**
* If this suggestion provider has static proposal list, override this method to return it.
*
* The returned sequence is rendered as follows:
* <pre>
* [
* {
* "label" -> "value1",
* "value" -> "value1"
* },
* {
* "label" -> "value2",
* "value" -> "value2"
* },
* ]
* </pre>
*
* Each element can be accessed as `option` in `template()` or `replace()` method.
*/
def values(repository: RepositoryInfo): Seq[String] = Nil
/**
* If this suggestion provider has static proposal list, override this method to return it.
*
* If your proposals have label and value, use this method instead of `values()`.
* The first element of tuple is used as a value, and the second element is used as a label.
*
* The returned sequence is rendered as follows:
* <pre>
* [
* {
* "label" -> "label1",
* "value" -> "value1"
* },
* {
* "label" -> "label2",
* "value" -> "value2"
* },
* ]
* </pre>
*
* Each element can be accessed as `option` in `template()` or `replace()` method.
*/
def options(repository: RepositoryInfo): Seq[(String, String)] = values(repository).map { value => (value, value) }
/**
* JavaScript fragment to generate a label of completion proposal. The default is: `option.label`.
*/
def template(implicit context: Context): String = "option.label"
/**
* JavaScript fragment to generate a replaced value of completion proposal. The default is: `option.value`
*/
def replace(implicit context: Context): String = "option.value"
/**
* If this suggestion provider needs some additional process to assemble the proposal list (e.g. It need to use Ajax
* to get a proposal list from the server), then override this method and return any JavaScript code.
*/
def additionalScript(implicit context: Context): String = ""
}
@@ -20,8 +97,6 @@ class UserNameSuggestionProvider extends SuggestionProvider {
override val id: String = "user"
override val prefix: String = "@"
override val context: Seq[String] = Seq("issues")
override def values(repository: RepositoryInfo): Seq[String] = Nil
override def template(implicit context: Context): String = "'@' + value"
override def additionalScript(implicit context: Context): String =
s"""$$.get('${context.path}/_user/proposals', { query: '', user: true, group: false }, function (data) { user = data.options; });"""
}
}

View File

@@ -132,7 +132,7 @@ trait ActivityService {
Activities insert Activity(userName, repositoryName, activityUserName,
"push",
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
Some(commits.take(5).map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
currentDate)
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,

View File

@@ -2,11 +2,10 @@ package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.Issue
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Notifier
trait HandleCommentService {
self: RepositoryService with IssuesService with ActivityService
@@ -21,7 +20,7 @@ trait HandleCommentService {
defining(repository.owner, repository.name){ case (owner, name) =>
val userName = loginAccount.userName
val (action, recordActivity) = actionOpt
val (action, actionActivity) = actionOpt
.collect {
case "close" if(!issue.closed) => true ->
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
@@ -36,54 +35,55 @@ trait HandleCommentService {
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")))
case (None, Some(action)) =>
Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
case (Some(content), _) =>
val id = Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
// record comment activity
if(issue.isPullRequest) recordCommentPullRequestActivity(owner, name, userName, issue.issueId, content)
else recordCommentIssueActivity(owner, name, userName, issue.issueId, content)
// extract references and create refer comment
createReferComment(owner, name, issue, content, loginAccount)
id
}
// 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, loginAccount)
}
actionActivity.foreach { f => f(owner, name, userName, issue.issueId, issue.title) }
// call web hooks
action match {
case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, loginAccount) }
case Some(act) => {
case None => commentId foreach (callIssueCommentWebHook(repository, issue, _, loginAccount))
case Some(act) =>
val webHookAction = act match {
case "open" => "opened"
case "reopen" => "reopened"
case "close" => "closed"
case _ => act
case "reopen" => "reopened"
}
if (issue.isPullRequest) {
if(issue.isPullRequest)
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, loginAccount)
} else {
else
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, loginAccount)
}
}
}
// 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}")
}
}
// call hooks
content foreach { x =>
if(issue.isPullRequest)
PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId.get, x, issue, repository))
else
PluginRegistry().getIssueHooks.foreach(_.addedComment(commentId.get, x, issue, repository))
}
action foreach {
case "close" =>
if(issue.isPullRequest)
PluginRegistry().getPullRequestHooks.foreach(_.closed(issue, repository))
else
PluginRegistry().getIssueHooks.foreach(_.closed(issue, repository))
case "reopen" =>
if(issue.isPullRequest)
PluginRegistry().getPullRequestHooks.foreach(_.reopened(issue, repository))
else
PluginRegistry().getIssueHooks.foreach(_.reopened(issue, repository))
}
commentId.map( issue -> _ )

View File

@@ -3,17 +3,16 @@ package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.Notifier
import gitbucket.core.util.Implicits._
// TODO: Merged with IssuesService?
trait IssueCreationService {
self: RepositoryService with WebHookIssueCommentService with LabelsService with IssuesService with ActivityService =>
def createIssue(repository: RepositoryInfo, title:String, body:Option[String],
assignee: Option[String], milestoneId: Option[Int], labelNames: Seq[String],
assignee: Option[String], milestoneId: Option[Int], priorityId: Option[Int], labelNames: Seq[String],
loginAccount: Account)(implicit context: Context, s: Session) : Issue = {
val owner = repository.owner
@@ -24,7 +23,8 @@ trait IssueCreationService {
// insert issue
val issueId = insertIssue(owner, name, userName, title, body,
if (manageable) assignee else None,
if (manageable) milestoneId else None)
if (manageable) milestoneId else None,
if (manageable) priorityId else None)
val issue: Issue = getIssue(owner, name, issueId.toString).get
// insert labels
@@ -46,10 +46,9 @@ trait IssueCreationService {
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, loginAccount)
// notifications
Notifier().toNotify(repository, issue, body.getOrElse("")) {
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
}
// call hooks
PluginRegistry().getIssueHooks.foreach(_.created(issue, repository))
issue
}

View File

@@ -32,8 +32,11 @@ trait IssuesService {
.list
def getMergedComment(owner: String, repository: String, issueId: Int)(implicit s: Session): Option[(IssueComment, Account)] = {
getCommentsForApi(owner, repository, issueId)
.collectFirst { case (comment, account, _) if comment.action == "merged" => (comment, account) }
IssueComments.filter(_.byIssue(owner, repository, issueId))
.filter(_.action === "merge".bind)
.join(Accounts).on { case t1 ~ t2 => t1.commentedUserName === t2.userName }
.map { case t1 ~ t2 => (t1, t2)}
.firstOption
}
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session): Option[IssueComment] = {
@@ -97,6 +100,30 @@ trait IssuesService {
.list.toMap
}
/**
* Returns the Map which contains issue count for each priority.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @return the Map which contains issue count for each priority (key is priority name, value is issue count)
*/
def countIssueGroupByPriorities(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
.join(Priorities).on { case t1 ~ t2 =>
t1.byPriority(t2.userName, t2.repositoryName, t2.priorityId)
}
.groupBy { case t1 ~ t2 =>
t2.priorityName
}
.map { case priorityName ~ t =>
priorityName -> t.length
}
.list.toMap
}
def getCommitStatues(userName: String, repositoryName: String, issueId: Int)(implicit s: Session): Option[CommitStatusInfo] = {
val status = PullRequests
.filter { pr =>
@@ -136,21 +163,23 @@ trait IssuesService {
(implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels
val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos)
.joinLeft (IssueLabels) .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.joinLeft (Labels) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t3.map(_.byLabel(t4.userName, t4.repositoryName, t4.labelId)) }
.joinLeft (Milestones) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 =>
(t1, t2.commentCount, t4.map(_.labelId), t4.map(_.labelName), t4.map(_.color), t5.map(_.title))
.joinLeft (IssueLabels) .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.joinLeft (Labels) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t3.map(_.byLabel(t4.userName, t4.repositoryName, t4.labelId)) }
.joinLeft (Milestones) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.joinLeft (Priorities) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 =>
(t1, t2.commentCount, t4.map(_.labelId), t4.map(_.labelName), t4.map(_.color), t5.map(_.title), t6.map(_.priorityName))
}
.list
.splitWith { (c1, c2) => c1._1.userName == c2._1.userName && c1._1.repositoryName == c2._1.repositoryName && c1._1.issueId == c2._1.issueId }
result.map { issues => issues.head match {
case (issue, commentCount, _, _, _, milestone) =>
case (issue, commentCount, _, _, _, milestone, priority) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))} toList,
milestone,
priority,
commentCount,
getCommitStatues(issue.userName, issue.repositoryName, issue.issueId))
}} toList
@@ -173,15 +202,16 @@ trait IssuesService {
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, true, offset, limit, repos)
.join(PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.join(Repositories).on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byRepository(t1.userName, t1.repositoryName) }
.join(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => (t1, t5, t2.commentCount, t3, t4, t6) }
.join (PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.join (Repositories).on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byRepository(t1.userName, t1.repositoryName) }
.join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.joinLeft(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName}
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
.list
}
@@ -204,6 +234,10 @@ trait IssuesService {
case "asc" => t1.updatedDate asc
case "desc" => t1.updatedDate desc
}
case "priority" => condition.direction match {
case "asc" => t2.priority asc
case "desc" => t2.priority desc
}
}
}
.drop(offset).take(limit).zipWithIndex
@@ -219,6 +253,7 @@ trait IssuesService {
.foldLeft[Rep[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId.? isEmpty, condition.milestone == Some(None)) &&
(t1.priorityId.? isEmpty, condition.priority == Some(None)) &&
(t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) &&
@@ -227,6 +262,11 @@ trait IssuesService {
(t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) &&
(t2.title === condition.milestone.get.get.bind)
} exists, condition.milestone.flatten.isDefined) &&
// Priority filter
(Priorities filter { t2 =>
(t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.priorityId)) &&
(t2.priorityName === condition.priority.get.get.bind)
} exists, condition.priority.flatten.isDefined) &&
// Assignee filter
(t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined) &&
// Label filter
@@ -253,7 +293,7 @@ trait IssuesService {
}
def insertIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int],
assignedUserName: Option[String], milestoneId: Option[Int], priorityId: Option[Int],
isPullRequest: Boolean = false)(implicit s: Session): Int = {
// next id number
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
@@ -264,6 +304,7 @@ trait IssuesService {
id,
loginUser,
milestoneId,
priorityId,
assignedUserName,
title,
content,
@@ -290,6 +331,7 @@ trait IssuesService {
def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: String)(implicit s: Session): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments returning IssueComments.map(_.commentId) insert IssueComment(
userName = owner,
repositoryName = repository,
@@ -305,27 +347,40 @@ trait IssuesService {
Issues
.filter (_.byPrimaryKey(owner, repository, issueId))
.map { t => (t.title, t.content.?, t.updatedDate) }
.update (title, content, currentDate)
.update(title, content, currentDate)
}
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String])(implicit s: Session): Int = {
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.assignedUserName?, t.updatedDate)).update(assignedUserName, currentDate)
}
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int])(implicit s: Session): Int = {
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.milestoneId?, t.updatedDate)).update(milestoneId, currentDate)
}
def updateComment(commentId: Int, content: String)(implicit s: Session): Int = {
IssueComments.filter (_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
def updatePriorityId(owner: String, repository: String, issueId: Int, priorityId: Option[Int])(implicit s: Session): Int = {
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.priorityId?, t.updatedDate)).update(priorityId, currentDate)
}
def deleteComment(commentId: Int)(implicit s: Session): Int = {
IssueComments filter (_.byPrimaryKey(commentId)) delete
def updateComment(issueId: Int, commentId: Int, content: String)(implicit s: Session): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
}
def deleteComment(issueId: Int, commentId: Int)(implicit s: Session): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match {
case Some(c) if c.action == "reopen_comment" =>
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Reopen", "reopen")
case Some(c) if c.action == "close_comment" =>
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close")
case Some(_) =>
IssueComments.filter(_.byPrimaryKey(commentId)).delete
}
}
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session): Int = {
(Issues filter (_.byPrimaryKey(owner, repository, issueId)) map(t => (t.closed, t.updatedDate))).update((closed, currentDate))
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.closed, t.updatedDate)).update(closed, currentDate)
}
/**
@@ -408,9 +463,8 @@ trait IssuesService {
def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session): Unit = {
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")
}
val userName = getAccountByMailAddress(commit.committerEmailAddress).map(_.userName).getOrElse(commit.committerName)
createComment(owner, repository, userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
@@ -430,6 +484,7 @@ object IssuesService {
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestone: Option[Option[String]] = None,
priority: Option[Option[String]] = None,
author: Option[String] = None,
assigned: Option[Option[String]] = None,
mentioned: Option[String] = None,
@@ -455,10 +510,14 @@ object IssuesService {
).flatten ++
labels.map(label => s"label:${label}") ++
List(
milestone.map { _ match {
milestone.map {
case Some(x) => s"milestone:${x}"
case None => "no:milestone"
}},
},
priority.map {
case Some(x) => s"priority:${x}"
case None => "no:priority"
},
(sort, direction) match {
case ("created" , "desc") => None
case ("created" , "asc" ) => Some("sort:created-asc")
@@ -466,6 +525,8 @@ object IssuesService {
case ("comments", "asc" ) => Some("sort:comments-asc")
case ("updated" , "desc") => Some("sort:updated-desc")
case ("updated" , "asc" ) => Some("sort:updated-asc")
case ("priority", "desc") => Some("sort:priority-desc")
case ("priority", "asc" ) => Some("sort:priority-asc")
case x => throw new MatchError(x)
},
visibility.map(visibility => s"visibility:${visibility}")
@@ -480,6 +541,10 @@ object IssuesService {
case Some(x) => "milestone=" + urlEncode(x)
case None => "milestone=none"
},
priority.map {
case Some(x) => "priority=" + urlEncode(x)
case None => "priority=none"
},
author .map(x => "author=" + urlEncode(x)),
assigned.map {
case Some(x) => "assigned=" + urlEncode(x)
@@ -512,6 +577,10 @@ object IssuesService {
case "none" => None
case x => Some(x)
},
param(request, "priority").map {
case "none" => None
case x => Some(x)
},
param(request, "author"),
param(request, "assigned").map {
case "none" => None
@@ -519,7 +588,7 @@ object IssuesService {
},
param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
param(request, "visibility"),
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
@@ -535,6 +604,6 @@ object IssuesService {
case class CommitStatusInfo(count: Int, successCount: Int, context: Option[String], state: Option[CommitState], targetUrl: Option[String], description: Option[String])
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int, status:Option[CommitStatusInfo])
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], priority: Option[String], commentCount: Int, status:Option[CommitStatusInfo])
}

View File

@@ -3,40 +3,55 @@ package gitbucket.core.service
import gitbucket.core.model.Account
import gitbucket.core.util.Directory._
import gitbucket.core.util.SyntaxSugars._
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.merge.{MergeStrategy, Merger, RecursiveMerger}
import org.eclipse.jgit.api.{Git, MergeResult}
import org.eclipse.jgit.transport.RefSpec
import org.eclipse.jgit.errors.NoMergeBaseException
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository}
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
import scala.collection.JavaConverters._
trait MergeService {
import MergeService._
/**
* Checks whether conflict will be caused in merging within pull request.
* Returns true if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Boolean = {
def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Option[String] = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
MergeCacheInfo(git, branch, issueId).checkConflict()
new MergeCacheInfo(git, branch, issueId).checkConflict()
}
}
/**
* Checks whether conflict will be caused in merging within pull request.
* only cache check.
* Returns Some(true) if conflict will be caused.
* Returns None if cache has not created yet.
*/
def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Boolean] = {
def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Option[String]] = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
MergeCacheInfo(git, branch, issueId).checkConflictCache()
new MergeCacheInfo(git, branch, issueId).checkConflictCache()
}
}
/** merge pull request */
def mergePullRequest(git:Git, branch: String, issueId: Int, message:String, committer:PersonIdent): Unit = {
MergeCacheInfo(git, branch, issueId).merge(message, committer)
/** merge the pull request with a merge commit */
def mergePullRequest(git: Git, branch: String, issueId: Int, message: String, committer: PersonIdent): Unit = {
new MergeCacheInfo(git, branch, issueId).merge(message, committer)
}
/** rebase to the head of the pull request branch */
def rebasePullRequest(git: Git, branch: String, issueId: Int, commits: Seq[RevCommit], committer: PersonIdent): Unit = {
new MergeCacheInfo(git, branch, issueId).rebase(committer, commits)
}
/** squash commits in the pull request and append it */
def squashPullRequest(git: Git, branch: String, issueId: Int, message: String, committer: PersonIdent): Unit = {
new MergeCacheInfo(git, branch, issueId).squash(message, committer)
}
/** fetch remote branch to my repository refs/pull/{issueId}/head */
def fetchAsPullRequest(userName: String, repositoryName: String, requestUserName: String, requestRepositoryName: String, requestBranch:String, issueId:Int){
using(Git.open(getRepositoryDir(userName, repositoryName))){ git =>
@@ -46,11 +61,12 @@ trait MergeService {
.call
}
}
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = {
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Either[String, (ObjectId, ObjectId, ObjectId)] = {
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${remoteBranch}"
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
@@ -67,12 +83,12 @@ trait MergeService {
val mergeTip = git.getRepository.resolve(tmpRefName)
try {
if(merger.merge(mergeBaseTip, mergeTip)){
Some((merger.getResultTreeId, mergeBaseTip, mergeTip))
Right((merger.getResultTreeId, mergeBaseTip, mergeTip))
} else {
None
Left(createConflictMessage(mergeTip, mergeBaseTip, merger))
}
} catch {
case e: NoMergeBaseException => None
case e: NoMergeBaseException => Left(e.toString)
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
@@ -81,30 +97,33 @@ trait MergeService {
}
}
}
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
* Checks whether conflict will be caused in merging. Returns `Some(errorMessage)` if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean =
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty
requestUserName: String, requestRepositoryName: String, requestBranch: String): Option[String] =
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).left.toOption
def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String,
loginAccount: Account, message: String): Option[ObjectId] = {
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) =>
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map { case (newTreeId, oldBaseId, oldHeadId) =>
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId))
Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge"))
}
oldBaseId
}
}.toOption
}
}
object MergeService{
object Util{
// return treeId
// return merge commit id
def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = {
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(treeId)
@@ -113,14 +132,14 @@ object MergeService{
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got mergeCommit Object Id
val inserter = repository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.close()
mergeCommitId
using(repository.newObjectInserter){ inserter =>
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
mergeCommitId
}
}
def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = {
// update refs
def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None): Unit = {
val refUpdate = repository.updateRef(ref)
refUpdate.setNewObjectId(newObjectId)
refUpdate.setForceUpdate(force)
@@ -129,33 +148,41 @@ object MergeService{
refUpdate.update()
}
}
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
val repository = git.getRepository
val mergedBranchName = s"refs/pull/${issueId}/merge"
val conflictedBranchName = s"refs/pull/${issueId}/conflict"
class MergeCacheInfo(git: Git, branch: String, issueId: Int){
private val repository = git.getRepository
private val mergedBranchName = s"refs/pull/${issueId}/merge"
private val conflictedBranchName = s"refs/pull/${issueId}/conflict"
lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}")
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
def checkConflictCache(): Option[Boolean] = {
Option(repository.resolve(mergedBranchName)).flatMap{ merged =>
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
def checkConflictCache(): Option[Option[String]] = {
Option(repository.resolve(mergedBranchName)).flatMap { merged =>
if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// merged branch exists
Some(false)
Some(None)
} else {
None
}
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
if(parseCommit(conflicted).getParents().toSet == Set( mergeBaseTip, mergeTip )){
val commit = parseCommit(conflicted)
if(commit.getParents().toSet == Set( mergeBaseTip, mergeTip )){
// conflict branch exists
Some(true)
Some(Some(commit.getFullMessage))
} else {
None
}
})
}
def checkConflict():Boolean ={
def checkConflict(): Option[String] ={
checkConflictCache.getOrElse(checkConflictForce)
}
def checkConflictForce():Boolean ={
def checkConflictForce(): Option[String] ={
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
@@ -163,36 +190,114 @@ object MergeService{
case e: NoMergeBaseException => true
}
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
val committer = mergeTipCommit.getCommitterIdent;
def updateBranch(treeId:ObjectId, message:String, branchName:String){
val committer = mergeTipCommit.getCommitterIdent
def _updateBranch(treeId: ObjectId, message: String, branchName: String){
// creates merge commit
val mergeCommitId = createMergeCommit(treeId, committer, message)
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
}
if(!conflicted){
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
_updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
None
} else {
updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName)
val message = createConflictMessage(mergeTip, mergeBaseTip, merger)
_updateBranch(mergeTipCommit.getTree().getId(), message, conflictedBranchName)
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
Some(message)
}
conflicted
}
// update branch from cache
def merge(message:String, committer:PersonIdent) = {
if(checkConflict()){
if(checkConflict().isDefined){
throw new RuntimeException("This pull request can't merge automatically.")
}
val mergeResultCommit = parseCommit( Option(repository.resolve(mergedBranchName)).getOrElse(throw new RuntimeException(s"not found branch ${mergedBranchName}")) )
val mergeResultCommit = parseCommit(Option(repository.resolve(mergedBranchName)).getOrElse {
throw new RuntimeException(s"Not found branch ${mergedBranchName}")
})
// creates merge commit
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
// update refs
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
}
def rebase(committer: PersonIdent, commits: Seq[RevCommit]): Unit = {
if(checkConflict().isDefined){
throw new RuntimeException("This pull request can't merge automatically.")
}
def _cloneCommit(commit: RevCommit, parents: Array[ObjectId]): CommitBuilder = {
val newCommit = new CommitBuilder()
newCommit.setTreeId(commit.getTree.getId)
parents.foreach { parentId =>
newCommit.addParentId(parentId)
}
newCommit.setAuthor(commit.getAuthorIdent)
newCommit.setCommitter(committer)
newCommit.setMessage(commit.getFullMessage)
newCommit
}
val mergeBaseTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeBaseTip ))
var previousId = mergeBaseTipCommit.getId
using(repository.newObjectInserter){ inserter =>
commits.foreach { commit =>
val nextCommit = _cloneCommit(commit, Array(previousId))
previousId = inserter.insert(nextCommit)
}
inserter.flush()
}
Util.updateRefs(repository, s"refs/heads/${branch}", previousId, false, committer, Some("rebased"))
}
def squash(message: String, committer: PersonIdent): Unit = {
if(checkConflict().isDefined){
throw new RuntimeException("This pull request can't merge automatically.")
}
val mergeBaseTipCommit = using(new RevWalk( repository ))(_.parseCommit(mergeBaseTip))
val mergeBranchHeadCommit = using(new RevWalk( repository ))(_.parseCommit(repository.resolve(mergedBranchName)))
// Create squash commit
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(mergeBranchHeadCommit.getTree.getId)
mergeCommit.setParentId(mergeBaseTipCommit)
mergeCommit.setAuthor(mergeBranchHeadCommit.getAuthorIdent)
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got squash commit Object Id
val newCommitId = using(repository.newObjectInserter){ inserter =>
val newCommitId = inserter.insert(mergeCommit)
inserter.flush()
newCommitId
}
Util.updateRefs(repository, mergedBranchName, newCommitId, true, committer)
// rebase to squash commit
Util.updateRefs(repository, s"refs/heads/${branch}", repository.resolve(mergedBranchName), false, committer, Some("squashed"))
}
// return treeId
private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
private def parseCommit(id: ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
}
private def createConflictMessage(mergeTip: ObjectId, mergeBaseTip: ObjectId, merger: Merger): String = {
val mergeResults = merger.asInstanceOf[RecursiveMerger].getMergeResults
s"Can't merge ${mergeTip.name} into ${mergeBaseTip.name}\n\n" +
"Conflicting files:\n" +
mergeResults.asScala.map { case (key, _) => "- " + key + "\n" }.mkString
}
}

View File

@@ -0,0 +1,84 @@
package gitbucket.core.service
import gitbucket.core.model.Priority
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.util.StringUtil
trait PrioritiesService {
def getPriorities(owner: String, repository: String)(implicit s: Session): List[Priority] =
Priorities.filter(_.byRepository(owner, repository)).sortBy(_.ordering asc).list
def getPriority(owner: String, repository: String, priorityId: Int)(implicit s: Session): Option[Priority] =
Priorities.filter(_.byPrimaryKey(owner, repository, priorityId)).firstOption
def getPriority(owner: String, repository: String, priorityName: String)(implicit s: Session): Option[Priority] =
Priorities.filter(_.byPriority(owner, repository, priorityName)).firstOption
def createPriority(owner: String, repository: String, priorityName: String, description: Option[String], color: String)(implicit s: Session): Int = {
val ordering = Priorities.filter(_.byRepository(owner, repository))
.list
.map(p => p.ordering)
.reduceOption(_ max _)
.map(m => m + 1)
.getOrElse(0)
Priorities returning Priorities.map(_.priorityId) insert Priority(
userName = owner,
repositoryName = repository,
priorityName = priorityName,
description = description,
isDefault = false,
ordering = ordering,
color = color
)
}
def updatePriority(owner: String, repository: String, priorityId: Int, priorityName: String, description: Option[String], color: String)
(implicit s: Session): Unit =
Priorities.filter(_.byPrimaryKey(owner, repository, priorityId))
.map(t => (t.priorityName, t.description.?, t.color))
.update(priorityName, description, color)
def reorderPriorities(owner: String, repository: String, order: Map[Int, Int])
(implicit s: Session): Unit = {
Priorities.filter(_.byRepository(owner, repository))
.list
.foreach(p => Priorities
.filter(_.byPrimaryKey(owner, repository, p.priorityId))
.map(_.ordering)
.update(order.get(p.priorityId).get))
}
def deletePriority(owner: String, repository: String, priorityId: Int)(implicit s: Session): Unit = {
Issues.filter(_.byRepository(owner, repository))
.filter(_.priorityId === priorityId)
.map(_.priorityId?)
.update(None)
Priorities.filter(_.byPrimaryKey(owner, repository, priorityId)).delete
}
def getDefaultPriority(owner: String, repository: String)(implicit s: Session): Option[Priority] = {
Priorities
.filter(_.byRepository(owner, repository))
.filter(_.isDefault)
.list
.headOption
}
def setDefaultPriority(owner: String, repository: String, priorityId: Option[Int])(implicit s: Session): Unit = {
Priorities
.filter(_.byRepository(owner, repository))
.filter(_.isDefault)
.map(_.isDefault)
.update(false)
priorityId.foreach(id => Priorities
.filter(_.byPrimaryKey(owner, repository, id))
.map(_.isDefault)
.update(true))
}
}

View File

@@ -1,11 +1,10 @@
package gitbucket.core.service
import gitbucket.core.model.{ProtectedBranch, ProtectedBranchContext, CommitState}
import gitbucket.core.model.{Session => _, _}
import gitbucket.core.plugin.ReceiveHook
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
trait ProtectedBranchService {
@@ -18,10 +17,11 @@ trait ProtectedBranchService {
.filter(_._1.byPrimaryKey(owner, repository, branch))
.list
.groupBy(_._1)
.headOption
.map { p => p._1 -> p._2.flatMap(_._2) }
.map { case (t1, contexts) =>
new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin)
}.headOption
}
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo =
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
@@ -45,12 +45,17 @@ trait ProtectedBranchService {
object ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService with RepositoryService with AccountService {
override def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Option[String] = {
val branch = command.getRefName.stripPrefix("refs/heads/")
if(branch != command.getRefName){
getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
val repositoryInfo = getRepository(owner, repository)
if(command.getType == ReceiveCommand.Type.DELETE && repositoryInfo.exists(_.repository.defaultBranch == branch)){
Some(s"refusing to delete the branch: ${command.getRefName}.")
} else {
getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
}
} else {
None
}
@@ -73,10 +78,19 @@ object ProtectedBranchService {
* Include administrators
* Enforce required status checks for repository administrators.
*/
includeAdministrators: Boolean) extends AccountService with CommitStatusService {
includeAdministrators: Boolean) extends AccountService with RepositoryService with CommitStatusService {
def isAdministrator(pusher: String)(implicit session: Session): Boolean =
pusher == owner || getGroupMembers(owner).exists(gm => gm.userName == pusher && gm.isManager)
pusher == owner || getGroupMembers(owner).exists(gm => gm.userName == pusher && gm.isManager) ||
getCollaborators(owner, repository).exists { case (collaborator, isGroup) =>
if(collaborator.role == Role.ADMIN.name){
if(isGroup){
getGroupMembers(collaborator.collaboratorName).exists(gm => gm.userName == pusher)
} else {
collaborator.collaboratorName == pusher
}
} else false
}
/**
* Can't be force pushed

View File

@@ -79,7 +79,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
commitIdFrom,
commitIdTo)
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean)
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Option[Boolean])
(implicit s: Session): List[PullRequest] =
PullRequests
.join(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
@@ -87,16 +87,16 @@ trait PullRequestService { self: IssuesService with CommitsService =>
(t1.requestUserName === userName.bind) &&
(t1.requestRepositoryName === repositoryName.bind) &&
(t1.requestBranch === branch.bind) &&
(t2.closed === closed.bind)
(t2.closed === closed.get.bind, closed.isDefined)
}
.map { case (t1, t2) => t1 }
.list
/**
* for repository viewer.
* 1. find pull request from from `branch` to othre branch on same repository
* 1. find pull request from `branch` to other branch on same repository
* 1. return if exists pull request to `defaultBranch`
* 2. return if exists pull request to othre branch
* 2. return if exists pull request to other branch
* 2. return None
*/
def getPullRequestFromBranch(userName: String, repositoryName: String, branch: String, defaultBranch: String)
@@ -118,7 +118,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
* 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 =>
getPullRequestsByRequest(owner, repository, branch, Some(false)).foreach { pullreq =>
if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){
// Update the git repository
val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest(
@@ -230,7 +230,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
val diffs = JGitUtil.getDiffs(newGit, Some(oldId.getName), newId.getName, true, false)
(commits, diffs)
}
@@ -244,8 +244,8 @@ object PullRequestService {
case class PullRequestCount(userName: String, count: Int)
case class MergeStatus(
hasConflict: Boolean,
commitStatues:List[CommitStatus],
conflictMessage: Option[String],
commitStatues: List[CommitStatus],
branchProtection: ProtectedBranchService.ProtectedBranchInfo,
branchIsOutOfDate: Boolean,
hasUpdatePermission: Boolean,
@@ -253,12 +253,13 @@ object PullRequestService {
hasMergePermission: Boolean,
commitIdTo: String){
val hasConflict = conflictMessage.isDefined
val statuses: List[CommitStatus] =
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
val hasProblem = hasRequiredStatusProblem || hasConflict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary:(CommitState, String) = {
val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet)

View File

@@ -1,68 +1,206 @@
package gitbucket.core.service
import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JGitUtil
import gitbucket.core.model.Account
import gitbucket.core.util.{FileUtil, JGitUtil, LockUtil}
import gitbucket.core.model.{Account, Role}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.servlet.Database
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.eclipse.jgit.lib.{Constants, FileMode}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
object RepositoryCreationService {
private val Creating = new ConcurrentHashMap[String, Option[String]]()
def isCreating(owner: String, repository: String): Boolean = {
Option(Creating.get(s"${owner}/${repository}")).map(_.isEmpty).getOrElse(false)
}
def startCreation(owner: String, repository: String): Unit = {
Creating.put(s"${owner}/${repository}", None)
}
def endCreation(owner: String, repository: String, error: Option[String]): Unit = {
error match {
case None => Creating.remove(s"${owner}/${repository}")
case Some(error) => Creating.put(s"${owner}/${repository}", Some(error))
}
}
def getCreationError(owner: String, repository: String): Option[String] = {
Option(Creating.remove(s"${owner}/${repository}")).getOrElse(None)
}
}
trait RepositoryCreationService {
self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService =>
self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService with PrioritiesService =>
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
def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String],
isPrivate: Boolean, createReadme: Boolean): Future[Unit] = {
createRepository(loginAccount, owner, name, description, isPrivate, if (createReadme) "README" else "EMPTY", None)
}
// Insert to the database at first
insertRepository(name, owner, description, isPrivate)
def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String],
isPrivate: Boolean, initOption: String, sourceUrl: Option[String]): Future[Unit] = Future {
RepositoryCreationService.startCreation(owner, name)
try {
Database() withTransaction { implicit session =>
val ownerAccount = getAccountByUserName(owner).get
val loginUserName = loginAccount.userName
// // Add collaborators for group repository
// if(ownerAccount.isGroupAccount){
// getGroupMembers(owner).foreach { member =>
// addCollaborator(owner, name, member.userName)
// }
// }
val copyRepositoryDir = if (initOption == "COPY") {
sourceUrl.flatMap { url =>
val dir = Files.createTempDirectory(s"gitbucket-${owner}-${name}").toFile
Git.cloneRepository().setBare(true).setURI(url).setDirectory(dir).setCloneAllBranches(true).call()
Some(dir)
}
} else None
// Insert default labels
insertDefaultLabels(owner, name)
// Create the actual repository
val gitdir = getRepositoryDir(owner, name)
JGitUtil.initRepository(gitdir)
// Insert to the database at first
insertRepository(name, owner, description, isPrivate)
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"
// // Add collaborators for group repository
// if(ownerAccount.isGroupAccount){
// getGroupMembers(owner).foreach { member =>
// addCollaborator(owner, name, member.userName)
// }
// }
// Insert default labels
insertDefaultLabels(owner, name)
// Insert default priorities
insertDefaultPriorities(owner, name)
// Create the actual repository
val gitdir = getRepositoryDir(owner, name)
JGitUtil.initRepository(gitdir)
if (initOption == "README") {
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")
}
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
copyRepositoryDir.foreach { dir =>
try {
using(Git.open(dir)) { git =>
git.push().setRemote(gitdir.toURI.toString).setPushAll().setPushTags().call()
}
} finally {
FileUtils.deleteQuietly(dir)
}
}
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)
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.created(owner, name))
}
RepositoryCreationService.endCreation(owner, name, None)
} catch {
case ex: Exception => RepositoryCreationService.endCreation(owner, name, Some(ex.toString))
}
}
// Create Wiki repository
createWikiRepository(loginAccount, owner, name)
def forkRepository(accountName: String, repository: RepositoryInfo, loginUserName: String): Future[Unit] = Future {
RepositoryCreationService.startCreation(accountName, repository.name)
try {
LockUtil.lock(s"${accountName}/${repository.name}") {
Database() withTransaction { implicit session =>
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
// Record activity
recordCreateRepositoryActivity(owner, name, loginUserName)
insertRepository(
repositoryName = repository.name,
userName = accountName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Set default collaborators for the private fork
if (repository.repository.isPrivate) {
// Copy collaborators from the source repository
getCollaborators(repository.owner, repository.name).foreach { case (collaborator, _) =>
addCollaborator(accountName, repository.name, collaborator.collaboratorName, collaborator.role)
}
// Register an owner of the source repository as a collaborator
addCollaborator(accountName, repository.name, repository.owner, Role.ADMIN.name)
}
// Insert default labels
insertDefaultLabels(accountName, repository.name)
// Insert default priorities
insertDefaultPriorities(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name)))
// Create Wiki repository
JGitUtil.cloneRepository(getWikiRepositoryDir(repository.owner, repository.name),
FileUtil.deleteIfExists(getWikiRepositoryDir(accountName, repository.name)))
// Copy LFS files
val lfsDir = getLfsDir(repository.owner, repository.name)
if (lfsDir.exists) {
FileUtils.copyDirectory(lfsDir, FileUtil.deleteIfExists(getLfsDir(accountName, repository.name)))
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name))
RepositoryCreationService.endCreation(accountName, repository.name, None)
}
}
} catch {
case ex: Exception => RepositoryCreationService.endCreation(accountName, repository.name, Some(ex.toString))
}
}
def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = {
@@ -74,5 +212,13 @@ trait RepositoryCreationService {
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
def insertDefaultPriorities(userName: String, repositoryName: String)(implicit s: Session): Unit = {
createPriority(userName, repositoryName, "highest", Some("All defects at this priority must be fixed before any public product is delivered."), "fc2929")
createPriority(userName, repositoryName, "very high", Some("Issues must be addressed before a final product is delivered."), "fc5629")
createPriority(userName, repositoryName, "high", Some("Issues should be addressed before a final product is delivered. If the issue cannot be resolved before delivery, it should be prioritized for the next release."), "fc9629")
createPriority(userName, repositoryName, "important", Some("Issues can be shipped with a final product, but should be reviewed before the next release."), "fccd29")
createPriority(userName, repositoryName, "default", Some("Default."), "acacac")
setDefaultPriority(userName, repositoryName, getPriority(userName, repositoryName, "default").map(_.priorityId))
}
}

View File

@@ -67,7 +67,7 @@ trait RepositorySearchService { self: IssuesService =>
files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult(
path.replaceFirst("\\.md$", ""),
path.stripSuffix(".md"),
commits(path).getCommitterIdent.getWhen,
highlightText,
lineNumber)

View File

@@ -59,13 +59,14 @@ trait RepositoryService { self: AccountService =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHooks = RepositoryWebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHookEvents = RepositoryWebHookEvents.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val priorities = Priorities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
@@ -83,7 +84,7 @@ trait RepositoryService { self: AccountService =>
Repositories.filter { t =>
(t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
}.map { t => t.parentUserName -> t.parentRepositoryName }.update(newUserName, newRepositoryName)
// Updates activity fk before deleting repository because activity is sorted by activityId
// and it can't be changed by deleting-and-inserting record.
@@ -94,17 +95,22 @@ trait RepositoryService { self: AccountService =>
deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
WebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
RepositoryWebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
RepositoryWebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Priorities .insertAll(priorities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
val newPriorities = Priorities.filter(_.byRepository(newUserName, newRepositoryName)).list
Issues.insertAll(issues.map { x => x.copy(
userName = newUserName,
repositoryName = newRepositoryName,
milestoneId = x.milestoneId.map { id =>
newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId
},
priorityId = x.priorityId.map { id =>
newPriorities.find(_.priorityName == priorities.find(_.priorityId == id).get.priorityName).get.priorityId
}
)} :_*)
@@ -133,7 +139,7 @@ trait RepositoryService { self: AccountService =>
repositoryName = newRepositoryName
)) :_*)
// TODO Drop transfered owner from collaborators?
// TODO Drop transferred owner from collaborators?
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages
@@ -157,22 +163,23 @@ trait RepositoryService { self: AccountService =>
}
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
CommitComments.filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
Issues .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
Activities .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
CommitComments .filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
Issues .filter(_.byRepository(userName, repositoryName)).delete
Priorities .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete
RepositoryWebHooks .filter(_.byRepository(userName, repositoryName)).delete
RepositoryWebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
ReleaseAssets .filter(_.byRepository(userName, repositoryName)).delete
Releases .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
Repositories
@@ -394,7 +401,7 @@ trait RepositoryService { self: AccountService =>
Collaborators
.join(Accounts).on(_.collaboratorName === _.userName)
.filter { case (t1, t2) => t1.byRepository(userName, repositoryName) }
.map { case (t1, t2) => (t1, t2.groupAccount) }
.map { case (t1, t2) => (t1, t2.groupAccount) }
.sortBy { case (t1, t2) => t1.collaboratorName }
.list
@@ -406,17 +413,26 @@ trait RepositoryService { self: AccountService =>
val q1 = Collaborators
.join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === false.bind) }
.filter { case (t1, t2) => t1.byRepository(userName, repositoryName) }
.map { case (t1, t2) => (t1.collaboratorName, t1.role) }
.map { case (t1, t2) => (t1.collaboratorName, t1.role) }
val q2 = Collaborators
.join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === true.bind) }
.join(GroupMembers).on { case ((t1, t2), t3) => t2.userName === t3.groupName }
.filter { case ((t1, t2), t3) => t1.byRepository(userName, repositoryName) }
.map { case ((t1, t2), t3) => (t3.userName, t1.role) }
.map { case ((t1, t2), t3) => (t3.userName, t1.role) }
q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1)
}
def hasOwnerRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true
case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName)) => true
case _ => false
}
}
def hasDeveloperRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
@@ -438,17 +454,31 @@ trait RepositoryService { self: AccountService =>
}
}
def isReadable(repository: Repository, loginAccount: Option[Account])(implicit s: Session): Boolean = {
if(!repository.isPrivate){
true
} else {
loginAccount match {
case Some(x) if(x.isAdmin) => true
case Some(x) if(repository.userName == x.userName) => true
case Some(x) if(getGroupMembers(repository.userName).exists(_.userName == x.userName)) => true
case Some(x) if(getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName)) => true
case _ => false
}
}
}
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
Query(Repositories.filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
}.length).first
def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[Repository] =
Repositories.filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
}
.sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
.sortBy(_.userName asc).list//.map(t => t.userName -> t.repositoryName).list
private val templateExtensions = Seq("md", "markdown")

View File

@@ -1,9 +1,9 @@
package gitbucket.core.service
import gitbucket.core.util.{Directory, SyntaxSugars}
import gitbucket.core.util.Implicits._
import Directory._
import SyntaxSugars._
import gitbucket.core.util.ConfigUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.SyntaxSugars._
import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
@@ -54,6 +54,7 @@ trait SystemSettingsService {
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
}
props.setProperty(SkinName, settings.skinName.toString)
using(new java.io.FileOutputStream(GitBucketConf)){ out =>
props.store(out, null)
}
@@ -111,7 +112,8 @@ trait SystemSettingsService {
getOptionValue(props, LdapKeystore, None)))
} else {
None
}
},
getValue(props, SkinName, "skin-blue")
)
}
}
@@ -136,18 +138,18 @@ object SystemSettingsService {
useSMTP: Boolean,
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap]){
def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
ldap: Option[Ldap],
skinName: String){
def sshAddress:Option[SshAddress] =
for {
host <- sshHost if ssh
}
yield SshAddress(
host,
sshPort.getOrElse(DefaultSshPort),
"git"
)
def baseUrl(request: HttpServletRequest): String = baseUrl.fold {
val url = request.getRequestURL.toString
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
url.substring(0, len).stripSuffix("/")
} (_.stripSuffix("/"))
def sshAddress:Option[SshAddress] = sshHost.collect { case host if ssh =>
SshAddress(host, sshPort.getOrElse(DefaultSshPort), "git")
}
}
case class Ldap(
@@ -219,24 +221,30 @@ object SystemSettingsService {
private val LdapTls = "ldap.tls"
private val LdapSsl = "ldap.ssl"
private val LdapKeystore = "ldap.keystore"
private val SkinName = "skinName"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default
else convertType(value).asInstanceOf[A]
}
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
getSystemProperty(key).getOrElse(getEnvironmentVariable(key).getOrElse {
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty){
default
} else {
convertType(value).asInstanceOf[A]
}
}
})
}
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default
else Some(convertType(value)).asInstanceOf[Option[A]]
}
private def convertType[A: ClassTag](value: String) =
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
if(c == classOf[Boolean]) value.toBoolean
else if(c == classOf[Int]) value.toInt
else value
}
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = {
getSystemProperty(key).orElse(getEnvironmentVariable(key).orElse {
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty){
default
} else {
Some(convertType(value)).asInstanceOf[Option[A]]
}
}
})
}
}

View File

@@ -3,12 +3,12 @@ package gitbucket.core.service
import fr.brouillard.oss.security.xhub.XHub
import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest}
import gitbucket.core.api._
import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, WebHookEvent}
import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, RepositoryWebHook, RepositoryWebHookEvent, AccountWebHook, AccountWebHookEvent}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import org.apache.http.client.utils.URLEncodedUtils
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName
import gitbucket.core.util.{RepositoryName, StringUtil}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
@@ -18,7 +18,7 @@ import org.eclipse.jgit.lib.ObjectId
import org.slf4j.LoggerFactory
import scala.concurrent._
import scala.util.{Success, Failure}
import scala.util.{Failure, Success}
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import gitbucket.core.model.WebHookContentType
@@ -32,45 +32,86 @@ trait WebHookService {
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
/** get All WebHook informations of repository */
def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(WebHook, Set[WebHook.Event])] =
WebHooks.filter(_.byRepository(owner, repository))
.join(WebHookEvents).on { (w, t) => t.byWebHook(w) }
def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(RepositoryWebHook, Set[WebHook.Event])] =
RepositoryWebHooks.filter(_.byRepository(owner, repository))
.join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) }
.map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
/** get All WebHook informations of repository event */
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
WebHooks.filter(_.byRepository(owner, repository))
.join(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) }
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[RepositoryWebHook] =
RepositoryWebHooks.filter(_.byRepository(owner, repository))
.join(RepositoryWebHookEvents).on { (wh, whe) => whe.byRepositoryWebHook(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])] =
WebHooks
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(RepositoryWebHook, Set[WebHook.Event])] =
RepositoryWebHooks
.filter(_.byPrimaryKey(owner, repository, url))
.join(WebHookEvents).on { (w, t) => t.byWebHook(w) }
.join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) }
.map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
WebHooks insert WebHook(owner, repository, url, ctype, token)
RepositoryWebHooks insert RepositoryWebHook(owner, repository, url, ctype, token)
events.map { event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event)
RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
}
}
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token))
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token))
RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
events.map { event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event)
RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
}
}
def deleteWebHook(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
/** get All AccountWebHook informations of user */
def getAccountWebHooks(owner: String)(implicit s: Session): List[(AccountWebHook, Set[WebHook.Event])] =
AccountWebHooks.filter(_.byAccount(owner))
.join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) }
.map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
/** get All AccountWebHook informations of repository event */
def getAccountWebHooksByEvent(owner: String, event: WebHook.Event)(implicit s: Session): List[AccountWebHook] =
AccountWebHooks.filter(_.byAccount(owner))
.join(AccountWebHookEvents).on { (wh, whe) => whe.byAccountWebHook(wh) }
.filter { case (wh, whe) => whe.event === event.bind}
.map{ case (wh, whe) => wh }
.list.distinct
/** get All AccountWebHook information from repository to url */
def getAccountWebHook(owner: String, url: String)(implicit s: Session): Option[(AccountWebHook, Set[WebHook.Event])] =
AccountWebHooks
.filter(_.byPrimaryKey(owner, url))
.join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) }
.map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addAccountWebHook(owner: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
AccountWebHooks insert AccountWebHook(owner, url, ctype, token)
events.map { event: WebHook.Event =>
AccountWebHookEvents insert AccountWebHookEvent(owner, url, event)
}
}
def updateAccountWebHook(owner: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
AccountWebHooks.filter(_.byPrimaryKey(owner, url)).map(w => (w.ctype, w.token)).update((ctype, token))
AccountWebHookEvents.filter(_.byAccountWebHook(owner, url)).delete
events.map { event: WebHook.Event =>
AccountWebHookEvents insert AccountWebHookEvent(owner, url, event)
}
}
def deleteAccountWebHook(owner: String, url :String)(implicit s: Session): Unit =
AccountWebHooks.filter(_.byPrimaryKey(owner, url)).delete
def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload])
(implicit s: Session, c: JsonFormat.Context): Unit = {
@@ -78,6 +119,10 @@ trait WebHookService {
if(webHooks.nonEmpty){
makePayload.map(callWebHook(event, webHooks, _))
}
val accountWebHooks = getAccountWebHooksByEvent(owner, event)
if(accountWebHooks.nonEmpty){
makePayload.map(callWebHook(event, accountWebHooks, _))
}
}
def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)
@@ -160,7 +205,7 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._
// https://developer.github.com/v3/activity/events/types/#issuesevent
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
(implicit s: Session, context: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.Issues){
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
for{
@@ -178,21 +223,23 @@ trait WebHookPullRequestService extends WebHookService {
}
def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -207,7 +254,7 @@ trait WebHookPullRequestService extends WebHookService {
/** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */
def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String)
(implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[WebHook]] =
(implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[RepositoryWebHook]] =
(for{
is <- Issues if is.closed === false.bind
pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId)
@@ -217,23 +264,25 @@ trait WebHookPullRequestService extends WebHookService {
bu <- Accounts if bu.userName === pr.userName
ru <- Accounts if ru.userName === pr.requestUserName
iu <- Accounts if iu.userName === is.openedUserName
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
wht <- WebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byWebHook(wh)
wh <- RepositoryWebHooks if wh.byRepository(is.userName , is.repositoryName)
wht <- RepositoryWebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byRepositoryWebHook(wh)
} yield {
((is, iu, pr, bu, ru), wh)
}).list.groupBy(_._1).mapValues(_.map(_._2))
def callPullRequestWebHookByRequestBranch(action: String, requestRepository: RepositoryService.RepositoryInfo, requestBranch: String, baseUrl: String, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
} yield {
val payload = WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
@@ -246,20 +295,22 @@ trait WebHookPullRequestService extends WebHookService {
callWebHook(WebHook.PullRequest, webHooks, payload)
}
}
}
trait WebHookPullRequestReviewCommentService extends WebHookService {
self: AccountService with RepositoryService with PullRequestService with IssuesService with CommitsService =>
def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo,
issue: Issue, pullRequest: PullRequest, baseUrl: String, sender: Account)
(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){
val users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestReviewCommentPayload(
@@ -267,6 +318,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
comment = comment,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -285,7 +337,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
import WebHookService._
def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
(implicit s: Session, c: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment){
for{
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
@@ -310,6 +362,35 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
object WebHookService {
trait WebHookPayload
// https://developer.github.com/v3/activity/events/types/#createevent
case class WebHookCreatePayload(
sender: ApiUser,
description: String,
ref: String,
ref_type: String,
master_branch: String,
repository: ApiRepository
) extends FieldSerializable with WebHookPayload {
val pusher_type = "user"
}
object WebHookCreatePayload {
def apply(git: Git, sender: Account, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account,
ref: String, refType: String): WebHookCreatePayload =
WebHookCreatePayload(
sender = ApiUser(sender),
ref = ref,
ref_type = refType,
description = repositoryInfo.repository.description.getOrElse(""),
master_branch = repositoryInfo.repository.defaultBranch,
repository = ApiRepository.forWebhookPayload(
repositoryInfo,
owner= ApiUser(repositoryOwner))
)
}
// https://developer.github.com/v3/activity/events/types/#pushevent
case class WebHookPushPayload(
pusher: ApiPusher,
@@ -321,9 +402,9 @@ object WebHookService {
repository: ApiRepository
) extends FieldSerializable with WebHookPayload {
val compare = commits.size match {
case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initalied repository
case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initialized repository
case 1 => ApiPath(s"/${repository.full_name}/commit/${after}")
case _ if before.filterNot(_=='0').isEmpty => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
case _ if before.forall(_=='0') => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}")
}
val head_commit = commits.lastOption
@@ -339,11 +420,22 @@ object WebHookService {
ref = refName,
before = ObjectId.toString(oldId),
after = ObjectId.toString(newId),
commits = commits.map{ commit => ApiCommit.forPushPayload(git, RepositoryName(repositoryInfo), commit) },
repository = ApiRepository.forPushPayload(
commits = commits.map{ commit => ApiCommit.forWebhookPayload(git, RepositoryName(repositoryInfo), commit) },
repository = ApiRepository.forWebhookPayload(
repositoryInfo,
owner= ApiUser(repositoryOwner))
)
def createDummyPayload(sender: Account): WebHookPushPayload =
WebHookPushPayload(
pusher = ApiPusher(sender),
sender = ApiUser(sender),
ref = "refs/heads/master",
before = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc",
after = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc",
commits = List.empty,
repository = ApiRepository.forDummyPayload(ApiUser(sender))
)
}
// https://developer.github.com/v3/activity/events/types/#issuesevent
@@ -367,6 +459,7 @@ object WebHookService {
def apply(action: String,
issue: Issue,
issueUser: Account,
assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -384,6 +477,7 @@ object WebHookService {
headRepo = headRepoPayload,
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
)
@@ -438,6 +532,7 @@ object WebHookService {
comment: CommitComment,
issue: Issue,
issueUser: Account,
assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -445,7 +540,7 @@ object WebHookService {
baseOwner: Account,
sender: Account,
mergedComment: Option[(IssueComment, Account)]
) : WebHookPullRequestReviewCommentPayload = {
): WebHookPullRequestReviewCommentPayload = {
val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender)
@@ -464,10 +559,60 @@ object WebHookService {
headRepo = headRepoPayload,
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
),
repository = baseRepoPayload,
sender = senderPayload)
}
}
// https://developer.github.com/v3/activity/events/types/#gollumevent
case class WebHookGollumPayload(
pages: Seq[WebHookGollumPagePayload],
repository: ApiRepository,
sender: ApiUser
) extends WebHookPayload
case class WebHookGollumPagePayload(
page_name: String,
title: String,
summary: Option[String] = None,
action: String, // created or edited
sha: String, // SHA of the latest commit
html_url: ApiPath
)
object WebHookGollumPayload {
def apply(
action: String,
pageName: String,
sha: String,
repository: RepositoryInfo,
repositoryUser: Account,
sender: Account
): WebHookGollumPayload = apply(Seq((action, pageName, sha)), repository, repositoryUser, sender)
def apply(
pages: Seq[(String, String, String)],
repository: RepositoryInfo,
repositoryUser: Account,
sender: Account
): WebHookGollumPayload = {
WebHookGollumPayload(
pages = pages.map { case (action, pageName, sha) =>
WebHookGollumPagePayload(
action = action,
page_name = pageName,
title = pageName,
sha = sha,
html_url = ApiPath(s"/${RepositoryName(repository).fullName}/wiki/${StringUtil.urlDecode(pageName)}")
)
},
repository = ApiRepository(repository, repositoryUser),
sender = ApiUser(sender)
)
}
}
}

View File

@@ -75,22 +75,6 @@ trait WikiService {
}
}
/**
* Returns the content of the specified file.
*/
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
if(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index)
val fileName = if(index < 0) path else path.substring(index + 1)
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes
}
} else None
}
/**
* Returns the list of wiki page names.
*/
@@ -237,7 +221,7 @@ trait WikiService {
builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer.fullName, committer.mailAddress,
if(message.trim.length == 0) {
if(message.trim.isEmpty) {
if(removed){
s"Rename ${currentPageName} to ${newPageName}"
} else if(created){

View File

@@ -0,0 +1,73 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http.HttpServletRequest
import org.scalatra.ScalatraFilter
import scala.collection.mutable.ListBuffer
class CompositeScalatraFilter extends Filter {
private val filters = new ListBuffer[(ScalatraFilter, String)]()
def mount(filter: ScalatraFilter, path: String): Unit = {
filters += ((filter, path))
}
override def init(filterConfig: FilterConfig): Unit = {
filters.foreach { case (filter, _) =>
filter.init(filterConfig)
}
}
override def destroy(): Unit = {
filters.foreach { case (filter, _) =>
filter.destroy()
}
}
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
val contextPath = request.getServletContext.getContextPath
val requestPath = request.asInstanceOf[HttpServletRequest].getRequestURI.substring(contextPath.length)
val checkPath = if(requestPath.endsWith("/")){
requestPath
} else {
requestPath + "/"
}
if(!checkPath.startsWith("/upload/") && !checkPath.startsWith("/git/") && !checkPath.startsWith("/git-lfs/") &&
!checkPath.startsWith("/plugin-assets/")){
filters
.filter { case (_, path) =>
val start = path.replaceFirst("/\\*$", "/")
checkPath.startsWith(start)
}
.foreach { case (filter, _) =>
val mockChain = new MockFilterChain()
filter.doFilter(request, response, mockChain)
if(mockChain.continue == false){
return ()
}
}
}
chain.doFilter(request, response)
}
}
class MockFilterChain extends FilterChain {
var continue: Boolean = false
override def doFilter(request: ServletRequest, response: ServletResponse): Unit = {
continue = true
}
}
//class FilterChainFilter(chain: FilterChain) extends Filter {
// override def init(filterConfig: FilterConfig): Unit = ()
// override def destroy(): Unit = ()
// override def doFilter(request: ServletRequest, response: ServletResponse, mockChain: FilterChain) = chain.doFilter(request, response)
//}

View File

@@ -1,37 +0,0 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.service.SystemSettingsService
/**
* A controller to provide GitHub compatible URL for Git clients.
*/
class GHCompatRepositoryAccessFilter extends Filter with SystemSettingsService {
/**
* Pattern of GitHub compatible repository URL.
* <code>/:user/:repo.git/</code>
*/
private val githubRepositoryPattern = """^/[^/]+/[^/]+\.git/.*""".r
override def init(filterConfig: FilterConfig) = {}
override def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) = {
implicit val request = req.asInstanceOf[HttpServletRequest]
val agent = request.getHeader("USER-AGENT")
val response = res.asInstanceOf[HttpServletResponse]
val requestPath = request.getRequestURI.substring(request.getContextPath.length)
requestPath match {
case githubRepositoryPattern() if agent != null && agent.toLowerCase.indexOf("git") >= 0 =>
response.sendRedirect(baseUrl + "/git" + requestPath)
case _ =>
chain.doFilter(req, res)
}
}
override def destroy() = {}
}

View File

@@ -51,21 +51,21 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
private def pluginRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
settings: SystemSettings, isUpdating: Boolean, filter: GitRepositoryFilter): Unit = {
implicit val r = request
Database() withSession { implicit session =>
val account = for {
auth <- Option(request.getHeader("Authorization"))
Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password)
} yield {
request.setAttribute(Keys.Request.UserName, account.userName)
account
}
val account = for {
auth <- Option(request.getHeader("Authorization"))
Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password)
} yield {
request.setAttribute(Keys.Request.UserName, account.userName)
account
}
if(filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)){
chain.doFilter(request, response)
} else {
AuthUtil.requireAuth(response)
if (filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)) {
chain.doFilter(request, response)
} else {
AuthUtil.requireAuth(response)
}
}
}
@@ -74,7 +74,7 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
val action = request.paths match {
case Array(_, repositoryOwner, repositoryName, _*) =>
Database() withSession { implicit session =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match {
case Some(repository) => {
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) {
// Authentication is not required

View File

@@ -1,6 +1,7 @@
package gitbucket.core.servlet
import java.io.File
import java.util
import java.util.Date
import gitbucket.core.api
@@ -22,6 +23,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.json4s.jackson.Serialization._
@@ -154,13 +156,23 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository)
val settings = loadSystemSettings()
val baseUrl = settings.baseUrl(request)
val sshUrl = settings.sshAddress.map { x => s"${x.genericUser}@${x.host}:${x.port}" }
if(!repository.endsWith(".wiki")){
defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
}
if(repository.endsWith(".wiki")){
defining(request) { implicit r =>
receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.stripSuffix(".wiki"), pusher, baseUrl, sshUrl))
}
}
}
}
@@ -170,7 +182,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)/*(implicit session: Session)*/
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService with CommitsService {
@@ -185,9 +197,10 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// call pre-commit hook
PluginRegistry().getReceiveHooks
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher))
.headOption.foreach { error =>
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
}
.headOption
.foreach { error =>
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
}
}
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
existIds = JGitUtil.getAllCommitIds(git)
@@ -210,7 +223,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
implicit val apiContext = api.JsonFormat.Context(baseUrl)
implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl)
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") {
@@ -285,12 +298,26 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// call web hook
callWebHookOf(owner, repository, WebHook.Push) {
for (pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner)) yield {
for {
pusherAccount <- getAccountByUserName(pusher)
ownerAccount <- getAccountByUserName(owner)
} yield {
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
newId = command.getNewId(), oldId = command.getOldId())
}
}
if (command.getType == ReceiveCommand.Type.CREATE) {
callWebHookOf(owner, repository, WebHook.Create) {
for {
pusherAccount <- getAccountByUserName(pusher)
ownerAccount <- getAccountByUserName(owner)
} yield {
val refType = if (refName(1) == "tags") "tag" else "branch"
WebHookCreatePayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
ref = branchName, refType = refType)
}
}
}
// call post-commit hook
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
@@ -309,6 +336,66 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook with WebHookService with AccountService with RepositoryService {
private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook])
override def onPostReceive(receivePack: ReceivePack, commands: util.Collection[ReceiveCommand]): Unit = {
Database() withTransaction { implicit session =>
try {
commands.asScala.headOption.foreach { command =>
implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl)
val refName = command.getRefName.split("/")
val commitIds = if (refName(1) == "tags") {
None
} else {
command.getType match {
case ReceiveCommand.Type.DELETE => None
case _ => Some((command.getOldId.getName, command.getNewId.name))
}
}
commitIds.map { case (oldCommitId, newCommitId) =>
val commits = using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
JGitUtil.getCommitLog(git, oldCommitId, newCommitId).flatMap { commit =>
val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false)
diffs.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") =>
val action = if(diff.changeType == ChangeType.ADD) "created" else "edited"
val fileName = diff.newPath
(action, fileName, commit.id)
}
}
}
val pages = commits
.groupBy { case (action, fileName, commitId) => fileName }
.map { case (fileName, commits) =>
(commits.head._1, fileName, commits.last._3)
}
callWebHookOf(owner, repository, WebHook.Gollum) {
for {
pusherAccount <- getAccountByUserName(pusher)
repositoryUser <- getAccountByUserName(owner)
repositoryInfo <- getRepository(owner, repository)
} yield {
WebHookGollumPayload(pages.toSeq, repositoryInfo, repositoryUser, pusherAccount)
}
}
}
}
} catch {
case ex: Exception => {
logger.error(ex.toString, ex)
throw ex
}
}
}
}
}
object GitLfs {
case class BatchRequest(

View File

@@ -1,25 +1,30 @@
package gitbucket.core.servlet
import java.io.File
import java.io.{File, FileOutputStream}
import akka.event.Logging
import com.typesafe.config.ConfigFactory
import gitbucket.core.GitBucketCoreModule
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.plugin.{PluginRegistry, PluginRepository}
import gitbucket.core.service.{ActivityService, SystemSettingsService}
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.JDBCUtil._
import gitbucket.core.model.Profile.profile.blockingApi._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContextEvent, ServletContextListener}
import org.apache.commons.io.{FileUtils, IOUtils}
import org.slf4j.LoggerFactory
import akka.actor.{Actor, Props, ActorSystem}
import akka.actor.{Actor, ActorSystem, Props}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import com.github.zafarkhaja.semver.{Version => Semver}
import scala.collection.JavaConverters._
/**
* Initialize GitBucket system.
* Update database schema and load plug-ins automatically in the context initializing.
@@ -54,44 +59,11 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
val manager = new JDBCVersionManager(conn)
// Check version
val versionFile = new File(GitBucketHome, "version")
if(versionFile.exists()){
val version = FileUtils.readFileToString(versionFile, "UTF-8")
if(version == "3.14"){
// Initialization for GitBucket 3.14
logger.info("Migration to GitBucket 4.x start")
// Backup current data
val dataMvFile = new File(GitBucketHome, "data.mv.db")
if(dataMvFile.exists) {
FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14"))
}
val dataTraceFile = new File(GitBucketHome, "data.trace.db")
if(dataTraceFile.exists) {
FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14"))
}
// Change form
manager.initialize()
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs =>
manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION"))
}
conn.update("DROP TABLE PLUGIN")
versionFile.delete()
logger.info("Migration to GitBucket 4.x completed")
} else {
throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.")
}
}
checkVersion(manager, conn)
// Run normal migration
logger.info("Start schema update")
val solidbase = new Solidbase()
solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
new Solidbase().migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
// Rescue code for users who updated from 3.14 to 4.0.0
// https://github.com/gitbucket/gitbucket/issues/1227
@@ -106,6 +78,9 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.")
}
// Install bundled plugins
extractBundledPlugins(gitbucketVersion)
// Load plugins
logger.info("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
@@ -117,7 +92,76 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
}
private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = {
logger.info("Check version")
val versionFile = new File(GitBucketHome, "version")
if(versionFile.exists()){
val version = FileUtils.readFileToString(versionFile, "UTF-8")
if(version == "3.14"){
// Initialization for GitBucket 3.14
logger.info("Migration to GitBucket 4.x start")
// Backup current data
val dataMvFile = new File(GitBucketHome, "data.mv.db")
if(dataMvFile.exists) {
FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14"))
}
val dataTraceFile = new File(GitBucketHome, "data.trace.db")
if(dataTraceFile.exists) {
FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14"))
}
// Change form
manager.initialize()
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs =>
manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION"))
}
conn.update("DROP TABLE PLUGIN")
versionFile.delete()
logger.info("Migration to GitBucket 4.x completed")
} else {
throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.")
}
}
}
private def extractBundledPlugins(gitbucketVersion: String): Unit = {
logger.info("Extract bundled plugins")
val cl = Thread.currentThread.getContextClassLoader
try {
using(cl.getResourceAsStream("plugins/plugins.json")){ pluginsFile =>
if(pluginsFile != null){
val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
val plugins = PluginRepository.parsePluginJson(pluginsJson)
plugins.foreach { plugin =>
plugin.versions.sortBy { x => Semver.valueOf(x.version) }.reverse.zipWithIndex.foreach { case (version, i) =>
val file = new File(PluginRepository.LocalRepositoryDir, version.file)
if(!file.exists) {
logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
FileUtils.forceMkdirParent(file)
using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)){ case (in, out) => IOUtils.copy(in, out) }
if(plugin.default && i == 0){
logger.info(s"Enable ${file.getName} in default")
FileUtils.copyFile(file, new File(PluginHome, version.file))
}
}
}
}
}
}
} catch {
case e: Exception => logger.error("Error in extracting bundled plugin", e)
}
}
override def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown Quartz scheduler
@@ -146,4 +190,4 @@ class DeleteOldActivityActor extends Actor with SystemSettingsService with Activ
}
}
}
}
}

View File

@@ -19,7 +19,7 @@ class PluginAssetsServlet extends HttpServlet {
.find { case (prefix, _, _) => path.startsWith("/plugin-assets" + prefix) }
.flatMap { case (prefix, resourcePath, classLoader) =>
val resourceName = path.substring(("/plugin-assets" + prefix).length)
Option(classLoader.getResourceAsStream(resourcePath.replaceFirst("^/", "") + resourceName))
Option(classLoader.getResourceAsStream(resourcePath.stripPrefix("/") + resourceName))
}
.map { in =>
try {

View File

@@ -0,0 +1,47 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http.HttpServletRequest
import gitbucket.core.controller.ControllerBase
import gitbucket.core.plugin.PluginRegistry
class PluginControllerFilter extends Filter {
private var filterConfig: FilterConfig = null
override def init(filterConfig: FilterConfig): Unit = {
this.filterConfig = filterConfig
}
override def destroy(): Unit = {
PluginRegistry().getControllers().foreach { case (controller, _) =>
controller.destroy()
}
}
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI
PluginRegistry().getControllers()
.filter { case (_, path) =>
val start = path.replaceFirst("/\\*$", "/")
(requestUri + "/").startsWith(start)
}
.foreach { case (controller, _) =>
controller match {
case x: ControllerBase if(x.config == null) => x.init(filterConfig)
case _ => ()
}
val mockChain = new MockFilterChain()
controller.doFilter(request, response, mockChain)
if(mockChain.continue == false){
return ()
}
}
chain.doFilter(request, response)
}
}

View File

@@ -19,7 +19,7 @@ import org.apache.sshd.server.scp.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException
object GitCommand {
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
}
@@ -154,7 +154,7 @@ class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCo
}
}
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshUrl: Option[String]) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService with DeployKeyService {
override protected def runTask(authType: AuthType): Unit = {
@@ -169,7 +169,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
val repository = git.getRepository
val receive = new ReceivePack(repository)
if (!repoName.endsWith(".wiki")) {
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl)
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
}
@@ -216,19 +216,26 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) exte
}
class GitCommandFactory(baseUrl: String) extends CommandFactory {
class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
import GitCommand._
logger.debug(s"command: $command")
command match {
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
case _ => new UnknownCommand(command)
val pluginCommand = PluginRegistry().getSshCommandProviders.collectFirst {
case f if f.isDefinedAt(command) => f(command)
}
pluginCommand match {
case Some(x) => x
case None => command match {
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl)
case _ => new UnknownCommand(command)
}
}
}

View File

@@ -6,7 +6,7 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.util.{Directory}
import gitbucket.core.util.Directory
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
@@ -22,7 +22,7 @@ object SshServer {
provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setCommandFactory(new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}")))
server.setShellFactory(new NoShell(sshAddress))
}

View File

@@ -97,16 +97,10 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService with A
{
defining(request.paths){ paths =>
getRepository(paths(0), paths(1)).map { repository =>
if(!repository.repository.isPrivate){
if(isReadable(repository.repository, context.loginAccount)){
action(repository)
} else {
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
Unauthorized()
}
} getOrElse NotFound()
}

View File

@@ -4,11 +4,15 @@ import com.typesafe.config.ConfigFactory
import java.io.File
import Directory._
import com.github.takezoe.slick.blocking.{BlockingH2Driver, BlockingMySQLDriver, BlockingJdbcProfile}
import ConfigUtil._
import com.github.takezoe.slick.blocking.{BlockingH2Driver, BlockingJdbcProfile, BlockingMySQLDriver}
import gitbucket.core.util.SyntaxSugars.defining
import liquibase.database.AbstractJdbcDatabase
import liquibase.database.core.{H2Database, MySQLDatabase, PostgresDatabase}
import org.apache.commons.io.FileUtils
import scala.reflect.ClassTag
object DatabaseConfig {
private lazy val config = {
@@ -30,14 +34,14 @@ object DatabaseConfig {
ConfigFactory.parseFile(file)
}
private lazy val dbUrl = config.getString("db.url")
private lazy val dbUrl = getValue("db.url", config.getString) //config.getString("db.url")
def url(directory: Option[String]): String =
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
lazy val url : String = url(None)
lazy val user : String = config.getString("db.user")
lazy val password : String = config.getString("db.password")
lazy val user : String = getValue("db.user", config.getString)
lazy val password : String = getValue("db.password", config.getString)
lazy val jdbcDriver : String = DatabaseType(url).jdbcDriver
lazy val slickDriver : BlockingJdbcProfile = DatabaseType(url).slickDriver
lazy val liquiDriver : AbstractJdbcDatabase = DatabaseType(url).liquiDriver
@@ -47,8 +51,16 @@ object DatabaseConfig {
lazy val minimumIdle : Option[Int] = getOptionValue("db.minimumIdle" , config.getInt)
lazy val maximumPoolSize : Option[Int] = getOptionValue("db.maximumPoolSize" , config.getInt)
private def getValue[T](path: String, f: String => T): T = {
getSystemProperty(path).getOrElse(getEnvironmentVariable(path).getOrElse{
f(path)
})
}
private def getOptionValue[T](path: String, f: String => T): Option[T] = {
if(config.hasPath(path)) Some(f(path)) else None
getSystemProperty(path).orElse(getEnvironmentVariable(path).orElse {
if(config.hasPath(path)) Some(f(path)) else None
})
}
}
@@ -80,7 +92,7 @@ object DatabaseType {
}
object MySQL extends DatabaseType {
val jdbcDriver = "com.mysql.jdbc.Driver"
val jdbcDriver = "org.mariadb.jdbc.Driver"
val slickDriver = BlockingMySQLDriver
val liquiDriver = new MySQLDatabase()
}
@@ -99,3 +111,33 @@ object DatabaseType {
}
}
}
object ConfigUtil {
def getEnvironmentVariable[A](key: String): Option[A] = {
val value = System.getenv("GITBUCKET_" + key.toUpperCase.replace('.', '_'))
if(value != null && value.nonEmpty){
Some(convertType(value)).asInstanceOf[Option[A]]
} else {
None
}
}
def getSystemProperty[A](key: String): Option[A] = {
val value = System.getProperty("gitbucket." + key)
if(value != null && value.nonEmpty){
Some(convertType(value)).asInstanceOf[Option[A]]
} else {
None
}
}
def convertType[A: ClassTag](value: String) =
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
if(c == classOf[Boolean]) value.toBoolean
else if(c == classOf[Long]) value.toLong
else if(c == classOf[Int]) value.toInt
else value
}
}

View File

@@ -28,8 +28,6 @@ object FileUtil {
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isUploadableType(name: String): Boolean = mimeTypeWhiteList contains getMimeType(name)
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0)
@@ -53,24 +51,34 @@ object FileUtil {
}
}
val mimeTypeWhiteList: Array[String] = Array(
"application/pdf",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"image/gif",
"image/jpeg",
"image/png",
"text/plain")
def getLfsFilePath(owner: String, repository: String, oid: String): String =
Directory.getLfsDir(owner, repository) + "/" + oid
def readableSize(size: Long): String = FileUtils.byteCountToDisplaySize(size)
/**
* Delete the given directory if it's empty.
* Do nothing if the given File is not a directory or not empty.
*/
def deleteDirectoryIfEmpty(dir: File): Unit = {
if(dir.isDirectory() && dir.list().isEmpty) {
FileUtils.deleteDirectory(dir)
}
}
/**
* Delete file or directory forcibly.
*/
def deleteIfExists(file: java.io.File): java.io.File = {
if(file.exists){
FileUtils.forceDelete(file)
}
file
}
lazy val MaxFileSize = if (System.getProperty("gitbucket.maxFileSize") != null)
System.getProperty("gitbucket.maxFileSize").toLong
else
3 * 1024 * 1024
}

View File

@@ -22,7 +22,8 @@ object Implicits {
// Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = JsonFormat.Context(context.baseUrl)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x => s"${x.genericUser}@${x.host}:${x.port}" })
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
@@ -77,11 +78,6 @@ object Implicits {
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^" + quote(request.getContextPath) + "/git/", "/")
def baseUrl:String = {
val url = request.getRequestURL.toString
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
url.substring(0, len).stripSuffix("/")
}
}
implicit class RichSession(private val session: HttpSession) extends AnyVal {

View File

@@ -75,7 +75,7 @@ object JDBCUtil {
var stringLiteral = false
while({ length = in.read(bytes); length != -1 }){
for(i <- 0 to length - 1){
for(i <- 0 until length){
val c = bytes(i)
if(c == '\''){
stringLiteral = !stringLiteral
@@ -146,13 +146,11 @@ object JDBCUtil {
}
}
val columnValues = values.map { value =>
value match {
case x: String => "'" + x.replace("'", "''") + "'"
case x: Timestamp => "'" + dateFormat.format(x) + "'"
case null => "NULL"
case x => x
}
val columnValues = values.map {
case x: String => "'" + x.replace("'", "''") + "'"
case x: Timestamp => "'" + dateFormat.format(x) + "'"
case null => "NULL"
case x => x
}
sb.append(columnValues.mkString(", "))
sb.append(");\n")

View File

@@ -1,5 +1,7 @@
package gitbucket.core.util
import java.io.ByteArrayOutputStream
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
import Directory._
@@ -14,15 +16,17 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec
import java.util.Date
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import org.cache2k.{Cache2kBuilder, CacheEntry}
import org.cache2k.Cache2kBuilder
import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException}
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter, RawTextComparator}
import org.eclipse.jgit.dircache.DirCacheEntry
import org.eclipse.jgit.util.io.DisabledOutputStream
import org.slf4j.LoggerFactory
/**
@@ -93,7 +97,7 @@ object JGitUtil {
val summary = getSummaryMessage(fullMessage, shortMessage)
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
val description = defining(fullMessage.trim.indexOf('\n')){ i =>
if(i >= 0){
Some(fullMessage.trim.substring(i).trim)
} else None
@@ -114,7 +118,8 @@ object JGitUtil {
newObjectId: Option[String],
oldMode: String,
newMode: String,
tooLarge: Boolean
tooLarge: Boolean,
patch: Option[String]
)
/**
@@ -146,9 +151,10 @@ object JGitUtil {
*
* @param name the module name
* @param path the path in the repository
* @param url the repository url of this module
* @param repositoryUrl the repository url of this module
* @param viewerUrl the repository viewer url of this module
*/
case class SubmoduleInfo(name: String, path: String, url: String)
case class SubmoduleInfo(name: String, path: String, repositoryUrl: String, viewerUrl: String)
case class BranchMergeInfo(ahead: Int, behind: Int, isMerged: Boolean)
@@ -184,11 +190,9 @@ object JGitUtil {
val dir = git.getRepository.getDirectory
val keyPrefix = dir.getAbsolutePath + "@"
cache.forEach(new Consumer[CacheEntry[String, Int]] {
override def accept(entry: CacheEntry[String, Int]): Unit = {
if(entry.getKey.startsWith(keyPrefix)){
cache.remove(entry.getKey)
}
cache.keys.forEach(key => {
if (key.startsWith(keyPrefix)) {
cache.remove(key)
}
})
}
@@ -226,9 +230,14 @@ object JGitUtil {
ref.getName.stripPrefix("refs/heads/")
}.toList,
// tags
git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
git.tagList.call.asScala.flatMap { ref =>
try {
val revCommit = getRevCommitFromId(git, ref.getObjectId)
Some(TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName))
} catch {
case _: IncorrectObjectTypeException =>
None
}
}.sortBy(_.time).toList
)
} catch {
@@ -244,9 +253,10 @@ object JGitUtil {
* @param git the Git object
* @param revision the branch name or commit id
* @param path the directory path (optional)
* @param baseUrl the base url of GitBucket instance. This parameter is used to generate links of submodules (optional)
* @return HTML of the file list
*/
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
def getFileList(git: Git, revision: String, path: String = ".", baseUrl: Option[String] = None): List[FileInfo] = {
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
if(objectId == null) return Nil
@@ -288,7 +298,7 @@ object JGitUtil {
@tailrec
def findLastCommits(result:List[(ObjectId, FileMode, String, String, Option[String], RevCommit)],
restList:List[((ObjectId, FileMode, String, String, Option[String]), Map[RevCommit, RevCommit])],
revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, String, Option[String], RevCommit)] ={
revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, String, Option[String], RevCommit)] = {
if(restList.isEmpty){
result
} else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty
@@ -332,7 +342,7 @@ object JGitUtil {
useTreeWalk(revCommit){ treeWalk =>
while (treeWalk.next()) {
val linkUrl = if (treeWalk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
getSubmodules(git, revCommit.getTree, baseUrl).find(_.path == treeWalk.getPathString).map(_.viewerUrl)
} else None
fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, treeWalk.getPathString, linkUrl)
}
@@ -359,9 +369,9 @@ object JGitUtil {
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
case (false, true ) => false
case _ => file1.name.compareTo(file2.name) < 0
case _ => file1.name.compareTo(file2.name) < 0
}
}.toList
}
}
}
@@ -369,7 +379,7 @@ object JGitUtil {
* Returns the first line of the commit message.
*/
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
defining(fullMessage.trim.indexOf("\n")){ i =>
defining(fullMessage.trim.indexOf('\n')){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
@@ -509,90 +519,49 @@ object JGitUtil {
}.toMap
}
/**
* Returns the tuple of diff of the given commit and the previous commit id.
*/
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
i.hasNext match {
case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next)
case _ => logs
}
def getPatch(git: Git, from: Option[String], to: String): String = {
val out = new ByteArrayOutputStream()
val df = new DiffFormatter(out)
df.setRepository(git.getRepository)
df.setDiffComparator(RawTextComparator.DEFAULT)
df.setDetectRenames(true)
df.format(getDiffEntries(git, from, to).head)
new String(out.toByteArray, "UTF-8")
}
private def getDiffEntries(git: Git, from: Option[String], to: String): Seq[DiffEntry] = {
using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
val commits = getCommitLog(revWalk.iterator, Nil)
val revCommit = commits(0)
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
df.setRepository(git.getRepository)
if(commits.length >= 2){
// not initial commit
val oldCommit = if(revCommit.getParentCount >= 2) {
// merge commit
revCommit.getParents.head
} else {
commits(1)
}
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
} else {
// initial commit
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.setRecursive(true)
treeWalk.addTree(revCommit.getTree)
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
while(treeWalk.next){
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
buffer.append((if(!fetchContent){
DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
newPath = treeWalk.getPathString,
oldContent = None,
newContent = None,
oldIsImage = false,
newIsImage = newIsImage,
oldObjectId = None,
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString,
tooLarge = false
)
} else {
DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
newPath = treeWalk.getPathString,
oldContent = None,
newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
oldIsImage = false,
newIsImage = newIsImage,
oldObjectId = None,
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString,
tooLarge = false
)
}))
val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
from match {
case None => {
toCommit.getParentCount match {
case 0 => df.scan(new EmptyTreeIterator(), new CanonicalTreeParser(null, git.getRepository.newObjectReader(), toCommit.getTree)).asScala
case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
}
(buffer.toList, None)
}
case Some(from) => {
val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
df.scan(fromCommit.getTree, toCommit.getTree).asScala
}
}
}
}
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = {
val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
def getParentCommitId(git: Git, id: String): Option[String] = {
using(new RevWalk(git.getRepository)){ revWalk =>
val commit = revWalk.parseCommit(git.getRepository.resolve(id))
commit.getParentCount match {
case 0 => None
case _ => Some(commit.getParent(0).getName)
}
}
}
val newTreeIter = new CanonicalTreeParser
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
import scala.collection.JavaConverters._
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
val diffs = git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala
def getDiffs(git: Git, from: Option[String], to: String, fetchContent: Boolean, makePatch: Boolean): List[DiffInfo] = {
val diffs = getDiffEntries(git, from, to)
diffs.map { diff =>
if(diffs.size > 100){
DiffInfo(
@@ -607,7 +576,8 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = true
tooLarge = true,
patch = None
)
} else {
val oldIsImage = FileUtil.isImage(diff.getOldPath)
@@ -625,7 +595,8 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false
tooLarge = false,
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
)
} else {
DiffInfo(
@@ -640,13 +611,23 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false
tooLarge = false,
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
)
}
}
}.toList
}
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
val out = new ByteArrayOutputStream()
using(new DiffFormatter(out)){ formatter =>
formatter.setRepository(git.getRepository)
formatter.format(diff)
val patch = new String(out.toByteArray) // TODO charset???
patch.split("\n").drop(4).mkString("\n")
}
}
/**
* Returns the list of branch names of the specified commit.
@@ -751,7 +732,7 @@ object JGitUtil {
/**
* Read submodule information from .gitmodules
*/
def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = {
def getSubmodules(git: Git, tree: RevTree, baseUrl: Option[String]): List[SubmoduleInfo] = {
val repository = git.getRepository
getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
(try {
@@ -759,7 +740,7 @@ object JGitUtil {
config.getSubsections("submodule").asScala.map { module =>
val path = config.getString("submodule", module, "path")
val url = config.getString("submodule", module, "url")
SubmoduleInfo(module, path, url)
SubmoduleInfo(module, path, url, StringUtil.getRepositoryViewerUrl(url, baseUrl))
}
} catch {
case e: ConfigInvalidException => {
@@ -823,17 +804,22 @@ object JGitUtil {
}
}
def isLfsPointer(loader: ObjectLoader): Boolean = {
!loader.isLarge && new String(loader.getBytes(), "UTF-8").startsWith("version https://git-lfs.github.com/spec/v1")
}
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
// Viewer
using(git.getRepository.getObjectDatabase){ db =>
val loader = db.open(objectId)
val isLfs = isLfsPointer(loader)
val large = FileUtil.isLarge(loader.getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
val size = Some(getContentSize(loader))
if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
if(!isLfs && bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", size, Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
} else {
@@ -994,13 +980,13 @@ object JGitUtil {
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
Option(git.getRepository.resolve(id)).map{ commitId =>
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository);
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository)
blamer.setStartCommit(commitId)
blamer.setFilePath(path)
val blame = blamer.call()
var blameMap = Map[String, JGitUtil.BlameInfo]()
var idLine = List[(String, Int)]()
val commits = 0.to(blame.getResultContents().size()-1).map{ i =>
val commits = 0.to(blame.getResultContents().size() - 1).map { i =>
val c = blame.getSourceCommit(i)
if(!blameMap.contains(c.name)){
blameMap += c.name -> JGitUtil.BlameInfo(
@@ -1010,7 +996,7 @@ object JGitUtil {
c.getAuthorIdent.getWhen,
Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next)
.map(_.name),
if(blame.getSourcePath(i)==path){ None }else{ Some(blame.getSourcePath(i)) },
if(blame.getSourcePath(i)==path){ None } else { Some(blame.getSourcePath(i)) },
c.getCommitterIdent.getWhen,
c.getShortMessage,
Set.empty)

View File

@@ -0,0 +1,68 @@
package gitbucket.core.util
import gitbucket.core.model.Account
import gitbucket.core.service.SystemSettingsService
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import SystemSettingsService.SystemSettings
class Mailer(settings: SystemSettings){
def send(to: String, subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Unit = {
createMail(subject, textMsg, htmlMsg, loginAccount).foreach { email =>
email.addTo(to).send
}
}
def sendBcc(bcc: Seq[String], subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Unit = {
createMail(subject, textMsg, htmlMsg, loginAccount).foreach { email =>
bcc.foreach { address =>
email.addBcc(address)
}
email.send()
}
}
def createMail(subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Option[HtmlEmail] = {
if(settings.notification == true){
settings.smtp.map { smtp =>
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
if(ssl == true) {
email.setSslSmtpPort(smtp.port.get.toString)
}
}
smtp.starttls.foreach { starttls =>
email.setStartTLSEnabled(starttls)
email.setStartTLSRequired(starttls)
}
smtp.fromAddress
.map (_ -> smtp.fromName.getOrElse(loginAccount.map(_.userName).getOrElse("GitBucket")))
.orElse (Some("notifications@gitbucket.com" -> loginAccount.map(_.userName).getOrElse("GitBucket")))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setTextMsg(textMsg)
htmlMsg.foreach { msg =>
email.setHtmlMsg(msg)
}
email
}
} else None
}
}
//class MockMailer extends Notifier {
// def toNotify(subject: String, textMsg: String, htmlMsg: Option[String] = None)
// (recipients: Account => Session => Seq[String])(implicit context: Context): Unit = ()
//}

View File

@@ -1,142 +0,0 @@
package gitbucket.core.util
import gitbucket.core.model.{Session, Issue, Account}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService}
import gitbucket.core.servlet.Database
import gitbucket.core.view.Markdown
import scala.concurrent._
import scala.util.{Success, Failure}
import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory
import gitbucket.core.controller.Context
import SystemSettingsService.Smtp
import SyntaxSugars.defining
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: Issue, loginAccount: Account)(notify: String => Unit)(implicit session: Session) =
(
// individual repository's owner
issue.userName ::
// group members of group repository
getGroupMembers(issue.userName).map(_.userName) :::
// collaborators
getCollaboratorUserNames(issue.userName, issue.repositoryName) :::
// participants
issue.openedUserName ::
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
)
.distinct
.withFilter ( _ != loginAccount.userName ) // the operation in person is excluded
.foreach (
getAccountByUserName(_)
.filterNot (_.isGroupAccount)
.filterNot (LDAPUtil.isDummyMailAddress(_))
.foreach (x => notify(x.mailAddress))
)
}
object Notifier {
// TODO We want to be able to switch to mock.
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
case _ => new MockMailer
}
def msgIssue(url: String) = (content: String) => s"""
|${content}<br/>
|--<br/>
|<a href="${url}">View it on GitBucket</a>
""".stripMargin
def msgPullRequest(url: String) = (content: String) => s"""
|${content}<hr/>
|View, comment on, or merge it at:<br/>
|<a href="${url}">${url}</a>
""".stripMargin
def msgComment(url: String) = (content: String) => s"""
|${content}<br/>
|--<br/>
|<a href="${url}">View it on GitBucket</a>
""".stripMargin
def msgStatus(url: String) = (content: String) => s"""
|${content} <a href="${url}">#${url split('/') last}</a>
""".stripMargin
}
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context): Unit = {
context.loginAccount.foreach { loginAccount =>
val database = Database()
val f = Future {
database withSession { implicit session =>
defining(
s"[${r.owner}/${r.name}] ${issue.title} (#${issue.issueId})" ->
msg(Markdown.toHtml(
markdown = content,
repository = r,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = false,
enableLineBreaks = false
))
) { case (subject, msg) =>
recipients(issue, loginAccount) { to => send(to, subject, msg, loginAccount) }
}
}
"Notifications Successful."
}
f.onComplete {
case Success(s) => logger.debug(s)
case Failure(t) => logger.error("Notifications Failed.", t)
}
}
}
def send(to: String, subject: String, msg: String, loginAccount: Account): Unit = {
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
if(ssl == true) {
email.setSslSmtpPort(smtp.port.get.toString)
}
}
smtp.starttls.foreach { starttls =>
email.setStartTLSEnabled(starttls)
email.setStartTLSRequired(starttls)
}
smtp.fromAddress
.map (_ -> smtp.fromName.getOrElse(loginAccount.userName))
.orElse (Some("notifications@gitbucket.com" -> loginAccount.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).send
}
}
class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context): Unit = {}
}

View File

@@ -1,7 +1,7 @@
package gitbucket.core.util
// TODO Move to gitbucket.core.api package?
case class RepositoryName(owner:String, name:String){
case class RepositoryName(owner: String, name: String){
val fullName = s"${owner}/${name}"
}

View File

@@ -123,19 +123,22 @@ object StringUtil {
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r
.findAllIn(message).matchData.map(_.group(1)).toSeq.distinct
private val GitBucketUrlPattern = "^(https?://.+)/git/(.+?)/(.+?)\\.git$".r
private val GitHubUrlPattern = "^https://(.+@)?github\\.com/(.+?)/(.+?)\\.git$".r
private val BitBucketUrlPattern = "^https?://(.+@)?bitbucket\\.org/(.+?)/(.+?)\\.git$".r
private val GitLabUrlPattern = "^https?://(.+@)?gitlab\\.com/(.+?)/(.+?)\\.git$".r
// /**
// * Encode search string for LIKE condition.
// * This method has been copied from Slick's SqlUtilsComponent.
// */
// def likeEncode(s: String) = {
// val b = new StringBuilder
// for(c <- s) c match {
// case '%' | '_' | '^' => b append '^' append c
// case _ => b append c
// }
// b.toString
// }
def getRepositoryViewerUrl(gitRepositoryUrl: String, baseUrl: Option[String]): String = {
def removeUserName(baseUrl: String): String = baseUrl.replaceFirst("(https?://).+@", "$1")
gitRepositoryUrl match {
case GitBucketUrlPattern(base, user, repository) if baseUrl.map(removeUserName(base).startsWith).getOrElse(false)
=> s"${removeUserName(base)}/$user/$repository"
case GitHubUrlPattern (_, user, repository) => s"https://github.com/$user/$repository"
case BitBucketUrlPattern(_, user, repository) => s"https://bitbucket.org/$user/$repository"
case GitLabUrlPattern (_, user, repository) => s"https://gitlab.com/$user/$repository"
case _ => gitRepositoryUrl
}
}
}

View File

@@ -53,4 +53,14 @@ object SyntaxSugars {
def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t)
}
/**
* Provides easier and explicit ways to access to a head value of `Map[String, Seq[String]]`.
* This is intended to use in implementations of scalatra-forms's `Constraint` or `ValueType`.
*/
implicit class HeadValueAccessibleMap(map: Map[String, Seq[String]]){
def value(key: String): String = map(key).head
def optionValue(key: String): Option[String] = map.get(key).flatMap(_.headOption)
def values(key: String): Seq[String] = map.get(key).getOrElse(Seq.empty)
}
}

View File

@@ -1,6 +1,6 @@
package gitbucket.core.util
import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
import org.scalatra.i18n.Messages
trait Validations {
@@ -19,6 +19,19 @@ trait Validations {
}
}
/**
* Constraint for the password.
*/
def password: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(System.getProperty("gitbucket.validate.password") != "false" && !value.matches("[a-zA-Z0-9\\-_.]+")){
Some(s"${name} contains invalid character.")
} else {
None
}
}
/**
* Constraint for the repository identifier.
*/

View File

@@ -38,7 +38,6 @@ object Markdown {
val source = if(enableTaskList) escapeTaskList(markdown) else markdown
val options = new Options()
options.setSanitize(true)
options.setBreaks(enableLineBreaks)
val renderer = new GitBucketMarkedRenderer(options, repository,

View File

@@ -128,7 +128,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = {
val fileName = filePath.reverse.head.toLowerCase
val fileName = filePath.last.toLowerCase
val extension = FileUtil.getExtension(fileName)
val renderer = PluginRegistry().getRenderer(extension)
renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context))
@@ -346,10 +346,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
// This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string)
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
private[this] val urlRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
def detectAndRenderLinks(text: String, repository: RepositoryInfo)(implicit context: Context): String = {
val matches = detectAndRenderLinksRegex.findAllMatchIn(text).toSeq
def urlLink(text: String): String = {
val matches = urlRegex.findAllMatchIn(text).toSeq
val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) =>
val url = m.group(0)
@@ -361,8 +361,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
// append rest fragment
val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x
decorateHtml(HtmlFormat.fill(out).toString, repository)
HtmlFormat.fill(out).toString
}
/**

View File

@@ -2,8 +2,7 @@
personalTokens: List[gitbucket.core.model.AccessToken],
gneratedToken: Option[(gitbucket.core.model.AccessToken, String)])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Applications"){
<div class="container body">
@gitbucket.core.account.html.menu("application", context.settings.ssh){
@gitbucket.core.account.html.menu("application", context.loginAccount.get.userName, false){
<div class="panel panel-default">
<div class="panel-heading strong">Personal access tokens</div>
<div class="panel-body">
@@ -49,5 +48,4 @@
</div>
</form>
}
</div>
}

View File

@@ -0,0 +1,14 @@
@(members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Create group"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Create group</h2>
<form id="form" method="post" action="@context.path/groups/new" validate="true">
@gitbucket.core.account.html.groupform(None, members, false)
<fieldset class="border-top">
<input type="submit" class="btn btn-success" value="Create group"/>
</fieldset>
</form>
</div>
</div>
}

View File

@@ -2,8 +2,7 @@
@import gitbucket.core.util.LDAPUtil
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("Edit your profile"){
<div class="container body">
@gitbucket.core.account.html.menu("profile", context.settings.ssh){
@gitbucket.core.account.html.menu("profile", context.loginAccount.get.userName, false){
@gitbucket.core.helper.html.information(info)
@gitbucket.core.helper.html.error(error)
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
@@ -57,11 +56,9 @@
<a href="@context.path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div>
<input type="submit" class="btn btn-success" value="Save"/>
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@helpers.url(account.userName)" class="btn btn-default">Cancel</a>}
</div>
</form>
}
</div>
}
<script>
$(function(){

View File

@@ -0,0 +1,19 @@
@(account: gitbucket.core.model.Account,
members: List[gitbucket.core.model.GroupMember],
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("Edit group"){
@gitbucket.core.account.html.menu("profile", account.userName, true){
@gitbucket.core.helper.html.information(info)
<h2>Edit group</h2>
<form id="form" method="post" action="@context.path/@account.userName/_editgroup" validate="true">
@gitbucket.core.account.html.groupform(Some(account), members, false)
<fieldset class="border-top">
<div class="pull-right">
<a href="@helpers.url(account.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete group</a>
</div>
<input type="submit" class="btn btn-success" value="Update group"/>
</fieldset>
</form>
}
}

View File

@@ -0,0 +1,16 @@
@(webHook: gitbucket.core.model.AccountWebHook,
events: Set[gitbucket.core.model.WebHook.Event],
account: gitbucket.core.model.Account,
create: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("Service Hooks"){
@gitbucket.core.account.html.menu("hooks", account.userName, account.isGroupAccount){
@gitbucket.core.settings.html.edithookform(
webHook, events, create,
helpers.url(account.userName) + "/_hooks/new",
helpers.url(account.userName) + "/_hooks/edit",
helpers.url(account.userName) + "/_hooks/delete",
helpers.url(account.userName) + "/_hooks/test"
)
}
}

View File

@@ -1,145 +0,0 @@
@(account: Option[gitbucket.core.model.Account], members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>@{if(account.isEmpty) "Create group" else "Edit group"}</h2>
<form id="form" method="post" action="@if(account.isEmpty){@context.path/groups/new} else {@context.path/@account.get.userName/_editgroup}" validate="true">
<div class="row">
<div class="col-md-5">
<fieldset class="form-group">
<label for="groupName" class="strong">Group name</label>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset class="form-group">
<label class="strong">URL (Optional)</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
</fieldset>
<fieldset class="form-group">
<label for="groupDescription" class="strong">Description (Optional)</label>
<div>
<textarea name="description" id="description" class="form-control">@account.map(_.description)</textarea>
</div>
</fieldset>
<fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label>
@gitbucket.core.helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="col-md-7">
<fieldset class="form-group">
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("memberName", 200, true, false)
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
</div>
</fieldset>
</div>
</div>
<fieldset class="border-top">
@if(account.isDefined){
<div class="pull-right">
<a href="@helpers.url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete group</a>
</div>
}
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create group} else {Update group}"/>
@if(account.isDefined){
<a href="@helpers.url(account.get.userName)" class="btn btn-default">Cancel</a>
}
</fieldset>
</form>
</div>
</div>
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-members').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#member-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-members').text('User has been already added.');
return false;
}
// check existence
$.post('@context.path/_user/existence', { 'userName': userName },
function(data, status){
if(data == 'user'){
addMemberHTML(userName, false);
} else {
$('#error-members').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
$(this).parent().remove();
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
return !(e.keyCode == 13);
});
$('#delete').click(function(){
return confirm('Once you delete this group, there is no going back.\nAre you sure?');
});
@members.map { member =>
addMemberHTML('@member.userName', @member.isManager);
}
function addMemberHTML(userName, isManager){
var memberButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="false" name="' + userName + '">Member</label>');
if(!isManager){
memberButton.addClass('active');
}
var managerButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="true" name="' + userName + '">Manager</label>');
if(isManager){
managerButton.addClass('active');
}
$('#member-list').append($('<li>')
.data('name', userName)
.append($('<div class="btn-group is_manager" data-toggle="buttons">')
.append(memberButton)
.append(managerButton))
.append(' ')
.append($('<a>').attr('href', '@context.path/' + userName).text(userName))
.append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateMembers(){
var members = $('#member-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
}).get().join(',');
$('#members').val(members);
}
});
</script>

View File

@@ -0,0 +1,132 @@
@(account: Option[gitbucket.core.model.Account],
members: List[gitbucket.core.model.GroupMember],
admin: Boolean)(implicit context: gitbucket.core.controller.Context)
<div class="row">
<div class="col-md-5">
<fieldset class="form-group">
<label for="groupName" class="strong">Group name</label>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
@if(account.isDefined && admin){
<label for="removed">
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
Disable
</label>
}
</fieldset>
<fieldset class="form-group">
<label class="strong">URL (Optional)</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
</fieldset>
<fieldset class="form-group">
<label for="groupDescription" class="strong">Description (Optional)</label>
<div>
<textarea name="description" id="description" class="form-control">@account.map(_.description)</textarea>
</div>
</fieldset>
<fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label>
@gitbucket.core.helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="col-md-7">
<fieldset class="form-group">
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("memberName", 200, true, false)
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
</div>
</fieldset>
</div>
</div>
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-members').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#member-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-members').text('User has been already added.');
return false;
}
// check existence
$.post('@context.path/_user/existence', { 'userName': userName },
function(data, status){
if(data == 'user'){
addMemberHTML(userName, false);
} else {
$('#error-members').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
$(this).parent().remove();
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
return !(e.keyCode == 13);
});
$('#delete').click(function(){
return confirm('Once you delete this group, there is no going back.\nAre you sure?');
});
@members.map { member =>
addMemberHTML('@member.userName', @member.isManager);
}
function addMemberHTML(userName, isManager){
var memberButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="false" name="' + userName + '">Member</label>');
if(!isManager){
memberButton.addClass('active');
}
var managerButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="true" name="' + userName + '">Manager</label>');
if(isManager){
managerButton.addClass('active');
}
$('#member-list').append($('<li>')
.data('name', userName)
.append($('<div class="btn-group is_manager" data-toggle="buttons">')
.append(memberButton)
.append(managerButton))
.append(' ')
.append($('<a>').attr('href', '@context.path/' + userName).text(userName))
.append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateMembers(){
var members = $('#member-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
}).get().join(',');
$('#members').val(members);
}
});
</script>

View File

@@ -0,0 +1,42 @@
@(account: gitbucket.core.model.Account,
webHooks: List[(gitbucket.core.model.AccountWebHook, Set[gitbucket.core.model.WebHook.Event])],
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("Service Hooks"){
@gitbucket.core.account.html.menu("hooks", account.userName, account.isGroupAccount){
@gitbucket.core.helper.html.information(info)
<div class="panel panel-default">
<div class="panel-heading strong">
Webhooks
</div>
<div class="panel-body">
<p>
Webhooks allow external services to be notified when certain events happen within your repository.
When the specified events happen, well send a POST request to each of the URLs you provide.
Learn more in <a href="https://github.com/takezoe/gitbucket/wiki/API-WebHook" target="_blank">GitBucket Wiki Webhook Page</a>.
</p>
<a href="@helpers.url(account.userName)/_hooks/new" class="btn btn-success pull-right" style="margin-bottom: 10px;">Add webhook</a>
<table class="table table-condensed" style="margin-bottom:0px;">
@webHooks.map { case (webHook, events) =>
<tr><td style="vertical-align: middle;">
<a href="@helpers.url(account.userName)/_hooks/edit?url=@helpers.urlEncode(webHook.url)" class="css-truncate" style="max-width:360px">
<span class="css-truncate-target">@webHook.url</span>
</a>
<em class="css-truncate" style="max-width: 225px;">(<span class="css-truncate-target">@events.map(_.name).mkString(", ")</span>)</em>
</td><td>
<div class="btn-group pull-right">
<a href="@helpers.url(account.userName)/_hooks/edit?url=@helpers.urlEncode(webHook.url)" class="btn btn-default">
<span class="octicon octicon-pencil"></span>
</a>
<a href="@helpers.url(account.userName)/_hooks/delete?url=@helpers.urlEncode(webHook.url)" class="btn btn-danger" onclick="return confirm('delete webhook for @webHook.url ?')">
<span class="octicon octicon-x"></span>
</a>
</div>
</td></tr>
}
</table>
</div>
</div>
}
}

View File

@@ -13,14 +13,14 @@
</div>
<div style="padding-left: 10px; padding-right: 10px;">
@account.description.map{ description =>
<p style="color: white;">@description</p>
<p>@description</p>
}
@if(account.url.isDefined){
<p style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a>
</p>
}
<p style="color: white;">
<p>
<i class="octicon octicon-clock"></i> Joined on @helpers.date(account.registeredDate)
</p>
</div>
@@ -43,6 +43,9 @@
} else {
<li@if(active == "activity"){ class="active"}><a href="@helpers.url(account.userName)?tab=activity">Public activity</a></li>
}
@*
<li@if(active == "webhooks"){ class="active"}><a href="@helpers.url(account.userName)?tab=webhooks">Webhooks</a></li>
*@
@gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab =>
@tab(account, context).map { link =>
<li@if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</a></li>

View File

@@ -1,24 +1,50 @@
@(active: String, ssh: Boolean)(body: Html)(implicit context: gitbucket.core.controller.Context)
@(active: String, userName: String, group: Boolean)(body: Html)(implicit context: gitbucket.core.controller.Context)
<div class="main-sidebar">
<div class="sidebar">
<ul class="sidebar-menu">
<li@if(active=="profile"){ class="active"}>
<a href="@context.path/@context.loginAccount.get.userName/_edit">Profile</a>
</li>
@if(ssh){
<li@if(active=="ssh"){ class="active"}>
<a href="@context.path/@context.loginAccount.get.userName/_ssh">SSH Keys</a>
</li>
}
<li@if(active=="application"){ class="active"}>
<a href="@context.path/@context.loginAccount.get.userName/_application">Applications</a>
</li>
@gitbucket.core.plugin.PluginRegistry().getAccountSettingMenus.map { menu =>
@menu(context).map { link =>
<li@if(active==link.id){ class="active"}>
<a href="@context.path/@link.path">@link.label</a>
@if(group){
<li class="menu-item-hover @if(active=="profile"){active}">
<a href="@context.path/@userName/_editgroup">
<i class="menu-icon octicon octicon-person"></i> <span>Profile</span>
</a>
</li>
<li class="menu-item-hover @if(active=="hooks"){active}">
<a href="@context.path/@userName/_hooks">
<i class="menu-icon octicon octicon-zap"></i> <span>Service Hooks</span>
</a>
</li>
} else {
<li class="menu-item-hover @if(active=="profile"){active}">
<a href="@context.path/@userName/_edit">
<i class="menu-icon octicon octicon-person"></i> <span>Profile</span>
</a>
</li>
@if(context.settings.ssh){
<li class="menu-item-hover @if(active=="ssh"){active}">
<a href="@context.path/@userName/_ssh">
<i class="menu-icon octicon octicon-key"></i> <span>SSH Keys</span>
</a>
</li>
}
<li class="menu-item-hover @if(active=="application"){active}">
<a href="@context.path/@userName/_application">
<i class="menu-icon octicon octicon-rocket"></i> <span>Applications</span>
</a>
</li>
<li class="menu-item-hover @if(active=="hooks"){active}">
<a href="@context.path/@userName/_hooks">
<i class="menu-icon octicon octicon-zap"></i> <span>Service Hooks</span>
</a>
</li>
@gitbucket.core.plugin.PluginRegistry().getAccountSettingMenus.map { menu =>
@menu(context).map { link =>
<li class="menu-item-hover @if(active==link.id){active}">
<a href="@context.path/@link.path">
<i class="menu-icon octicon octicon-plug"></i> <span>@link.label</span>
</a>
</li>
}
}
}
</ul>
</div>

View File

@@ -39,7 +39,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
</fieldset>
<fieldset class="form-group">
<label for="description" class="strong">Description (optional):</label>
<input type="text" name="description" id="description" class="form-control" style="width: 95%;"/>
<input type="text" name="description" id="description" class="form-control" />
</fieldset>
<fieldset class="border-top">
<label class="radio">
@@ -58,14 +58,30 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
</label>
</fieldset>
<fieldset class="border-top">
<label for="createReadme" class="checkbox">
<input type="checkbox" name="createReadme" id="createReadme"/>
<label class="radio">
<input type="radio" name="initOption" value="EMPTY" checked/>
<span class="strong">Create an empty repository</span>
<div class="normal muted">
Create an empty repository. You have to initialize by yourself initially.
</div>
</label>
<label class="radio">
<input type="radio" name="initOption" value="README"/>
<span class="strong">Initialize this repository with a README</span>
<div class="normal muted">
This will let you immediately clone the repository to your computer. Skip this step if youre importing an existing repository.
Create a repository which has README.md. You can clone the repository immediately.
</div>
</label>
<label class="radio">
<input type="radio" name="initOption" value="COPY"/>
<span class="strong">Copy existing git repository</span>
<div class="normal muted">
Create new repository from existing git repository.
</div>
</label>
</fieldset>
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" disabled placeholder="Source git repository URL..."/>
<span id="error-sourceUrl" class="error"></span>
<fieldset class="border-top form-actions">
<input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset>
@@ -83,4 +99,8 @@ $('#owner-dropdown a').click(function(){
$('#owner-dropdown span.strong').html($(this).find('span').html());
});
$('input[name=initOption]').click(function () {
$('#sourceUrl').prop('disabled', $('input[name=initOption]:checked').val() != 'COPY');
});
</script>

View File

@@ -1,8 +1,7 @@
@(account: gitbucket.core.model.Account, sshKeys: List[gitbucket.core.model.SshKey])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.ssh.SshUtil
@gitbucket.core.html.main("SSH Keys"){
<div class="container body">
@gitbucket.core.account.html.menu("ssh", context.settings.ssh){
@gitbucket.core.account.html.menu("ssh", context.loginAccount.get.userName, false){
<div class="panel panel-default">
<div class="panel-heading strong">SSH Keys</div>
<div class="panel-body">
@@ -37,5 +36,4 @@
</div>
</form>
}
</div>
}

View File

@@ -0,0 +1,89 @@
@(tables: Seq[gitbucket.core.controller.Table])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("Database viewer") {
@gitbucket.core.admin.html.menu("dbviewer") {
<div class="container">
<div class="col-md-3">
<div id="table-tree">
<ul>
@tables.map { table =>
<li data-jstree='{"icon":"@context.path/assets/common/images/table.gif"}'><a href="javascript:void(0);" class="table-link">@table.name</a>
<ul>
@table.columns.map { column =>
<li data-jstree='{"icon":"@context.path/assets/common/images/column.gif"}'>@column.name
@if(column.primaryKey){ (PK) }
</li>
}
</ul>
</li>
}
</ul>
</div>
</div>
<div class="col-md-9">
<div id="editor" style="width: 100%; height: 300px;"></div>
<div class="block">
<input type="button" value="Run query" id="run-query" class="btn btn-success">
</div>
<div id="result"></div>
</div>
</div>
}
}
<script src="@helpers.assets("/vendors/ace/ace.js")" type="text/javascript" charset="utf-8"></script>
<script src="@helpers.assets("/vendors/vakata-jstree-3.3.4/jstree.min.js")" type="text/javascript" charset="utf-8"></script>
<link rel="stylesheet" href="@helpers.assets("/vendors/vakata-jstree-3.3.4/themes/default/style.min.css")" />
<script>
$(function(){
$('#editor').text($('#initial').val());
var editor = ace.edit("editor");
editor.setTheme("ace/theme/monokai");
editor.getSession().setMode("ace/mode/sql");
$('#table-tree').jstree();
$('.table-link').click(function(e){
var query = editor.getValue();
if(query != ''){
query = query + '\n';
}
console.log(e);
editor.setValue(query + 'SELECT * FROM ' + $(e.target).text());
});
$('#run-query').click(function(){
console.log(editor.getValue());
$.post('@context.path/admin/dbviewer/_query', { query: editor.getValue() }, function(data){
if(data.type == "query"){
var table = $('<table class="table table-bordered table-hover table-scroll">');
var header = $('<tr>');
$.each(data.columns, function(i, column){
header.append($('<th>').text(column));
});
table.append($('<thead>').append(header));
var body = $('<tbody>');
$.each(data.rows, function(i, rs){
var row = $('<tr>');
$.each(data.columns, function(i, column){
row.append($('<td>').text(rs[column]));
});
body.append(row);
});
table.append(body);
$('#result').empty().append(table);
} else if(data.type == "update"){
$('#result').empty().append($('<span>').text('Updated ' + data.rows + ' rows.'));
} else if(data.type == "error"){
$('#result').empty().append($('<span class="error">').text(data.message));
}
});
});
});
</script>

View File

@@ -2,25 +2,42 @@
<div class="main-sidebar">
<div class="sidebar">
<ul class="sidebar-menu" id="system-admin-menu-container">
<li@if(active=="users"){ class="active"}>
<a href="@context.path/admin/users">User management</a>
<li class="menu-item-hover @if(active=="users"){active}">
<a href="@context.path/admin/users">
<i class="menu-icon octicon octicon-person"></i>
<span>User management</span>
</a>
</li>
<li@if(active=="system"){ class="active"}>
<a href="@context.path/admin/system">System settings</a>
<li class="menu-item-hover @if(active=="system"){active}">
<a href="@context.path/admin/system">
<i class="menu-icon octicon octicon-gear"></i>
<span>System settings</span></a>
</li>
<li@if(active=="plugins"){ class="active"}>
<a href="@context.path/admin/plugins">Plugins</a>
<li class="menu-item-hover @if(active=="plugins"){active}">
<a href="@context.path/admin/plugins">
<i class="menu-icon octicon octicon-plug"></i>
<span>Plugins</span>
</a>
</li>
<li@if(active=="data"){ class="active"}>
<a href="@context.path/admin/data">Data export / import</a>
<li class="menu-item-hover @if(active=="data"){active}">
<a href="@context.path/admin/data">
<i class="menu-icon octicon octicon-database"></i>
<span>Data export / import</span>
</a>
</li>
<li>
<a href="@context.path/console/login.jsp" target="_blank">H2 console</a>
<li class="menu-item-hover @if(active=="dbviewer"){active}">
<a href="@context.path/admin/dbviewer">
<i class="menu-icon octicon octicon-database"></i>
<span>Database viewer</span>
</a>
</li>
@gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu =>
@menu(context).map { link =>
<li@if(active==link.id){ class="active"}>
<a href="@context.path/@link.path">@link.label</a>
<a href="@context.path/@link.path">
<i class="menu-icon octicon octicon-@link.icon.getOrElse("plug")"></i>
<span>@link.label</span>
</a>
</li>
}
}

View File

@@ -1,18 +1,32 @@
@(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context)
@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean)], info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Plugins"){
@gitbucket.core.admin.html.menu("plugins") {
<h1>Installed plugins</h1>
@gitbucket.core.helper.html.information(info)
<form action="@context.path/admin/plugins/_reload" method="POST" class="pull-right">
<input type="submit" value="Reload plugins" class="btn btn-default">
</form>
<h1>Plugins</h1>
@if(plugins.size > 0) {
<ul>
@plugins.map { plugin =>
@plugins.map { case (plugin, enabled) =>
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
}
</ul>
@plugins.map { plugin =>
@plugins.map { case (plugin, enabled) =>
<div class="panel panel-default">
<div class="panel-heading strong" id="@plugin.pluginId">@plugin.pluginName</div>
<div class="panel-heading strong" id="@plugin.pluginId">
@if(enabled){
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_uninstall" method="POST" class="pull-right uninstall-form">
<input type="submit" value="Uninstall" class="btn btn-danger btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
</form>
} else {
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_install" method="POST" class="pull-right install-form">
<input type="submit" value="Install" class="btn btn-success btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
</form>
}
@plugin.pluginName
</div>
<div class="panel-body">
<div class="row">
<label class="col-md-2">Id</label>
@@ -38,3 +52,16 @@
}
}
}
<script>
$(function(){
$('.uninstall-form').click(function(e){
var name = $(e.target).data('name');
return confirm('Uninstall ' + name + '. Are you sure?');
});
$('.install-form').click(function(e){
var name = $(e.target).data('name');
return confirm('Install ' + name + '. Are you sure?');
});
});
</script>

View File

@@ -1,5 +1,6 @@
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.util.DatabaseConfig
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("System settings"){
@gitbucket.core.admin.html.menu("system"){
@gitbucket.core.helper.html.information(info)
@@ -59,6 +60,44 @@
<textarea name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea>
</fieldset>
<!--====================================================================-->
<!-- AdminLTE SkinName -->
<!--====================================================================-->
<hr>
<label class="strong">
AdminLTE skin name
</label>
<div class="form-group">
<label class="control-label col-md-2" for="skinName">Skin name</label>
<div class="col-md-10">
<select id="skinName" name="skinName" class="form-control">
<optgroup label="Dark">
@Seq(
("skin-black", "Black"),
("skin-blue", "Blue"),
("skin-green", "Green"),
("skin-purple", "Purple"),
("skin-red", "Red"),
("skin-yellow", "Yellow"),
).map{ skin =>
<option value="@skin._1"@if(skin._1 == context.settings.skinName){ selected=""}>@skin._2</option>
}
</optgroup>
<optgroup label="Light">
@Seq(
("skin-black-light", "Light black"),
("skin-blue-light", "Light blue"),
("skin-green-light", "Light green"),
("skin-purple-light", "Light purple"),
("skin-red-light", "Light red"),
("skin-yellow-light", "Light yellow"),
).map{ skin =>
<option value="@skin._1"@if(skin._1 == context.settings.skinName){ selected=""} >@skin._2</option>
}
</optgroup>
</select>
</div>
</div>
<!--====================================================================-->
<!-- Account registration -->
<!--====================================================================-->
<hr>
@@ -107,8 +146,8 @@
<label><span class="strong">Limit of activity logs</span> (Unlimited if it is not specified or zero)</label>
<fieldset>
<div class="form-group">
<label class="control-label col-md-3" for="activityLogLimit">Limit</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="activityLogLimit">Limit</label>
<div class="col-md-10">
<input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@context.settings.activityLogLimit"/>
<span id="error-activityLogLimit" class="error"></span>
</div>
@@ -139,15 +178,15 @@
</fieldset>
<div class="ssh">
<div class="form-group">
<label class="control-label col-md-3" for="sshHost">SSH host</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="sshHost">SSH host</label>
<div class="col-md-10">
<input type="text" id="sshHost" name="sshHost" class="form-control" value="@context.settings.sshHost"/>
<span id="error-sshHost" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="sshPort">SSH port</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="sshPort">SSH port</label>
<div class="col-md-10">
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@context.settings.sshPort"/>
<span id="error-sshPort" class="error"></span>
</div>
@@ -166,83 +205,83 @@
</fieldset>
<div class="ldap">
<div class="form-group">
<label class="control-label col-md-3" for="ldapHost">LDAP host</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapHost">LDAP host</label>
<div class="col-md-10">
<input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@context.settings.ldap.map(_.host)"/>
<span id="error-ldap_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapPort">LDAP port</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapPort">LDAP port</label>
<div class="col-md-10">
<input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@context.settings.ldap.map(_.port)"/>
<span id="error-ldap_port" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapBindDN">Bind DN</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapBindDN">Bind DN</label>
<div class="col-md-10">
<input type="text" id="ldapBindDN" name="ldap.bindDN" class="form-control" value="@context.settings.ldap.map(_.bindDN)"/>
<span id="error-ldap_bindDN" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapBindPassword">Bind password</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapBindPassword">Bind password</label>
<div class="col-md-10">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@context.settings.ldap.map(_.bindPassword)"/>
<span id="error-ldap_bindPassword" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapBaseDN">Base DN</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapBaseDN">Base DN</label>
<div class="col-md-10">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" class="form-control" value="@context.settings.ldap.map(_.baseDN)"/>
<span id="error-ldap_baseDN" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapUserNameAttribute">User name attribute</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapUserNameAttribute">User name attribute</label>
<div class="col-md-10">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" class="form-control" value="@context.settings.ldap.map(_.userNameAttribute)"/>
<span id="error-ldap_userNameAttribute" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapAdditionalFilterCondition">Additional filter condition</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapAdditionalFilterCondition">Additional filter condition</label>
<div class="col-md-10">
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" class="form-control" value="@context.settings.ldap.map(_.additionalFilterCondition)"/>
<span id="error-ldap_additionalFilterCondition" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapFullNameAttribute">Full name attribute</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapFullNameAttribute">Full name attribute</label>
<div class="col-md-10">
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" class="form-control" value="@context.settings.ldap.map(_.fullNameAttribute)"/>
<span id="error-ldap_fullNameAttribute" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapMailAttribute">Mail address attribute</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapMailAttribute">Mail address attribute</label>
<div class="col-md-10">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" class="form-control" value="@context.settings.ldap.map(_.mailAttribute)"/>
<span id="error-ldap_mailAttribute" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3">Enable TLS</label>
<div class="col-md-9">
<label class="control-label col-md-2">Enable TLS</label>
<div class="col-md-10">
<input type="checkbox" name="ldap.tls"@if(context.settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3">Enable SSL</label>
<div class="col-md-9">
<label class="control-label col-md-2">Enable SSL</label>
<div class="col-md-10">
<input type="checkbox" name="ldap.ssl"@if(context.settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="ldapBindDN">Keystore</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="ldapBindDN">Keystore</label>
<div class="col-md-10">
<input type="text" id="ldapKeystore" name="ldap.keystore" class="form-control" value="@context.settings.ldap.map(_.keystore)"/>
<span id="error-ldap_keystore" class="error"></span>
</div>
@@ -273,52 +312,52 @@
</fieldset>
<div class="useSMTP">
<div class="form-group">
<label class="control-label col-md-3" for="smtpHost">SMTP host</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="smtpHost">SMTP host</label>
<div class="col-md-10">
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="smtpPort">SMTP port</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="smtpPort">SMTP port</label>
<div class="col-md-10">
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="smtpUser">SMTP user</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="smtpUser">SMTP user</label>
<div class="col-md-10">
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="smtpPassword">SMTP password</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="smtpPassword">SMTP password</label>
<div class="col-md-10">
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="smtpSsl">Enable SSL</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="smtpSsl">Enable SSL</label>
<div class="col-md-10">
<input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="smtpStarttls">Enable STARTTLS</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="smtpStarttls">Enable STARTTLS</label>
<div class="col-md-10">
<input type="checkbox" id="smtpStarttls" name="smtp.starttls"@if(context.settings.smtp.flatMap(_.starttls).getOrElse(false)){ checked}/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="fromAddress">FROM address</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="fromAddress">FROM address</label>
<div class="col-md-10">
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="fromName">FROM name</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="fromName">FROM name</label>
<div class="col-md-10">
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@context.settings.smtp.map(_.fromName)"/>
</div>
</div>
@@ -328,17 +367,17 @@
<input type="button" id="sendTestMail" value="Send"/>
</div>
</div>
@*
<!--====================================================================-->
<!-- GitLFS -->
<!--====================================================================-->
@*
<hr>
<label class="strong">
GitLFS <span class="muted normal">(Enter the LFS server url to enable GitLFS support)</span>
</label>
<div class="form-group">
<label class="control-label col-md-3" for="smtpHost">LFS server url</label>
<div class="col-md-9">
<label class="control-label col-md-2" for="smtpHost">LFS server url</label>
<div class="col-md-10">
<input type="text" id="lfsServerUrl" name="lfs.serverUrl" class="form-control" value="@context.settings.lfs.serverUrl"/>
<span id="error-lfs_serverUrl" class="error"></span>
</div>
@@ -354,6 +393,14 @@
}
<script>
$(function(){
$('#skinName').change(function(evt) {
var that = $(evt.target);
var themeCss = $('link[rel="stylesheet"][href*="skin-"]');
var oldVal = new RegExp('(skin-.*?).min.css').exec(themeCss.attr('href'))[1];
themeCss.attr('href', themeCss.attr('href').replace(oldVal, that.val()));
$(document.body).removeClass(oldVal).addClass(that.val());
});
$('#sendTestMail').click(function(){
var host = $('#smtpHost' ).val();
var port = $('#smtpPort' ).val();

View File

@@ -1,134 +1,19 @@
@(account: Option[gitbucket.core.model.Account], members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(if(account.isEmpty) "New group" else "Update group"){
@gitbucket.core.admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@context.path/admin/users/_newgroup} else {@context.path/admin/users/@account.get.userName/_editgroup}" validate="true">
<div class="row">
<div class="col-md-6">
<fieldset class="form-group">
<label for="groupName" class="strong">Group name</label>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
@if(account.isDefined){
<label for="removed">
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
Disable
</label>
}
</fieldset>
<fieldset class="form-group">
<label class="strong">URL (Optional)</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
</fieldset>
<fieldset class="form-group">
<label class="strong">Description (Optional)</label>
<textarea name="description" id="description" class="form-control">@account.map(_.description)</textarea>
</fieldset>
<fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label>
@gitbucket.core.helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="col-md-6">
<fieldset class="form-group">
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("memberName", 200, true, false)
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
</div>
</fieldset>
</div>
</div>
<form id="form" method="post" action="@context.path/admin/users/@(account.map(x => s"${x.userName}/_editgroup").getOrElse("_newgroup"))" validate="true">
@gitbucket.core.account.html.groupform(account, members, true
)
<fieldset class="border-top">
@if(account.isDefined){
<div class="pull-right">
<a href="@helpers.url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete group</a>
</div>
}
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create group} else {Update group}"/>
<a href="@context.path/admin/users" class="btn btn-default">Cancel</a>
</fieldset>
</form>
}
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-members').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#member-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-members').text('User has been already added.');
return false;
}
// check existence
$.post('@context.path/_user/existence', { 'userName': userName },
function(data, status){
if(data == 'user'){
addMemberHTML(userName, false);
} else {
$('#error-members').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
$(this).parent().remove();
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
return !(e.keyCode == 13);
});
@members.map { member =>
addMemberHTML('@member.userName', @member.isManager);
}
function addMemberHTML(userName, isManager){
var memberButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="false" name="' + userName + '">Member</label>');
if(!isManager){
memberButton.addClass('active');
}
var managerButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="true" name="' + userName + '">Manager</label>');
if(isManager){
managerButton.addClass('active');
}
$('#member-list').append($('<li>')
.data('name', userName)
.append($('<div class="btn-group is_manager" data-toggle="buttons">')
.append(memberButton)
.append(managerButton))
.append(' ')
.append($('<a>').attr('href', '@context.path/' + userName).text(userName))
.append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateMembers(){
var members = $('#member-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
}).get().join(',');
$('#members').val(members);
}
});
</script>

Some files were not shown because too many files have changed in this diff Show More