mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-14 01:15:50 +01:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
26
src/main/resources/update/gitbucket-core_4.14.sql
Normal file
26
src/main/resources/update/gitbucket-core_4.14.sql
Normal 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);
|
||||
38
src/main/resources/update/gitbucket-core_4.14.xml
Normal file
38
src/main/resources/update/gitbucket-core_4.14.xml
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
124
src/main/scala/gitbucket/core/api/ApiCommits.scala
Normal file
124
src/main/scala/gitbucket/core/api/ApiCommits.scala
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
))
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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(){
|
||||
|
||||
25
src/main/scala/gitbucket/core/model/AccountWebHook.scala
Normal file
25
src/main/scala/gitbucket/core/model/AccountWebHook.scala
Normal 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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
43
src/main/scala/gitbucket/core/model/Priorities.scala
Normal file
43
src/main/scala/gitbucket/core/model/Priorities.scala
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
27
src/main/scala/gitbucket/core/model/RepositoryWebHook.scala
Normal file
27
src/main/scala/gitbucket/core/model/RepositoryWebHook.scala
Normal 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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
10
src/main/scala/gitbucket/core/plugin/AccountHook.scala
Normal file
10
src/main/scala/gitbucket/core/plugin/AccountHook.scala
Normal 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 = ()
|
||||
|
||||
}
|
||||
22
src/main/scala/gitbucket/core/plugin/IssueHook.scala
Normal file
22
src/main/scala/gitbucket/core/plugin/IssueHook.scala
Normal 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 = ()
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
)
|
||||
425
src/main/scala/gitbucket/core/plugin/PluginRegistry.scala
Normal file
425
src/main/scala/gitbucket/core/plugin/PluginRegistry.scala
Normal 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")
|
||||
}
|
||||
|
||||
}
|
||||
43
src/main/scala/gitbucket/core/plugin/PluginRepository.scala
Normal file
43
src/main/scala/gitbucket/core/plugin/PluginRepository.scala
Normal 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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; });"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 -> _ )
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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]]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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)
|
||||
//}
|
||||
@@ -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() = {}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
68
src/main/scala/gitbucket/core/util/Mailer.scala
Normal file
68
src/main/scala/gitbucket/core/util/Mailer.scala
Normal 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 = ()
|
||||
//}
|
||||
@@ -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 = {}
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
14
src/main/twirl/gitbucket/core/account/creategroup.scala.html
Normal file
14
src/main/twirl/gitbucket/core/account/creategroup.scala.html
Normal 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>
|
||||
}
|
||||
@@ -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(){
|
||||
|
||||
19
src/main/twirl/gitbucket/core/account/editgroup.scala.html
Normal file
19
src/main/twirl/gitbucket/core/account/editgroup.scala.html
Normal 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>
|
||||
}
|
||||
}
|
||||
16
src/main/twirl/gitbucket/core/account/edithook.scala.html
Normal file
16
src/main/twirl/gitbucket/core/account/edithook.scala.html
Normal 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
132
src/main/twirl/gitbucket/core/account/groupform.scala.html
Normal file
132
src/main/twirl/gitbucket/core/account/groupform.scala.html
Normal 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>
|
||||
42
src/main/twirl/gitbucket/core/account/hooks.scala.html
Normal file
42
src/main/twirl/gitbucket/core/account/hooks.scala.html
Normal 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, we’ll 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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 you’re 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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
89
src/main/twirl/gitbucket/core/admin/dbviewer.scala.html
Normal file
89
src/main/twirl/gitbucket/core/admin/dbviewer.scala.html
Normal 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>
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user