mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-07-08 15:43:28 +02:00
Merge branch 'master' into pr-show-commitstaus-in-commits
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>
|
||||
@@ -31,9 +31,8 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
|
||||
// Register controllers
|
||||
context.mount(new AnonymousAccessController, "/*")
|
||||
|
||||
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")
|
||||
@@ -44,6 +43,7 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
|
||||
context.mount(new RepositoryViewerController, "/*")
|
||||
context.mount(new WikiController, "/*")
|
||||
context.mount(new LabelsController, "/*")
|
||||
context.mount(new PrioritiesController, "/*")
|
||||
context.mount(new MilestonesController, "/*")
|
||||
context.mount(new IssuesController, "/*")
|
||||
context.mount(new PullRequestsController, "/*")
|
||||
|
||||
@@ -31,5 +31,14 @@ object GitBucketCoreModule extends Module("gitbucket-core",
|
||||
new Version("4.10.0"),
|
||||
new Version("4.11.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.11.xml")
|
||||
)
|
||||
),
|
||||
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")
|
||||
)
|
||||
|
||||
@@ -37,7 +37,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,
|
||||
@@ -53,4 +53,14 @@ object ApiRepository{
|
||||
def forPushPayload(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)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ 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, RepositoryWebHookEvent, 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._
|
||||
@@ -16,17 +17,16 @@ import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.scalatra.BadRequest
|
||||
|
||||
|
||||
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 +40,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 +49,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()))),
|
||||
@@ -109,6 +109,47 @@ 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, 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){
|
||||
Seq(name -> messages("error.required").format(name))
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays user information.
|
||||
*/
|
||||
@@ -191,6 +232,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
} getOrElse NotFound()
|
||||
})
|
||||
|
||||
get("/captures/(.*)".r) {
|
||||
multiParams("captures").head
|
||||
}
|
||||
|
||||
get("/:userName/_delete")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
|
||||
@@ -206,9 +251,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 +318,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 +444,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 +460,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/:groupName/_editgroup")(managersOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
// TODO Don't use Option.get
|
||||
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()
|
||||
}
|
||||
@@ -433,16 +598,23 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(accountName, repository.name)
|
||||
// Insert default priorities
|
||||
insertDefaultPriorities(accountName, repository.name)
|
||||
|
||||
// clone repository actually
|
||||
JGitUtil.cloneRepository(
|
||||
getRepositoryDir(repository.owner, repository.name),
|
||||
getRepositoryDir(accountName, repository.name))
|
||||
FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name)))
|
||||
|
||||
// Create Wiki repository
|
||||
JGitUtil.cloneRepository(
|
||||
getWikiRepositoryDir(repository.owner, repository.name),
|
||||
getWikiRepositoryDir(accountName, repository.name))
|
||||
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)
|
||||
|
||||
@@ -33,6 +33,7 @@ class ApiController extends ApiControllerBase
|
||||
with WebHookIssueCommentService
|
||||
with WikiService
|
||||
with ActivityService
|
||||
with PrioritiesService
|
||||
with OwnerAuthenticator
|
||||
with UsersAuthenticator
|
||||
with GroupManagerAuthenticator
|
||||
@@ -52,6 +53,7 @@ trait ApiControllerBase extends ControllerBase {
|
||||
with RepositoryCreationService
|
||||
with IssueCreationService
|
||||
with HandleCommentService
|
||||
with PrioritiesService
|
||||
with OwnerAuthenticator
|
||||
with UsersAuthenticator
|
||||
with GroupManagerAuthenticator
|
||||
@@ -365,6 +367,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 +381,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()
|
||||
|
||||
@@ -147,6 +147,13 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -156,7 +163,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
override def validate(name: String, value: String, params: Map[String, 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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,12 @@ import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
*/
|
||||
class FileUploadController extends ScalatraServlet with FileUploadSupport with RepositoryService with AccountService {
|
||||
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||
val maxFileSize = if (System.getProperty("gitbucket.maxFileSize") != null)
|
||||
System.getProperty("gitbucket.maxFileSize").toLong
|
||||
else
|
||||
3 * 1024 * 1024
|
||||
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(maxFileSize)))
|
||||
|
||||
post("/image"){
|
||||
execute({ (file, fileId) =>
|
||||
@@ -31,6 +36,13 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
|
||||
}, 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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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 io.github.gitbucket.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, String], messages: Messages): Option[String] = {
|
||||
val owner = params("owner")
|
||||
val repository = params("repository")
|
||||
params.get("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
|
||||
@@ -23,14 +24,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,6 +45,7 @@ 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)
|
||||
|
||||
@@ -63,6 +65,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
commitIdTo: String,
|
||||
assignedUserName: Option[String],
|
||||
milestoneId: Option[Int],
|
||||
priorityId: Option[Int],
|
||||
labelNames: Option[String]
|
||||
)
|
||||
|
||||
@@ -92,12 +95,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))
|
||||
}
|
||||
}
|
||||
@@ -138,22 +144,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 +237,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
|
||||
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
@@ -261,10 +281,8 @@ 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(_.merged(issue, repository))
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
@@ -359,10 +377,10 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
title,
|
||||
commits,
|
||||
diffs,
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
((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)
|
||||
},
|
||||
}).filter { case (owner, name) => hasGuestRole(owner, name, context.loginAccount) },
|
||||
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||
originId,
|
||||
forkedId,
|
||||
@@ -375,6 +393,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)
|
||||
)
|
||||
}
|
||||
@@ -430,6 +449,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,10 +488,8 @@ 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}")
|
||||
@@ -505,6 +523,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,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.settings.html
|
||||
import gitbucket.core.model.WebHook
|
||||
import gitbucket.core.model.{WebHook, RepositoryWebHook}
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.util._
|
||||
@@ -40,7 +40,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,21 +133,12 @@ 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))
|
||||
FileUtils.moveDirectory(dir, getRepositoryFilesDir(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))
|
||||
}
|
||||
}
|
||||
// Delete parent directory
|
||||
FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name))
|
||||
|
||||
// Call hooks
|
||||
PluginRegistry().getRepositoryHooks.foreach(_.renamed(repository.owner, repository.name, form.repositoryName))
|
||||
}
|
||||
@@ -159,7 +150,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) =>
|
||||
@@ -221,8 +212,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)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -260,7 +251,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
|
||||
@@ -297,7 +288,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())
|
||||
@@ -311,7 +302,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()
|
||||
})
|
||||
|
||||
@@ -364,7 +355,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
FileUtils.moveDirectory(dir, getAttachedDir(form.newOwner, repository.name))
|
||||
}
|
||||
}
|
||||
// Delere parent directory
|
||||
// Delete parent directory
|
||||
FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name))
|
||||
|
||||
// Call hooks
|
||||
@@ -402,7 +393,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()
|
||||
}
|
||||
}
|
||||
flash += "info" -> "Garbage collection has been executed."
|
||||
|
||||
@@ -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
|
||||
@@ -18,15 +18,14 @@ 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.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.scalatra._
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
|
||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
@@ -45,6 +44,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 +59,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 +79,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 +94,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(
|
||||
@@ -183,11 +200,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)
|
||||
@@ -197,9 +253,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()
|
||||
}
|
||||
})
|
||||
@@ -211,8 +273,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()
|
||||
}
|
||||
})
|
||||
@@ -226,7 +294,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}/${
|
||||
@@ -243,21 +312,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)}"
|
||||
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}")
|
||||
})
|
||||
@@ -286,12 +365,16 @@ 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()
|
||||
}
|
||||
@@ -558,6 +641,116 @@ 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.userName, 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.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 val readmeFiles = PluginRegistry().renderableExtensions.map { extension =>
|
||||
s"readme.${extension}"
|
||||
} ++ Seq("readme.txt", "readme")
|
||||
@@ -608,77 +801,6 @@ 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)
|
||||
|
||||
@@ -705,6 +827,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,7 +6,7 @@ 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._
|
||||
@@ -15,6 +15,10 @@ import gitbucket.core.util.StringUtil._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.scalatra.i18n.Messages
|
||||
import com.github.zafarkhaja.semver.{Version => Semver}
|
||||
import gitbucket.core.GitBucketCoreModule
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
@@ -106,7 +110,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 +121,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())),
|
||||
@@ -181,7 +185,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 +293,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 +307,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 +349,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)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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._
|
||||
@@ -13,11 +15,12 @@ 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)
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 = ()
|
||||
|
||||
}
|
||||
20
src/main/scala/gitbucket/core/plugin/IssueHook.scala
Normal file
20
src/main/scala/gitbucket/core/plugin/IssueHook.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.Issue
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
|
||||
trait IssueHook {
|
||||
|
||||
def created(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
|
||||
def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
|
||||
def closed(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
|
||||
def reopened(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
|
||||
|
||||
}
|
||||
|
||||
trait PullRequestHook extends IssueHook {
|
||||
|
||||
def merged(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
|
||||
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
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 play.twirl.api.Html
|
||||
|
||||
/**
|
||||
* Trait for define plugin interface.
|
||||
@@ -69,6 +71,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 +101,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 +201,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.
|
||||
*/
|
||||
@@ -209,12 +261,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 +300,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))
|
||||
}
|
||||
@@ -248,11 +315,17 @@ abstract class Plugin {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -2,13 +2,18 @@ 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
|
||||
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
|
||||
@@ -16,46 +21,53 @@ 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.slf4j.LoggerFactory
|
||||
import play.twirl.api.Html
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
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 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 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 repositoryHooks = new ConcurrentLinkedQueue[RepositoryHook]
|
||||
private val issueHooks = new ConcurrentLinkedQueue[IssueHook]
|
||||
|
||||
private val suggestionProviders = new ListBuffer[SuggestionProvider]
|
||||
suggestionProviders += new UserNameSuggestionProvider()
|
||||
private val pullRequestHooks = new ConcurrentLinkedQueue[PullRequestHook]
|
||||
|
||||
def addPlugin(pluginInfo: PluginInfo): Unit = plugins += pluginInfo
|
||||
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]
|
||||
|
||||
def getPlugins(): List[PluginInfo] = plugins.toList
|
||||
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
|
||||
suggestionProviders.add(new UserNameSuggestionProvider())
|
||||
|
||||
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 += ((id, encoded))
|
||||
images.put(id, encoded)
|
||||
}
|
||||
|
||||
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
|
||||
@@ -68,28 +80,28 @@ class PluginRegistry {
|
||||
addImage(id, bytes)
|
||||
}
|
||||
|
||||
def getImage(id: String): String = images(id)
|
||||
def getImage(id: String): String = images.get(id)
|
||||
|
||||
def addController(path: String, controller: ControllerBase): Unit = controllers += ((controller, path))
|
||||
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.toSeq
|
||||
def getControllers(): Seq[(ControllerBase, String)] = controllers.asScala.toSeq
|
||||
|
||||
def addJavaScript(path: String, script: String): Unit = javaScripts += ((path, script))
|
||||
def addJavaScript(path: String, script: String): Unit = javaScripts.add((path, script)) //javaScripts += ((path, script))
|
||||
|
||||
def getJavaScript(currentPath: String): List[String] = javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
|
||||
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 += ((extension, renderer))
|
||||
def addRenderer(extension: String, renderer: Renderer): Unit = renderers.put(extension, renderer)
|
||||
|
||||
def getRenderer(extension: String): Renderer = renderers.getOrElse(extension, DefaultRenderer)
|
||||
def getRenderer(extension: String): Renderer = renderers.asScala.getOrElse(extension, DefaultRenderer)
|
||||
|
||||
def renderableExtensions: Seq[String] = renderers.keys.toSeq
|
||||
def renderableExtensions: Seq[String] = renderers.keys.asScala.toSeq
|
||||
|
||||
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings += routing
|
||||
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings.add(routing)
|
||||
|
||||
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.toSeq
|
||||
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.asScala.toSeq
|
||||
|
||||
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
|
||||
PluginRegistry().getRepositoryRoutings().find {
|
||||
@@ -99,53 +111,73 @@ class PluginRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks += commitHook
|
||||
def addAccountHook(accountHook: AccountHook): Unit = accountHooks.add(accountHook)
|
||||
|
||||
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
|
||||
def getAccountHooks: Seq[AccountHook] = accountHooks.asScala.toSeq
|
||||
|
||||
def addRepositoryHook(repositoryHook: RepositoryHook): Unit = repositoryHooks += repositoryHook
|
||||
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks.add(commitHook)
|
||||
|
||||
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.toSeq
|
||||
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.asScala.toSeq
|
||||
|
||||
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus += globalMenu
|
||||
def addRepositoryHook(repositoryHook: RepositoryHook): Unit = repositoryHooks.add(repositoryHook)
|
||||
|
||||
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
|
||||
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.asScala.toSeq
|
||||
|
||||
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus += repositoryMenu
|
||||
def addIssueHook(issueHook: IssueHook): Unit = issueHooks.add(issueHook)
|
||||
|
||||
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq
|
||||
def getIssueHooks: Seq[IssueHook] = issueHooks.asScala.toSeq
|
||||
|
||||
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs += repositorySettingTab
|
||||
def addPullRequestHook(pullRequestHook: PullRequestHook): Unit = pullRequestHooks.add(pullRequestHook)
|
||||
|
||||
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq
|
||||
def getPullRequestHooks: Seq[PullRequestHook] = pullRequestHooks.asScala.toSeq
|
||||
|
||||
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs += profileTab
|
||||
def addRepositoryHeader(repositoryHeader: (RepositoryInfo, Context) => Option[Html]): Unit = repositoryHeaders.add(repositoryHeader)
|
||||
|
||||
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq
|
||||
def getRepositoryHeaders: Seq[(RepositoryInfo, Context) => Option[Html]] = repositoryHeaders.asScala.toSeq
|
||||
|
||||
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus += systemSettingMenu
|
||||
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus.add(globalMenu)
|
||||
|
||||
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq
|
||||
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.asScala.toSeq
|
||||
|
||||
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus += accountSettingMenu
|
||||
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus.add(repositoryMenu)
|
||||
|
||||
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq
|
||||
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.asScala.toSeq
|
||||
|
||||
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = dashboardTabs += dashboardTab
|
||||
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs.add(repositorySettingTab)
|
||||
|
||||
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
|
||||
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.asScala.toSeq
|
||||
|
||||
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings += assetsMapping
|
||||
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs.add(profileTab)
|
||||
|
||||
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.toSeq
|
||||
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.asScala.toSeq
|
||||
|
||||
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators += textDecorator
|
||||
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus.add(systemSettingMenu)
|
||||
|
||||
def getTextDecorators: Seq[TextDecorator] = textDecorators.toSeq
|
||||
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.asScala.toSeq
|
||||
|
||||
def addSuggestionProvider(suggestionProvider: SuggestionProvider): Unit = suggestionProviders += suggestionProvider
|
||||
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus.add(accountSettingMenu)
|
||||
|
||||
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.toSeq
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,79 +187,233 @@ object PluginRegistry {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
|
||||
|
||||
private val instance = new 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 = {
|
||||
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
|
||||
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]
|
||||
// Clean installed directory
|
||||
val installedDir = new File(PluginHome, ".installed")
|
||||
if(installedDir.exists){
|
||||
FileUtils.deleteDirectory(installedDir)
|
||||
}
|
||||
installedDir.mkdir()
|
||||
|
||||
// Migration
|
||||
val solidbase = new Solidbase()
|
||||
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
|
||||
val pluginJars = listPluginJars(pluginDir)
|
||||
val extraJars = extraPluginDir.map { extraDir => listPluginJars(new File(extraDir)) }.getOrElse(Nil)
|
||||
|
||||
// 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}")
|
||||
(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: _*))
|
||||
|
||||
// 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
|
||||
))
|
||||
// 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}")
|
||||
}
|
||||
|
||||
} catch {
|
||||
case e: Throwable => {
|
||||
logger.error(s"Error during plugin initialization: ${pluginJar.getAbsolutePath}", e)
|
||||
// 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 = {
|
||||
instance.getPlugins().foreach { pluginInfo =>
|
||||
def shutdown(context: ServletContext, settings: SystemSettings): Unit = synchronized {
|
||||
instance.getPlugins().foreach { plugin =>
|
||||
try {
|
||||
pluginInfo.pluginClass.shutdown(instance, context, settings)
|
||||
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", e)
|
||||
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)
|
||||
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(
|
||||
pluginId: String,
|
||||
pluginName: String,
|
||||
pluginVersion: String,
|
||||
description: String,
|
||||
pluginClass: Plugin
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
gitbucket.core.servlet.Database() withTransaction { session =>
|
||||
logger.info("Reloading plugins...")
|
||||
PluginRegistry.reload(context, loadSystemSettings(), session.conn)
|
||||
logger.info("Reloading finished.")
|
||||
}
|
||||
}
|
||||
detectedWatchKey.reset()
|
||||
}
|
||||
} catch {
|
||||
case _: InterruptedException => watchKey.cancel()
|
||||
}
|
||||
|
||||
logger.info("Shutdown PluginWatchThread")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
41
src/main/scala/gitbucket/core/plugin/PluginRepository.scala
Normal file
41
src/main/scala/gitbucket/core/plugin/PluginRepository.scala
Normal file
@@ -0,0 +1,41 @@
|
||||
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,
|
||||
file: String,
|
||||
range: String
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ trait ActivityService {
|
||||
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||
"open_issue",
|
||||
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(title),
|
||||
Some(title),
|
||||
currentDate)
|
||||
|
||||
def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
|
||||
@@ -132,10 +132,10 @@ 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,
|
||||
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
|
||||
tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit =
|
||||
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||
"create_tag",
|
||||
@@ -167,7 +167,7 @@ trait ActivityService {
|
||||
None,
|
||||
currentDate)
|
||||
|
||||
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit =
|
||||
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit =
|
||||
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||
"fork",
|
||||
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
|
||||
|
||||
@@ -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
|
||||
@@ -204,6 +233,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 +252,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 +261,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 +292,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 +303,7 @@ trait IssuesService {
|
||||
id,
|
||||
loginUser,
|
||||
milestoneId,
|
||||
priorityId,
|
||||
assignedUserName,
|
||||
title,
|
||||
content,
|
||||
@@ -290,6 +330,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 +346,33 @@ 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)).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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,6 +477,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 +503,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 +518,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 +534,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 +570,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 +581,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 +597,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])
|
||||
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ object MergeService{
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
|
||||
val committer = mergeTipCommit.getCommitterIdent;
|
||||
val committer = mergeTipCommit.getCommitterIdent
|
||||
def updateBranch(treeId:ObjectId, message:String, branchName:String){
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(treeId, committer, message)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,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))
|
||||
|
||||
@@ -94,9 +94,9 @@ trait PullRequestService { self: IssuesService with CommitsService =>
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@@ -256,7 +256,7 @@ object PullRequestService {
|
||||
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 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) = {
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.eclipse.jgit.dircache.DirCache
|
||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||
|
||||
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) {
|
||||
@@ -30,6 +30,9 @@ trait RepositoryCreationService {
|
||||
// Insert default labels
|
||||
insertDefaultLabels(owner, name)
|
||||
|
||||
// Insert default priorities
|
||||
insertDefaultPriorities(owner, name)
|
||||
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(owner, name)
|
||||
JGitUtil.initRepository(gitdir)
|
||||
@@ -74,5 +77,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
|
||||
@@ -81,7 +82,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.
|
||||
@@ -92,17 +93,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)) :_*)
|
||||
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
|
||||
}
|
||||
)} :_*)
|
||||
|
||||
@@ -129,7 +135,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
|
||||
@@ -161,10 +167,11 @@ trait RepositoryService { self: AccountService =>
|
||||
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
|
||||
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||
WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
|
||||
RepositoryWebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||
RepositoryWebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
|
||||
DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -220,23 +220,28 @@ object SystemSettingsService {
|
||||
private val LdapSsl = "ldap.ssl"
|
||||
private val LdapKeystore = "ldap.keystore"
|
||||
|
||||
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,7 +223,7 @@ 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{
|
||||
@@ -207,7 +252,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,14 +262,14 @@ 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)
|
||||
@@ -246,12 +291,13 @@ 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 = {
|
||||
(implicit s: Session, c: JsonFormat.Context): Unit = {
|
||||
import WebHookService._
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){
|
||||
for{
|
||||
@@ -285,7 +331,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())
|
||||
@@ -321,9 +367,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
|
||||
@@ -344,6 +390,17 @@ object WebHookService {
|
||||
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
|
||||
@@ -470,4 +527,53 @@ object WebHookService {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -237,7 +237,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){
|
||||
|
||||
@@ -23,10 +23,11 @@ class GHCompatRepositoryAccessFilter extends Filter with SystemSettingsService {
|
||||
val agent = request.getHeader("USER-AGENT")
|
||||
val response = res.asInstanceOf[HttpServletResponse]
|
||||
val requestPath = request.getRequestURI.substring(request.getContextPath.length)
|
||||
val queryString = if (request.getQueryString != null) "?" + request.getQueryString else ""
|
||||
|
||||
requestPath match {
|
||||
case githubRepositoryPattern() if agent != null && agent.toLowerCase.indexOf("git") >= 0 =>
|
||||
response.sendRedirect(baseUrl + "/git" + requestPath)
|
||||
response.sendRedirect(baseUrl + "/git" + requestPath + queryString)
|
||||
case _ =>
|
||||
chain.doFilter(req, res)
|
||||
}
|
||||
|
||||
@@ -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._
|
||||
|
||||
|
||||
@@ -161,6 +163,12 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
receivePack.setPostReceiveHook(hook)
|
||||
}
|
||||
}
|
||||
|
||||
if(repository.endsWith(".wiki")){
|
||||
defining(request) { implicit r =>
|
||||
receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.stripSuffix(".wiki"), pusher, baseUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +178,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)
|
||||
extends PostReceiveHook with PreReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
|
||||
with WebHookPullRequestService with CommitsService {
|
||||
@@ -185,9 +193,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)
|
||||
@@ -285,8 +294,10 @@ 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())
|
||||
}
|
||||
@@ -309,6 +320,67 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
|
||||
}
|
||||
|
||||
class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: 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)
|
||||
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, commit.id, false)
|
||||
diffs._1.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") =>
|
||||
val action = if(diff.changeType == ChangeType.ADD) "created" else "edited"
|
||||
val fileName = diff.newPath
|
||||
println(action + " - " + fileName + " - " + commit.id)
|
||||
(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,45 @@
|
||||
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 controller = PluginRegistry().getControllers().filter { case (_, path) =>
|
||||
val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI
|
||||
val start = path.replaceFirst("/\\*$", "/")
|
||||
path.endsWith("/*") && (requestUri + "/").startsWith(start)
|
||||
}
|
||||
|
||||
val filterChainWrapper = controller.foldLeft(chain){ case (chain, (controller, _)) =>
|
||||
new FilterChainWrapper(controller, chain)
|
||||
}
|
||||
filterChainWrapper.doFilter(request, response)
|
||||
}
|
||||
|
||||
class FilterChainWrapper(controller: ControllerBase, chain: FilterChain) extends FilterChain {
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse): Unit = {
|
||||
if(controller.config == null){
|
||||
controller.init(filterConfig)
|
||||
}
|
||||
controller.doFilter(request, response, chain)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -90,4 +90,4 @@ object Directory {
|
||||
def getWikiRepositoryDir(owner: String, repository: String): File =
|
||||
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +68,24 @@ object FileUtil {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -14,7 +14,7 @@ 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
|
||||
@@ -93,7 +93,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
|
||||
@@ -226,9 +226,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 {
|
||||
@@ -288,7 +293,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
|
||||
@@ -359,9 +364,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 +374,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
|
||||
}
|
||||
@@ -994,13 +999,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 +1015,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)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import gitbucket.core.model.{Session, Issue, Account}
|
||||
import gitbucket.core.model.{Session, Account}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService}
|
||||
import gitbucket.core.service.SystemSettingsService
|
||||
import gitbucket.core.servlet.Database
|
||||
import gitbucket.core.view.Markdown
|
||||
|
||||
import scala.concurrent._
|
||||
import scala.util.{Success, Failure}
|
||||
@@ -13,87 +12,38 @@ 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 {
|
||||
/**
|
||||
* The trait for notifications.
|
||||
* This is used by notifications plugin, which provides notifications feature on GitBucket.
|
||||
* Please see the plugin for details.
|
||||
*/
|
||||
trait Notifier {
|
||||
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
|
||||
(msg: String => String)(implicit context: Context): Unit
|
||||
def toNotify(subject: String, msg: String)
|
||||
(recipients: Account => Session => Seq[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 = {
|
||||
def toNotify(subject: String, msg: String)
|
||||
(recipients: Account => Session => Seq[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) }
|
||||
database withSession { session =>
|
||||
recipients(loginAccount)(session) foreach { to =>
|
||||
send(to, subject, msg, loginAccount)
|
||||
}
|
||||
}
|
||||
"Notifications Successful."
|
||||
@@ -137,6 +87,6 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
|
||||
}
|
||||
class MockMailer extends Notifier {
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
|
||||
(msg: String => String)(implicit context: Context): Unit = {}
|
||||
def toNotify(subject: String, msg: String)
|
||||
(recipients: Account => Session => Seq[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}"
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,4 @@ object StringUtil {
|
||||
// }
|
||||
// b.toString
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>}
|
||||
@@ -61,7 +60,6 @@
|
||||
</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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<a href="@context.path/console/login.jsp" target="_blank">
|
||||
<i class="menu-icon octicon octicon-database"></i>
|
||||
<span>H2 console</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-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,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>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Organization"){
|
||||
@gitbucket.core.helper.html.dropdown("Organization", filter = ("organization", "Find Organization...")){
|
||||
@groups.map { group =>
|
||||
<li>
|
||||
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
|
||||
@@ -60,4 +60,4 @@
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
|
||||
@issues.map { case IssueInfo(issue, labels, milestone, priority, commentCount, commitStatus) =>
|
||||
<tr>
|
||||
<td style="padding-top: 12px; padding-bottom: 12px;">
|
||||
<a href="@context.path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
} else {
|
||||
<li><form class="sidebar-form"><input type="text" id="filter-box" class="form-control input-sm" placeholder="Find repository"/></form></li>
|
||||
@userRepositories.zipWithIndex.map { case (repository, i) =>
|
||||
<li class="repo-link">
|
||||
<li class="repo-link menu-item-hover">
|
||||
@if(repository.owner == context.loginAccount.get.userName){
|
||||
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span class="strong">@repository.name</span></a>
|
||||
} else {
|
||||
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) @repository.owner/<span class="strong">@repository.name</span></a>
|
||||
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span>@repository.owner/<span class="strong">@repository.name</span></span></a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
@@ -30,8 +30,8 @@
|
||||
} else {
|
||||
<li><form class="sidebar-form"><input type="text" id="filter-box" class="form-control input-sm" placeholder="Find repository"/></form></li>
|
||||
@recentRepositories.zipWithIndex.map { case (repository, i) =>
|
||||
<li class="repo-link">
|
||||
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) @repository.owner/<span class="strong">@repository.name</span></a>
|
||||
<li class="repo-link menu-item-hover">
|
||||
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span>@repository.owner/<span class="strong">@repository.name</span></span></a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -49,10 +49,7 @@ $(function(){
|
||||
$('#filter-box').keyup(function(){
|
||||
var inputVal = $('#filter-box').val();
|
||||
$.each($('li.repo-link a'), function(index, elem) {
|
||||
console.log(inputVal);
|
||||
console.log(elem.text.trim());
|
||||
console.log(elem.text.trim().lastIndexOf(inputVal, 0));
|
||||
if (!inputVal || !elem.text.trim() || elem.text.trim().indexOf(inputVal) >= 0) {
|
||||
if ( !inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >= 0 ) {
|
||||
$(elem).parent().show();
|
||||
} else {
|
||||
$(elem).parent().hide();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
$('#branch-control-input').keyup(function() {
|
||||
var inputVal = $('#branch-control-input').val();
|
||||
$.each($('#branch-control-input').parent().parent().find('a'), function(index, elem) {
|
||||
if (!inputVal || !elem.text.trim() || elem.text.trim().match(new RegExp(inputVal,'i'))) {
|
||||
if (!inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >= 0) {
|
||||
$(elem).parent().show();
|
||||
} else {
|
||||
$(elem).parent().hide();
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<div class="input-group" style="margin-bottom: 0px;">
|
||||
@html
|
||||
<span class="input-group-btn">
|
||||
<span id="@copyButtonId" class="btn btn-default" @if(style.nonEmpty){style="@style"}
|
||||
<span id="@copyButtonId" class="btn btn-sm btn-default" @if(style.nonEmpty){style="@style"}
|
||||
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
} else {
|
||||
<span id="@copyButtonId" class="btn btn-default" @if(style.nonEmpty){style="@style"}
|
||||
<span id="@copyButtonId" class="btn btn-sm btn-default" @if(style.nonEmpty){style="@style"}
|
||||
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
|
||||
}
|
||||
<script>
|
||||
@@ -18,7 +18,7 @@
|
||||
if (document.queryCommandSupported('copy')) {
|
||||
var title = $('#@copyButtonId').attr('title');
|
||||
$('#@copyButtonId').tooltip({
|
||||
@* if default container is used then 2 lines tooltip text is displayd because tooptip width is narrow. *@
|
||||
@* if default container is used then 2 lines tooltip text is displayed because tooptip width is narrow. *@
|
||||
container: 'body'
|
||||
});
|
||||
$('#@copyButtonId').on('click', function() {
|
||||
|
||||
@@ -286,11 +286,11 @@ $(function(){
|
||||
var table = diffText.closest("table[data-diff-id]");
|
||||
var i = table.data("diff-id");
|
||||
var ignoreWhiteSpace = table.find('.ignore-whitespace').prop('checked');
|
||||
diffUsingJS('oldText-'+i, 'newText-'+i, diffText.attr('id'), viewType, ignoreWhiteSpace);
|
||||
diffUsingJS('oldText-' + i, 'newText-' + i, diffText.attr('id'), viewType, ignoreWhiteSpace);
|
||||
var add = diffText.find("table").attr("add") * 1;
|
||||
var del = diffText.find("table").attr("del") * 1;
|
||||
table.find(".diffstat").text(add+del+" ").append(renderStatBar(add,del)).attr("title",add+" additions & "+del+" deletions").tooltip();
|
||||
$('span.diffstat[data-diff-id="'+i+'"]')
|
||||
table.find(".diffstat").text(add + del + " ").append(renderStatBar(add, del)).attr("title", add + " additions & " + del + " deletions").tooltip();
|
||||
$('span.diffstat[data-diff-id="' + i + '"]')
|
||||
.html('<span class="text-diff-added">+' + add + '</span><span class="text-diff-deleted">-' + del + '</span>')
|
||||
.append(renderStatBar(add, del).attr('title', (add + del) + " lines changed").tooltip());
|
||||
|
||||
|
||||
@@ -2,43 +2,45 @@
|
||||
prefix: String = "",
|
||||
style : String = "",
|
||||
right : Boolean = false,
|
||||
filter: String = "")(body: Html)
|
||||
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
|
||||
<button
|
||||
class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">
|
||||
@if(value.isEmpty){
|
||||
<i class="octicon octicon-gear"></i>
|
||||
} else {
|
||||
@if(prefix.nonEmpty){
|
||||
<span class="muted">@prefix:</span>
|
||||
}
|
||||
<span class="strong">@value</span>
|
||||
}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu@if(right){ pull-right}">
|
||||
@if(filter.nonEmpty) {
|
||||
<li><input id="@filter-input" type="text" class="form-control input-sm dropdown-filter-input" placeholder="Filter"/></li>
|
||||
}
|
||||
@body
|
||||
</ul>
|
||||
</div>
|
||||
@if(filter.nonEmpty) {
|
||||
<script>
|
||||
$(function(){
|
||||
$('#@{filter}-input').parent().click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
$('#@{filter}-input').keyup(function() {
|
||||
var inputVal = $('#@{filter}-input').val();
|
||||
$.each($('#@{filter}-input').parent().parent().find('a'), function(index, elem) {
|
||||
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
|
||||
$(elem).parent().show();
|
||||
filter: (String, String) = ("",""))(body: Html)
|
||||
@defining(if(filter._1.isEmpty) "" else filter._1 + "-" + scala.util.Random.alphanumeric.take(4).mkString){ filterId =>
|
||||
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
|
||||
<button
|
||||
class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">
|
||||
@if(value.isEmpty){
|
||||
<i class="octicon octicon-gear"></i>
|
||||
} else {
|
||||
$(elem).parent().hide();
|
||||
@if(prefix.nonEmpty){
|
||||
<span class="muted">@prefix:</span>
|
||||
}
|
||||
<span class="strong">@value</span>
|
||||
}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu@if(right){ pull-right}">
|
||||
@if(filterId.nonEmpty) {
|
||||
<li><input id="@filterId-input" type="text" class="form-control input-sm dropdown-filter-input" placeholder="@filter._2"/></li>
|
||||
}
|
||||
@body
|
||||
</ul>
|
||||
</div>
|
||||
@if(filterId.nonEmpty) {
|
||||
<script>
|
||||
$(window).load(function(){
|
||||
$('#@{filterId}-input').parent().click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
$('#@{filterId}-input').keyup(function() {
|
||||
var inputVal = $('#@{filterId}-input').val();
|
||||
$.each($('#@{filterId}-input').parent().parent().find('a'), function(index, elem) {
|
||||
if ( !inputVal || !elem.text.trim() || elem.text.trim().toLowerCase().indexOf(inputVal.toLowerCase()) >=0 ) {
|
||||
$(elem).parent().show();
|
||||
} else {
|
||||
$(elem).parent().hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
@import gitbucket.core.view.helpers
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
|
||||
<id>tag:@context.host,2013:gitbucket</id>
|
||||
<title>Gitbucket's activities</title>
|
||||
<title>GitBucket's activities</title>
|
||||
<link type="application/atom+xml" rel="self" href="@context.baseUrl/activities.atom"/>
|
||||
<author>
|
||||
<name>Gitbucket</name>
|
||||
<name>GitBucket</name>
|
||||
<uri>@context.baseUrl</uri>
|
||||
</author>
|
||||
<updated>@helpers.datetimeRFC3339(if(activities.isEmpty) new java.util.Date else activities.map(_.activityDate).max)</updated>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-tabs fill-width" style="margin-bottom: 10px;">
|
||||
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
|
||||
<li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
|
||||
<li><a href="#tab@(uid + 1)" data-toggle="tab" id="preview@uid">Preview</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" style="margin-top: 4px;" id="tab@uid">
|
||||
@@ -32,7 +32,7 @@
|
||||
generateScript = !enableWikiLink
|
||||
)(textarea)
|
||||
</div>
|
||||
<div class="tab-pane" id="tab@(uid+1)">
|
||||
<div class="tab-pane" id="tab@(uid + 1)">
|
||||
<div class="markdown-body" id="preview-area@uid">
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,6 +44,7 @@
|
||||
$(function(){
|
||||
@if(elastic){
|
||||
$('#content@uid').elastic();
|
||||
$('#content@uid').trigger('blur');
|
||||
}
|
||||
|
||||
$('#preview@uid').click(function(){
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@(collaborators: List[String],
|
||||
milestones: List[gitbucket.core.model.Milestone],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
defaultPriority: Option[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
isManageable: Boolean,
|
||||
content: String,
|
||||
@@ -29,7 +31,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, isManageable, repository)
|
||||
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), priorities, defaultPriority, labels, isManageable, repository)
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
@(content: String, commentId: Int,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
<span id="error-edit-content-@commentId" class="error"></span>
|
||||
@gitbucket.core.helper.html.attached(repository, "issues"){
|
||||
<textarea id="edit-content-@commentId" class="form-control">@content</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="cancel-comment-@commentId" class="btn btn-danger" value="Cancel"/>
|
||||
<input type="button" id="update-comment-@commentId" class="btn btn-default pull-right" value="Update comment"/>
|
||||
@gitbucket.core.helper.html.preview(
|
||||
repository = repository,
|
||||
content = content,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true,
|
||||
completionContext = "issues",
|
||||
style = "",
|
||||
elastic = true,
|
||||
tabIndex = 1
|
||||
)
|
||||
<div class="pull-right">
|
||||
<input type="button" id="cancel-comment-@commentId" class="btn btn-default" value="Cancel"/>
|
||||
<input type="button" id="update-comment-@commentId" class="btn btn-success" value="Update comment"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
@@ -17,13 +27,14 @@ $(function(){
|
||||
};
|
||||
|
||||
$('#update-comment-@commentId').click(function(){
|
||||
var content = $(this).parent().parent().find('textarea[name=content]').val();
|
||||
$('#update-comment-@commentId, #cancel-comment-@commentId').attr('disabled', 'disabled');
|
||||
$.ajax({
|
||||
url: '@context.path/@repository.owner/@repository.name/issue_comments/edit/@commentId',
|
||||
type: 'POST',
|
||||
data: {
|
||||
issueId : 0, // TODO
|
||||
content : $('#edit-content-@commentId').val()
|
||||
content : content
|
||||
}
|
||||
}).done(
|
||||
callback
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
@(content: Option[String], issueId: Int,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.helper.html.attached(repository, "issues"){
|
||||
<textarea id="edit-content" class="form-control">@content.getOrElse("")</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="cancel-issue" class="btn btn-danger" value="Cancel"/>
|
||||
<input type="button" id="update-issue" class="btn btn-default pull-right" value="Update comment"/>
|
||||
@gitbucket.core.helper.html.preview(
|
||||
repository = repository,
|
||||
content = content.getOrElse(""),
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true,
|
||||
completionContext = "issues",
|
||||
style = "",
|
||||
elastic = true,
|
||||
tabIndex = 1
|
||||
)
|
||||
<div class="pull-right">
|
||||
<input type="button" id="cancel-issue" class="btn btn-default" value="Cancel"/>
|
||||
<input type="button" id="update-issue" class="btn btn-success" value="Update comment"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
var callback = function(data){
|
||||
$('#update, #cancel').removeAttr('disabled');
|
||||
$('#issueContent').empty().html(data.content);
|
||||
prettyPrint();
|
||||
};
|
||||
|
||||
$('#update-issue').click(function(){
|
||||
var content = $(this).parent().parent().find('textarea[name=content]').val();
|
||||
$('#update, #cancel').attr('disabled', 'disabled');
|
||||
$.ajax({
|
||||
url: '@context.path/@repository.owner/@repository.name/issues/edit/@issueId',
|
||||
type: 'POST',
|
||||
data: {
|
||||
content : $('#edit-content').val()
|
||||
}
|
||||
data: { content : content }
|
||||
}).done(
|
||||
callback
|
||||
).fail(function(req) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
issueLabels: List[gitbucket.core.model.Label],
|
||||
collaborators: List[String],
|
||||
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
isEditable: Boolean,
|
||||
isManageable: Boolean,
|
||||
@@ -54,7 +55,7 @@
|
||||
@gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, isManageable, repository)
|
||||
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
issueLabels: List[gitbucket.core.model.Label],
|
||||
collaborators: List[String],
|
||||
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
defaultPriority: Option[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
isManageable: Boolean,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@@ -11,7 +13,7 @@
|
||||
<span class="muted small strong">Labels</span>
|
||||
@if(isManageable){
|
||||
<div class="pull-right">
|
||||
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = "labels") {
|
||||
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("labels", "Filter Labels")) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
||||
@@ -32,11 +34,45 @@
|
||||
@gitbucket.core.issues.html.labellist(issueLabels)
|
||||
</ul>
|
||||
<hr/>
|
||||
|
||||
<div style="margin-bottom: 14px;">
|
||||
<span class="muted small strong">Priority</span>
|
||||
@if(isManageable){
|
||||
<div class="pull-right">
|
||||
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("priority", "Filter Priority")) {
|
||||
<li><a href="javascript:void(0);" class="priority" data-id=""><i class="octicon octicon-x"></i> Clear priority</a></li>
|
||||
@priorities.map { priority =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="priority" data-id="@priority.priorityId" data-name="@priority.priorityName" data-color="#@priority.color" data-font-color="#@priority.fontColor"@if(!priority.description.isEmpty) { title="@priority.description.get" }>
|
||||
@gitbucket.core.helper.html.checkicon(issue.flatMap(_.priorityId).orElse(defaultPriority.map(_.priorityId)).map(id => id == priority.priorityId).getOrElse(false))
|
||||
<span class="label" style="background-color: #@priority.color;"> </span>
|
||||
@priority.priorityName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<span id="label-priority">
|
||||
@issue.flatMap(_.priorityId).orElse(defaultPriority.map(_.priorityId)).map { priorityId =>
|
||||
@priorities.collect { case priority if(priority.priorityId == priorityId) =>
|
||||
<a class="issue-priority" style="background-color: #@priority.color; color: #@priority.fontColor;" href="@helpers.url(repository)/issues?priority=@helpers.urlEncode(priority.priorityName)&state=open"@if(!priority.description.isEmpty) { title="@priority.description.get" }>@priority.priorityName</a>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted small">No priority</span>
|
||||
}
|
||||
</span>
|
||||
@if(issue.isEmpty){
|
||||
<input type="hidden" name="priorityId" value="@defaultPriority.map(_.priorityId).map(_.toString).getOrElse("")"/>
|
||||
}
|
||||
<hr/>
|
||||
|
||||
<div style="margin-bottom: 14px;">
|
||||
<span class="muted small strong">Milestone</span>
|
||||
@if(isManageable){
|
||||
<div class="pull-right">
|
||||
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = "milestone") {
|
||||
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("milestone", "Filter Milestone")) {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="octicon octicon-x"></i> Clear this milestone</a></li>
|
||||
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
|
||||
<li>
|
||||
@@ -88,7 +124,7 @@
|
||||
<span class="muted small strong">Assignee</span>
|
||||
@if(isManageable){
|
||||
<div class="pull-right">
|
||||
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = "assignee") {
|
||||
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("assignee", "Filter Assignee")) {
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
@@ -112,6 +148,9 @@
|
||||
<input type="hidden" name="assignedUserName" value=""/>
|
||||
}
|
||||
@issue.map { issue =>
|
||||
@gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebarComponent =>
|
||||
@sidebarComponent(issue, repository, context)
|
||||
}
|
||||
<hr/>
|
||||
<div style="margin-bottom: 14px;">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
@@ -149,6 +188,20 @@ $(function(){
|
||||
);
|
||||
});
|
||||
|
||||
$('a.priority').click(function(){
|
||||
var priorityName = $(this).data('name');
|
||||
var priorityId = $(this).data('id');
|
||||
var description = $(this).attr('title');
|
||||
var color = $(this).data('color');
|
||||
var fontColor = $(this).data('font-color');
|
||||
$.post('@helpers.url(repository)/issues/@issue.issueId/priority',
|
||||
{ priorityId: priorityId },
|
||||
function(data){
|
||||
displayPriority(priorityName, priorityId, description, color, fontColor);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
@@ -185,6 +238,16 @@ $(function(){
|
||||
$('input[name=milestoneId]').val(milestoneId);
|
||||
});
|
||||
|
||||
$('a.priority').click(function(){
|
||||
var priorityName = $(this).data('name');
|
||||
var priorityId = $(this).data('id');
|
||||
var description = $(this).attr('title');
|
||||
var color = $(this).data('color');
|
||||
var fontColor = $(this).data('font-color');
|
||||
displayPriority(priorityName, priorityId, description, color, fontColor);
|
||||
$('input[name=priorityId]').val(priorityId);
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
@@ -219,6 +282,23 @@ $(function(){
|
||||
}
|
||||
}
|
||||
|
||||
function displayPriority(priorityName, priorityId, description, color, fontColor){
|
||||
$('a.priority i.octicon-check').removeClass('octicon-check');
|
||||
if(priorityId == ''){
|
||||
$('#label-priority').html($('<span class="muted small">').text('No priority'));
|
||||
} else {
|
||||
$('#label-priority').html($('<a class="issue-priority">').text(priorityName)
|
||||
.attr('href', '@helpers.url(repository)/issues?priority=' + encodeURIComponent(priorityName) + '&state=open')
|
||||
.attr('title', description)
|
||||
.css({
|
||||
"background-color": color,
|
||||
"color": fontColor
|
||||
}));
|
||||
|
||||
$('a.priority[data-id=' + priorityId + '] i').addClass('octicon-check');
|
||||
}
|
||||
}
|
||||
|
||||
function displayAssignee($this, userName){
|
||||
$('a.assign i.octicon-check').removeClass('octicon-check');
|
||||
if(userName == ''){
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
page: Int,
|
||||
collaborators: List[String],
|
||||
milestones: List[gitbucket.core.model.Milestone],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
@@ -38,7 +39,7 @@
|
||||
}
|
||||
}
|
||||
</form>
|
||||
@gitbucket.core.issues.html.listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), isManageable)
|
||||
@gitbucket.core.issues.html.listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, priorities, labels, Some(repository), isManageable)
|
||||
@if(isManageable){
|
||||
<form id="batcheditForm" method="POST">
|
||||
<input type="hidden" name="value"/>
|
||||
@@ -51,6 +52,7 @@
|
||||
@if(isManageable){
|
||||
<script>
|
||||
$(function(){
|
||||
@*
|
||||
$('a.header-link').mouseover(function(e){
|
||||
var target = e.target;
|
||||
if(e.target.tagName != 'A'){
|
||||
@@ -70,6 +72,7 @@ $(function(){
|
||||
$(target).children('img.header-icon-hover').css('display', 'none');
|
||||
$(target).children('img.header-icon' ).css('display', 'inline');
|
||||
});
|
||||
*@
|
||||
|
||||
$('.table-issues input[type=checkbox]').change(function(){
|
||||
var all = $('.table-issues input[type=checkbox][value]');
|
||||
@@ -117,6 +120,9 @@ $(function(){
|
||||
$('a.toggle-milestone').click(function(){
|
||||
submitBatchEdit('@helpers.url(repository)/issues/batchedit/milestone', $(this).data('id'));
|
||||
});
|
||||
$('a.toggle-priority').click(function(){
|
||||
submitBatchEdit('@helpers.url(repository)/issues/batchedit/priority', $(this).data('id'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||
collaborators: List[String] = Nil,
|
||||
milestones: List[gitbucket.core.model.Milestone] = Nil,
|
||||
priorities: List[gitbucket.core.model.Priority] = Nil,
|
||||
labels: List[gitbucket.core.model.Label] = Nil,
|
||||
repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None,
|
||||
isManageable: Boolean = false)(implicit context: gitbucket.core.controller.Context)
|
||||
@@ -27,7 +28,7 @@
|
||||
<th style="background-color: #eee;">
|
||||
<input type="checkbox"/>
|
||||
<span id="table-issues-control">
|
||||
@gitbucket.core.helper.html.dropdown("Author") {
|
||||
@gitbucket.core.helper.html.dropdown("Author", filter = ("author", "Find Author...")) {
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
|
||||
@@ -37,7 +38,7 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Label") {
|
||||
@gitbucket.core.helper.html.dropdown("Label", filter = ("label", "Find Label...")) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
|
||||
@@ -48,7 +49,23 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Milestone") {
|
||||
@gitbucket.core.helper.html.dropdown("Priority", filter = ("priority", "Find Priority...")) {
|
||||
<li>
|
||||
<a href="@condition.copy(priority = (if(condition.priority == Some(None)) None else Some(None))).toURL">
|
||||
@gitbucket.core.helper.html.checkicon(condition.priority == Some(None)) Issues with no priority
|
||||
</a>
|
||||
</li>
|
||||
@priorities.map { priority =>
|
||||
<li>
|
||||
<a href="@condition.copy(priority = (if(condition.priority == Some(Some(priority.priorityName))) None else Some(Some(priority.priorityName)))).toURL"@if(!priority.description.isEmpty) { title="@priority.description.get" }>
|
||||
@gitbucket.core.helper.html.checkicon(condition.priority == Some(Some(priority.priorityName)))
|
||||
<span style="background-color: #@priority.color;" class="label-color"> </span>
|
||||
@priority.priorityName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Milestone", filter = ("milestone", "Find Milestone...")) {
|
||||
<li>
|
||||
<a href="@condition.copy(milestone = (if(condition.milestone == Some(None)) None else Some(None))).toURL">
|
||||
@gitbucket.core.helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone
|
||||
@@ -62,7 +79,7 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Assignee") {
|
||||
@gitbucket.core.helper.html.dropdown("Assignee", filter = ("assignee", "Find Assignee...")) {
|
||||
<li>
|
||||
<a href="@condition.copy(assigned = (if(condition.assigned == Some(None)) None else Some(None))).toURL">
|
||||
@gitbucket.core.helper.html.checkicon(condition.assigned == Some(None)) Assigned to nobody
|
||||
@@ -88,6 +105,16 @@
|
||||
@gitbucket.core.helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="priority", direction="asc").toURL">
|
||||
@gitbucket.core.helper.html.checkicon(condition.sort == "priority" && condition.direction == "asc") Highest priority
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="priority", direction="desc" ).toURL">
|
||||
@gitbucket.core.helper.html.checkicon(condition.sort == "priority" && condition.direction == "desc") Lowest priority
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="desc").toURL">
|
||||
@gitbucket.core.helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
|
||||
@@ -116,7 +143,7 @@
|
||||
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
|
||||
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Label") {
|
||||
@gitbucket.core.helper.html.dropdown("Label", filter = ("label", "Find Label...")) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
|
||||
@@ -127,13 +154,21 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Milestone") {
|
||||
@gitbucket.core.helper.html.dropdown("Priority", filter = ("priority", "Find Priority...")) {
|
||||
<li><a href="javascript:void(0);" class="toggle-priority" data-id="">No priority</a></li>
|
||||
@priorities.map { priority =>
|
||||
<li><a href="javascript:void(0);" class="toggle-priority" data-id="@priority.priorityId"@if(!priority.description.isEmpty) { title="@priority.description.get" }>
|
||||
<span style="background-color: #@priority.color;" class="label"> </span>
|
||||
@priority.priorityName</a></li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Milestone", filter = ("milestone", "Find Milestone...")) {
|
||||
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown("Assignee") {
|
||||
@gitbucket.core.helper.html.dropdown("Assignee", filter = ("assignee", "Find Assignee...")) {
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="octicon"></i>@helpers.avatar(collaborator, 20) @collaborator</a></li>
|
||||
@@ -171,7 +206,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
|
||||
@issues.map { case IssueInfo(issue, labels, milestone, priority, commentCount, commitStatus) =>
|
||||
<tr>
|
||||
<td style="padding-top: 12px; padding-bottom: 12px;">
|
||||
@if(isManageable){
|
||||
@@ -208,6 +243,10 @@
|
||||
</span>
|
||||
<div class="small muted" style="margin-left: 12px; margin-top: 2px;">
|
||||
#@issue.issueId opened @gitbucket.core.helper.html.datetimeago(issue.registeredDate) by @helpers.user(issue.openedUserName, styleClass="username")
|
||||
@priority.map(priority => priorities.filter(p => p.priorityName == priority).head).map { priority =>
|
||||
<span style="margin: 20px;"><a href="@condition.copy(priority = Some(Some(priority.priorityName))).toURL" class="username"@if(!priority.description.isEmpty) { title="@priority.description.get" }><i class="octicon octicon-flame"></i>
|
||||
<span class="issue-priority issue-priority-inline" style="background-color: #@priority.color; color: #@priority.fontColor;">@priority.priorityName</span></a></span>
|
||||
}
|
||||
@milestone.map { milestone =>
|
||||
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
@(priority: Option[gitbucket.core.model.Priority],
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@defining(priority.map(_.priorityId).getOrElse("new")){ priorityId =>
|
||||
<div id="edit-priority-area-@priorityId">
|
||||
<form class="form-inline">
|
||||
<input type="text" id="priorityName-@priorityId" style="width: 300px; float: left; margin-right: 4px;" class="form-control" value="@priority.map(_.priorityName)"@if(priorityId == "new"){ placeholder="New priority name"}/>
|
||||
<div id="priority-color-@priorityId" class="input-group color bscp" data-color="#@priority.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
|
||||
<input type="text" class="form-control" id="priorityColor-@priorityId" value="#@priority.map(_.color).getOrElse("888888")" style="width: 100px;">
|
||||
<span class="input-group-addon"><i style="background-color: #@priority.map(_.color).getOrElse("888888");"></i></span>
|
||||
</div>
|
||||
<script>
|
||||
$('div#priority-color-@priorityId').colorpicker({format: "hex"});
|
||||
</script>
|
||||
<input type="text" id="description-@priorityId" style="width: 500px; float: left; margin-left: 4px;" class="form-control" value="@priority.flatMap(_.description).getOrElse("")" placeholder="Description..." />
|
||||
<span class="pull-right">
|
||||
<span id="priority-error-@priorityId" class="error"></span>
|
||||
<input type="button" id="cancel-@priorityId" class="btn btn-default priority-edit-cancel" value="Cancel">
|
||||
<input type="button" id="submit-@priorityId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(priorityId == "new") "Create priority" else "Save changes")"/>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#submit-@priorityId').click(function(e){
|
||||
$.post('@helpers.url(repository)/issues/priorities/@{if(priorityId == "new") "new" else priorityId + "/edit"}', {
|
||||
'priorityName' : $('#priorityName-@priorityId').val(),
|
||||
'description' : $('#description-@priorityId').val(),
|
||||
'priorityColor': $('#priorityColor-@priorityId').val()
|
||||
}, function(data, status){
|
||||
$('div#edit-priority-area-@priorityId').remove();
|
||||
@if(priorityId == "new"){
|
||||
$('#new-priority-table').hide();
|
||||
// Insert row into the top of table
|
||||
$('#priorities-table tbody').append(data);
|
||||
} else {
|
||||
// Replace table row
|
||||
$('#priority-row-@priorityId').after(data).remove();
|
||||
}
|
||||
$('#priority-no-priorities').hide();
|
||||
updatePriorityCount();
|
||||
}).fail(function(xhr, status, error){
|
||||
var errors = JSON.parse(xhr.responseText);
|
||||
if(errors.priorityName){
|
||||
$('span#priority-error-@priorityId').text(errors.priorityName);
|
||||
} else if(errors.description){
|
||||
$('span#priority-error-@priorityId').text(errors.description);
|
||||
} else if(errors.priorityColor){
|
||||
$('span#priority-error-@priorityId').text(errors.priorityColor);
|
||||
} else {
|
||||
$('span#priority-error-@priorityId').text('error');
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#cancel-@priorityId').click(function(e){
|
||||
$('div#edit-priority-area-@priorityId').remove();
|
||||
@if(priorityId == "new"){
|
||||
$('#new-priority-table').hide();
|
||||
} else {
|
||||
$('#priority-@priorityId').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
124
src/main/twirl/gitbucket/core/issues/priorities/list.scala.html
Normal file
124
src/main/twirl/gitbucket/core/issues/priorities/list.scala.html
Normal file
@@ -0,0 +1,124 @@
|
||||
@(priorities: List[gitbucket.core.model.Priority],
|
||||
counts: Map[String, Int],
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"Priorities - ${repository.owner}/${repository.name}"){
|
||||
@gitbucket.core.html.menu("priorities", repository){
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right" style="margin-bottom: 10px;">
|
||||
<a class="btn btn-success" href="javascript:void(0);" id="new-priority-button">New priority</a>
|
||||
</div>
|
||||
}
|
||||
<table class="table table-bordered table-hover table-issues" id="new-priority-table" style="display: none;">
|
||||
<tr><td></td></tr>
|
||||
</table>
|
||||
<table id="priorities-table" class="table table-bordered table-hover table-issues">
|
||||
<thead>
|
||||
<tr id="priority-row-header">
|
||||
<th style="background-color: #eee;">
|
||||
<span class="small">@priorities.size priorities</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@priorities.map { priority =>
|
||||
@gitbucket.core.issues.priorities.html.priority(priority, counts, repository, hasWritePermission)
|
||||
}
|
||||
<tr id="priority-no-priorities" @if(!priorities.isEmpty){ style="display:none" }>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
No priorities to show.
|
||||
@if(hasWritePermission){
|
||||
Click on the "New priority" button above to create one.
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#new-priority-button').click(function(e){
|
||||
if($('#edit-priority-area-new').size() != 0){
|
||||
$('div#edit-priority-area-new').remove();
|
||||
$('#new-priority-table').hide();
|
||||
} else {
|
||||
$.get('@helpers.url(repository)/issues/priorities/new',
|
||||
function(data){
|
||||
$('#new-priority-table').show().find('tr td').append(data);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@if(hasWritePermission){
|
||||
$('#priorities-table tbody').sortable({
|
||||
axis: 'y',
|
||||
cursor: 'move',
|
||||
helper: function (e, ui) {
|
||||
ui.children().each(function() {
|
||||
$(this).width($(this).width());
|
||||
});
|
||||
return ui;
|
||||
},
|
||||
handle: '.priority-sort-handle',
|
||||
update: function() {
|
||||
var ids = [];
|
||||
$('tr.priority-row').each(function(idx, elem) {
|
||||
ids.push($(elem).attr('id').replace('priority-row-', ''));
|
||||
});
|
||||
$.post('@helpers.url(repository)/issues/priorities/reorder', {
|
||||
'order' : ids.join(',')
|
||||
}).fail(function(xhr, status, error){
|
||||
alert('Unable to reorder priorities.');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function deletePriority(priorityId){
|
||||
if(confirm('Once you delete this priority, there is no going back.\nAre you sure?')){
|
||||
$.post('@helpers.url(repository)/issues/priorities/' + priorityId + '/delete', function(){
|
||||
$('tr#priority-row-' + priorityId).remove();
|
||||
if ($('tr.priority-row').size() == 0) {
|
||||
$('#priority-no-priorities').show();
|
||||
}
|
||||
updatePriorityCount();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function editPriority(priorityId){
|
||||
$.get('@helpers.url(repository)/issues/priorities/' + priorityId + '/edit',
|
||||
function(data){
|
||||
$('#priority-' + priorityId).hide().parent().append(data);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updatePriorityCount() {
|
||||
var $counter = $('#priority-row-header span');
|
||||
$counter.text($counter.text().replace(/\d+/, $('tr.priority-row').size()));
|
||||
}
|
||||
|
||||
function setDefaultPriority(priorityId){
|
||||
var $a = $('a[data-id="' + priorityId + '"].priority-default');
|
||||
var isDefault = $a.data('default');
|
||||
$.post('@helpers.url(repository)/issues/priorities/default',
|
||||
{ priorityId: isDefault ? '' : priorityId },
|
||||
function(){
|
||||
$('.priority-default')
|
||||
.removeClass('priority-default-selected')
|
||||
.data('default', false)
|
||||
.attr('title', 'Set as default');
|
||||
if (!isDefault) {
|
||||
$a
|
||||
.addClass('priority-default-selected')
|
||||
.data('default', true)
|
||||
.attr('title', 'Unset as default');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
@(priority: gitbucket.core.model.Priority,
|
||||
counts: Map[String, Int],
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
<tr id="priority-row-@priority.priorityId" class="priority-row">
|
||||
<td style="padding-top: 15px; padding-bottom: 15px;">
|
||||
<div class="milestone row" id="priority-@priority.priorityId">
|
||||
<div class="col-md-2">
|
||||
@if(hasWritePermission) {
|
||||
<div class="pull-left priority-sort-handle" style="margin-top: 3px"><i class="octicon octicon-grabber"></i></div>
|
||||
}
|
||||
<div style="margin-top: 6px">
|
||||
<a href="@helpers.url(repository)/issues?priority=@helpers.urlEncode(priority.priorityName)&state=open" id="priority-row-content-@priority.priorityId">
|
||||
<span style="background-color: #@priority.color; color: #@priority.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
|
||||
<i class="octicon octicon-flame" style="color: #@priority.fontColor;"></i> @priority.priorityName
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="@if(hasWritePermission){col-md-6} else {col-md-8}">
|
||||
<span>@priority.description.getOrElse("")</span>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div class="pull-right">
|
||||
@if(hasWritePermission){
|
||||
<a href="javascript:void(0);" onclick="setDefaultPriority(@priority.priorityId)" data-id="@priority.priorityId" data-default="@priority.isDefault" class="priority-default@if(priority.isDefault){ priority-default-selected}" title="@if(priority.isDefault){Unset as default} else {Set as default}"><i class="octicon octicon-star"></i></a>
|
||||
} else if(priority.isDefault) {
|
||||
<i class="octicon octicon-star priority-default priority-default-selected" title="Default"></i>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div class="pull-right">
|
||||
<span class="muted">@counts.get(priority.priorityName).getOrElse(0) open issues</span>
|
||||
</div>
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
<div class="col-md-2">
|
||||
<div class="pull-right">
|
||||
<a href="javascript:void(0);" onclick="editPriority(@priority.priorityId)">Edit</a>
|
||||
|
||||
<a href="javascript:void(0);" onclick="deletePriority(@priority.priorityId)">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -15,11 +15,15 @@
|
||||
<link href="@helpers.assets("/vendors/colorpicker/css/bootstrap-colorpicker.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/vendors/google-code-prettify/prettify.css")" type="text/css" rel="stylesheet"/>
|
||||
<link href="@helpers.assets("/vendors/facebox/facebox.css")" rel="stylesheet"/>
|
||||
<link href="@helpers.assets("/vendors/AdminLTE-2.3.8/css/AdminLTE.min.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/vendors/AdminLTE-2.3.8/css/skins/skin-blue.min.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/vendors/AdminLTE-2.3.11/css/AdminLTE.min.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/vendors/AdminLTE-2.3.11/css/skins/skin-blue.min.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/vendors/font-awesome-4.6.3/css/font-awesome.min.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.min.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.structure.min.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.theme.min.css")" rel="stylesheet">
|
||||
<link href="@helpers.assets("/common/css/gitbucket.css")" rel="stylesheet">
|
||||
<script src="@helpers.assets("/vendors/jquery/jquery-1.12.2.min.js")"></script>
|
||||
<script src="@helpers.assets("/vendors/jquery-ui/jquery-ui.min.js")"></script>
|
||||
<script src="@helpers.assets("/vendors/dropzone/dropzone.js")"></script>
|
||||
<script src="@helpers.assets("/common/js/validation.js")"></script>
|
||||
<script src="@helpers.assets("/common/js/gitbucket.js")"></script>
|
||||
@@ -36,13 +40,13 @@
|
||||
@repository.map { repository =>
|
||||
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
|
||||
}
|
||||
<script src="@helpers.assets("/vendors/AdminLTE-2.3.8/js/app.js")" type="text/javascript"></script>
|
||||
<script src="@helpers.assets("/vendors/AdminLTE-2.3.11/js/app.js")" type="text/javascript"></script>
|
||||
</head>
|
||||
<body class="skin-blue page-load @if(context.sidebarCollapse){sidebar-collapse}">
|
||||
<body class="skin-blue page-load @if(body.toString.contains("menu-item-hover")){sidebar-mini} @if(context.sidebarCollapse){sidebar-collapse}">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<a href="@context.path/" class="logo">
|
||||
<img src="@helpers.assets("/common/images/gitbucket.png")" style="width: 24px; height: 24px; display: inline;"/>
|
||||
<img src="@helpers.assets("/common/images/gitbucket.svg")" style="width: 24px; height: 24px; display: inline;"/>
|
||||
GitBucket
|
||||
<span class="header-version">@gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion</span>
|
||||
</a>
|
||||
@@ -72,26 +76,34 @@
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
@if(context.loginAccount.isDefined){
|
||||
<li class="dropdown">
|
||||
<li class="dropdown notifications-menu">
|
||||
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#">
|
||||
<i class="octicon octicon-plus" style="color: black;"></i><span class="caret" style="color: black; vertical-align: middle;"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li><a href="@context.path/new">New repository</a></li>
|
||||
<li><a href="@context.path/groups/new">New group</a></li>
|
||||
<ul class="dropdown-menu pull-right" style="width: auto;">
|
||||
<li>
|
||||
<ul class="menu">
|
||||
<li><a href="@context.path/new">New repository</a></li>
|
||||
<li><a href="@context.path/groups/new">New group</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<li class="dropdown notifications-menu">
|
||||
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#" data-toggle="tooltip" data-placement="bottom" title="Signed is as @context.loginAccount.get.userName">
|
||||
@helpers.avatar(context.loginAccount.get.userName, 16)<span class="caret" style="color: black; vertical-align: middle;"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li><a href="@helpers.url(context.loginAccount.get.userName)">Your profile</a></li>
|
||||
<li><a href="@helpers.url(context.loginAccount.get.userName)/_edit">Account settings</a></li>
|
||||
@if(context.loginAccount.get.isAdmin){
|
||||
<li><a href="@context.path/admin/users">System administration</a></li>
|
||||
}
|
||||
<li><a href="@context.path/signout">Sign out</a></li>
|
||||
<ul class="dropdown-menu pull-right" style="width: auto;">
|
||||
<li>
|
||||
<ul class="menu">
|
||||
<li><a href="@helpers.url(context.loginAccount.get.userName)">Your profile</a></li>
|
||||
<li><a href="@helpers.url(context.loginAccount.get.userName)/_edit">Account settings</a></li>
|
||||
@if(context.loginAccount.get.isAdmin){
|
||||
<li><a href="@context.path/admin/users">System administration</a></li>
|
||||
}
|
||||
<li><a href="@context.path/signout">Sign out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
} else {
|
||||
|
||||
@@ -6,83 +6,96 @@
|
||||
@import gitbucket.core.view.helpers
|
||||
|
||||
@menuitem(path: String, name: String, label: String, icon: String, count: Int = 0) = {
|
||||
<li @if(active == name){class="active"}>
|
||||
<li class = "menu-item-hover @if(active == name){active}">
|
||||
@if(path.startsWith("http")){
|
||||
<a href="@path" target="_blank">
|
||||
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right-container">@count</span> }
|
||||
<i class="menu-icon octicon octicon-@icon"></i>
|
||||
<span>@label</span>
|
||||
@if(count > 0){
|
||||
<span class="pull-right-container"><span class="label label-primary pull-right-container">@count</span></span>
|
||||
}
|
||||
</a>
|
||||
} else {
|
||||
<a href="@helpers.url(repository)@path">
|
||||
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right-container">@count</span> }
|
||||
<i class="menu-icon octicon octicon-@icon"></i>
|
||||
<span>@label</span>
|
||||
@if(count > 0) {
|
||||
<span class="pull-right-container"><span class="label label-primary pull-right">@count</span></span>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
|
||||
<div class="main-sidebar">
|
||||
<div class="sidebar">
|
||||
<ul class="sidebar-menu">
|
||||
@menuitem("", "files", "Files", "code")
|
||||
@if(repository.branchList.nonEmpty) {
|
||||
@menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length)
|
||||
@menuitem("/tags", "tags", "Tags", "tag", repository.tags.length)
|
||||
<div class="main-sidebar">
|
||||
<div class="sidebar">
|
||||
<ul class="sidebar-menu">
|
||||
@menuitem("", "files", "Files", "code")
|
||||
@if(repository.branchList.nonEmpty) {
|
||||
@menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length)
|
||||
@menuitem("/tags", "tags", "Tags", "tag", repository.tags.length)
|
||||
}
|
||||
@if(repository.repository.options.issuesOption != "DISABLE") {
|
||||
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
|
||||
@menuitem("/pulls", "pulls", "Pull requests", "git-pull-request", repository.pullCount)
|
||||
@menuitem("/issues/labels", "labels", "Labels", "tag")
|
||||
@menuitem("/issues/priorities", "priorities", "Priorities", "flame")
|
||||
@menuitem("/issues/milestones", "milestones", "Milestones", "milestone")
|
||||
} else {
|
||||
@repository.repository.options.externalIssuesUrl.map { externalIssuesUrl =>
|
||||
@menuitem(externalIssuesUrl, "issues", "Issues", "issue-opened")
|
||||
}
|
||||
@if(repository.repository.options.issuesOption != "DISABLE") {
|
||||
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
|
||||
@menuitem("/pulls", "pulls", "Pull requests", "git-pull-request", repository.pullCount)
|
||||
@menuitem("/issues/labels", "labels", "Labels", "tag")
|
||||
@menuitem("/issues/milestones", "milestones", "Milestones", "milestone")
|
||||
} else {
|
||||
@repository.repository.options.externalIssuesUrl.map { externalIssuesUrl =>
|
||||
@menuitem(externalIssuesUrl, "issues", "Issues", "issue-opened")
|
||||
}
|
||||
}
|
||||
@if(repository.repository.options.wikiOption != "DISABLE") {
|
||||
@menuitem("/wiki", "wiki", "Wiki", "book")
|
||||
} else {
|
||||
@repository.repository.options.externalWikiUrl.map { externalWikiUrl =>
|
||||
@menuitem(externalWikiUrl, "wiki", "Wiki", "book")
|
||||
}
|
||||
@if(repository.repository.options.wikiOption != "DISABLE") {
|
||||
@menuitem("/wiki", "wiki", "Wiki", "book")
|
||||
} else {
|
||||
@repository.repository.options.externalWikiUrl.map { externalWikiUrl =>
|
||||
@menuitem(externalWikiUrl, "wiki", "Wiki", "book")
|
||||
}
|
||||
}
|
||||
@if(repository.repository.options.allowFork) {
|
||||
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
|
||||
}
|
||||
@if(context.loginAccount.isDefined && (context.loginAccount.get.isAdmin || repository.managers.contains(context.loginAccount.get.userName))){
|
||||
@menuitem("/settings", "settings", "Settings", "tools")
|
||||
}
|
||||
@gitbucket.core.plugin.PluginRegistry().getRepositoryMenus.map { menu =>
|
||||
@menu(repository, context).map { link =>
|
||||
@menuitem(link.path, link.id, link.label, link.icon.getOrElse("ruby"))
|
||||
}
|
||||
@if(repository.repository.options.allowFork) {
|
||||
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
|
||||
}
|
||||
@if(context.loginAccount.isDefined && (context.loginAccount.get.isAdmin || repository.managers.contains(context.loginAccount.get.userName))){
|
||||
@menuitem("/settings", "settings", "Settings", "tools")
|
||||
}
|
||||
@gitbucket.core.plugin.PluginRegistry().getRepositoryMenus.map { menu =>
|
||||
@menu(repository, context).map { link =>
|
||||
@menuitem(link.path, link.id, link.label, link.icon.getOrElse("ruby"))
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="content body clearfix">
|
||||
<div class="headbar">
|
||||
<div class="container">
|
||||
@gitbucket.core.helper.html.information(info)
|
||||
@gitbucket.core.helper.html.error(error)
|
||||
<div class="head">
|
||||
@gitbucket.core.helper.html.repositoryicon(repository, true)
|
||||
<a href="@helpers.url(repository.owner)">@repository.owner</a> / <a href="@helpers.url(repository)" class="strong">@repository.name</a>
|
||||
|
||||
@defining(repository.repository){ x =>
|
||||
@if(repository.repository.originRepositoryName.isDefined){
|
||||
<div class="forked">
|
||||
forked from <a href="@context.path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
|
||||
</div>
|
||||
}
|
||||
@x.description.map { description =>
|
||||
<div class="normal muted" style="margin-left: 36px; font-size: 80%;">@Html(helpers.detectAndRenderLinks(description, repository))</div>
|
||||
}
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="content body clearfix">
|
||||
<div class="headbar">
|
||||
<div class="container">
|
||||
@gitbucket.core.helper.html.information(info)
|
||||
@gitbucket.core.helper.html.error(error)
|
||||
<div class="head">
|
||||
<div class="pull-right">
|
||||
@gitbucket.core.plugin.PluginRegistry().getRepositoryHeaders.map { repositoryHeaderComponent =>
|
||||
@repositoryHeaderComponent(repository, context)
|
||||
}
|
||||
</div>
|
||||
@gitbucket.core.helper.html.repositoryicon(repository, true)
|
||||
<a href="@helpers.url(repository.owner)">@repository.owner</a> / <a href="@helpers.url(repository)" class="strong">@repository.name</a>
|
||||
|
||||
@defining(repository.repository){ x =>
|
||||
@if(repository.repository.originRepositoryName.isDefined){
|
||||
<div class="forked">
|
||||
forked from <a href="@context.path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
|
||||
</div>
|
||||
}
|
||||
@x.description.map { description =>
|
||||
<div class="normal muted" style="margin-left: 36px; font-size: 80%;">@Html(helpers.detectAndRenderLinks(description, repository))</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@body
|
||||
</div>
|
||||
@body
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,29 +14,30 @@
|
||||
hasOriginWritePermission: Boolean,
|
||||
collaborators: List[String],
|
||||
milestones: List[gitbucket.core.model.Milestone],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"Pull requests - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@gitbucket.core.html.menu("pulls", repository){
|
||||
<div class="pullreq-info">
|
||||
<div id="compare-edit">
|
||||
@gitbucket.core.helper.html.dropdown(originRepository.owner + "/" + originRepository.name, "base fork") {
|
||||
@gitbucket.core.helper.html.dropdown(originRepository.owner + "/" + originRepository.name, "base fork", filter=("origin_repo", "Find Repository...")) {
|
||||
@members.map { case (owner, name) =>
|
||||
<li><a href="#" class="origin-owner" data-owner="@owner" data-name="@name">@gitbucket.core.helper.html.checkicon(owner == originRepository.owner) @owner/@name</a></li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown(originId, "base") {
|
||||
@gitbucket.core.helper.html.dropdown(originId, "base", filter=("origin_branch", "Find Branch...")) {
|
||||
@originRepository.branchList.map { branch =>
|
||||
<li><a href="#" class="origin-branch" data-branch="@helpers.encodeRefName(branch)">@gitbucket.core.helper.html.checkicon(branch == originId) @branch</a></li>
|
||||
}
|
||||
}
|
||||
...
|
||||
@gitbucket.core.helper.html.dropdown(forkedRepository.owner + "/" + forkedRepository.name, "head fork") {
|
||||
@gitbucket.core.helper.html.dropdown(forkedRepository.owner + "/" + forkedRepository.name, "head fork", filter=("forked_repo", "Find Repository...")) {
|
||||
@members.map { case (owner, name) =>
|
||||
<li><a href="#" class="forked-owner" data-owner="@owner" data-name="@name">@gitbucket.core.helper.html.checkicon(owner == forkedRepository.owner) @owner/@name</a></li>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.helper.html.dropdown(forkedId, "compare") {
|
||||
@gitbucket.core.helper.html.dropdown(forkedId, "compare", filter=("forked_branch", "Find Branch...")) {
|
||||
@forkedRepository.branchList.map { branch =>
|
||||
<li><a href="#" class="forked-branch" data-branch="@helpers.encodeRefName(branch)">@gitbucket.core.helper.html.checkicon(branch == forkedId) @branch</a></li>
|
||||
}
|
||||
@@ -81,7 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), labels, hasOriginWritePermission, repository)
|
||||
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), priorities, None, labels, hasOriginWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user